Rehost annual stock repurchase policy branch

This commit is contained in:
Jan Petykiewicz 2026-04-18 00:28:54 -07:00
commit 0658626a57
8 changed files with 592 additions and 17 deletions

View file

@ -95,6 +95,9 @@ headlessly as a pure runtime reader over owned annual-finance state, support-adj
and current world finance policy rather than as a notes-only atlas fragment. The later deep- and current world finance policy rather than as a notes-only atlas fragment. The later deep-
distress bankruptcy fallback is now rehosted on that same owner surface too, using the save-native distress bankruptcy fallback is now rehosted on that same owner surface too, using the save-native
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 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
pure reader over owned save-native state instead of a guessed finance-side approximation.
The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we
should prefer rehosting the owning source state or the real reader/setter family rather than should prefer rehosting the owning source state or the real reader/setter family rather than
guessing one more derived leaf field from nearby offsets. A checked-in guessing one more derived leaf field from nearby offsets. A checked-in

View file

@ -108,6 +108,7 @@ struct SaveSliceProjection {
has_chairman_selection_override: bool, has_chairman_selection_override: bool,
selected_chairman_profile_id: Option<u32>, selected_chairman_profile_id: Option<u32>,
chairman_issue_opinion_terms_raw_i32: BTreeMap<u32, Vec<i32>>, chairman_issue_opinion_terms_raw_i32: BTreeMap<u32, Vec<i32>>,
chairman_personality_raw_u8: BTreeMap<u32, u8>,
candidate_availability: BTreeMap<String, u32>, candidate_availability: BTreeMap<String, u32>,
named_locomotive_availability: BTreeMap<String, u32>, named_locomotive_availability: BTreeMap<String, u32>,
locomotive_catalog: Option<Vec<RuntimeLocomotiveCatalogEntry>>, locomotive_catalog: Option<Vec<RuntimeLocomotiveCatalogEntry>>,
@ -318,6 +319,7 @@ pub fn project_save_slice_to_runtime_state_import(
.world_issue_opinion_base_terms_raw_i32, .world_issue_opinion_base_terms_raw_i32,
company_market_state: projection.company_market_state, company_market_state: projection.company_market_state,
chairman_issue_opinion_terms_raw_i32: projection.chairman_issue_opinion_terms_raw_i32, chairman_issue_opinion_terms_raw_i32: projection.chairman_issue_opinion_terms_raw_i32,
chairman_personality_raw_u8: projection.chairman_personality_raw_u8,
..RuntimeServiceState::default() ..RuntimeServiceState::default()
}, },
}; };
@ -448,6 +450,11 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
.chairman_issue_opinion_terms_raw_i32 .chairman_issue_opinion_terms_raw_i32
.clone() .clone()
}, },
chairman_personality_raw_u8: if projection.has_chairman_projection {
projection.chairman_personality_raw_u8
} else {
base_state.service_state.chairman_personality_raw_u8.clone()
},
..base_state.service_state.clone() ..base_state.service_state.clone()
}, },
}; };
@ -867,6 +874,10 @@ fn project_save_slice_components(
.world_finance_neighborhood_state .world_finance_neighborhood_state
.as_ref() .as_ref()
.map(|state| state.dividend_policy_raw_u8), .map(|state| state.dividend_policy_raw_u8),
building_density_growth_setting_raw_u32: save_slice
.world_finance_neighborhood_state
.as_ref()
.map(|state| state.building_density_growth_setting_raw_u32),
stock_issue_and_buyback_allowed: save_slice stock_issue_and_buyback_allowed: save_slice
.world_finance_neighborhood_state .world_finance_neighborhood_state
.as_ref() .as_ref()
@ -1227,6 +1238,7 @@ fn project_save_slice_components(
has_chairman_selection_override, has_chairman_selection_override,
selected_chairman_profile_id, selected_chairman_profile_id,
chairman_issue_opinion_terms_raw_i32, chairman_issue_opinion_terms_raw_i32,
chairman_personality_raw_u8,
) = if let Some(table) = &save_slice.chairman_profile_table { ) = if let Some(table) = &save_slice.chairman_profile_table {
metadata.insert( metadata.insert(
"save_slice.chairman_profile_table_source_kind".to_string(), "save_slice.chairman_profile_table_source_kind".to_string(),
@ -1253,6 +1265,7 @@ fn project_save_slice_components(
table.selected_chairman_profile_id.is_some(), table.selected_chairman_profile_id.is_some(),
table.selected_chairman_profile_id, table.selected_chairman_profile_id,
BTreeMap::new(), BTreeMap::new(),
BTreeMap::new(),
) )
} else { } else {
( (
@ -1279,10 +1292,26 @@ fn project_save_slice_components(
.iter() .iter()
.map(|entry| (entry.profile_id, entry.issue_opinion_terms_raw_i32.clone())) .map(|entry| (entry.profile_id, entry.issue_opinion_terms_raw_i32.clone()))
.collect::<BTreeMap<_, _>>(), .collect::<BTreeMap<_, _>>(),
table
.entries
.iter()
.filter_map(|entry| {
entry
.personality_byte_0x291
.map(|value| (entry.profile_id, value))
})
.collect::<BTreeMap<_, _>>(),
) )
} }
} else { } else {
(Vec::new(), false, false, None, BTreeMap::new()) (
Vec::new(),
false,
false,
None,
BTreeMap::new(),
BTreeMap::new(),
)
}; };
let named_locomotive_cost = BTreeMap::new(); let named_locomotive_cost = BTreeMap::new();
@ -1374,6 +1403,7 @@ fn project_save_slice_components(
has_chairman_selection_override, has_chairman_selection_override,
selected_chairman_profile_id, selected_chairman_profile_id,
chairman_issue_opinion_terms_raw_i32, chairman_issue_opinion_terms_raw_i32,
chairman_personality_raw_u8,
candidate_availability, candidate_availability,
named_locomotive_availability, named_locomotive_availability,
locomotive_catalog, locomotive_catalog,
@ -5326,6 +5356,7 @@ mod tests {
holdings_value_total: 700, holdings_value_total: 700,
net_worth_total: 1200, net_worth_total: 1200,
purchasing_power_total: 1500, purchasing_power_total: 1500,
personality_byte_0x291: Some(12),
issue_opinion_terms_raw_i32: Vec::new(), issue_opinion_terms_raw_i32: Vec::new(),
}, },
crate::SmpLoadedChairmanProfileEntry { crate::SmpLoadedChairmanProfileEntry {
@ -5338,6 +5369,7 @@ mod tests {
holdings_value_total: 600, holdings_value_total: 600,
net_worth_total: 900, net_worth_total: 900,
purchasing_power_total: 1100, purchasing_power_total: 1100,
personality_byte_0x291: Some(20),
issue_opinion_terms_raw_i32: Vec::new(), issue_opinion_terms_raw_i32: Vec::new(),
}, },
], ],
@ -6019,6 +6051,8 @@ mod tests {
bankruptcy_policy_raw_hex: "0x00".to_string(), bankruptcy_policy_raw_hex: "0x00".to_string(),
dividend_policy_raw_u8: 1, dividend_policy_raw_u8: 1,
dividend_policy_raw_hex: "0x01".to_string(), dividend_policy_raw_hex: "0x01".to_string(),
building_density_growth_setting_raw_u32: 1,
building_density_growth_setting_raw_hex: "0x00000001".to_string(),
labels: vec![ labels: vec![
"current_calendar_tuple_word".to_string(), "current_calendar_tuple_word".to_string(),
"current_calendar_tuple_word_2".to_string(), "current_calendar_tuple_word_2".to_string(),
@ -13956,6 +13990,7 @@ mod tests {
world_issue_opinion_base_terms_raw_i32: Vec::new(), world_issue_opinion_base_terms_raw_i32: Vec::new(),
company_market_state: BTreeMap::new(), company_market_state: BTreeMap::new(),
chairman_issue_opinion_terms_raw_i32: BTreeMap::new(), chairman_issue_opinion_terms_raw_i32: BTreeMap::new(),
chairman_personality_raw_u8: BTreeMap::new(),
}, },
}; };
let save_slice = SmpLoadedSaveSlice { let save_slice = SmpLoadedSaveSlice {

View file

@ -53,12 +53,13 @@ pub use runtime::{
RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric, RuntimeCargoPriceTarget, RuntimeCargoProductionTarget, RuntimeChairmanMetric,
RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany, RuntimeChairmanProfile, RuntimeChairmanTarget, RuntimeCompany,
RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState, RuntimeCompanyAnnualCreditorPressureState, RuntimeCompanyAnnualDeepDistressState,
RuntimeCompanyAnnualFinanceState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyAnnualFinanceState, RuntimeCompanyAnnualStockRepurchaseState,
RuntimeCompanyControllerKind, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyBondSlot, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind,
RuntimeCompanyMetric, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyMarketMetric, RuntimeCompanyMarketState, RuntimeCompanyMetric,
RuntimeCompanyTarget, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCompanyStatBandCandidate, RuntimeCompanyStatSelector, RuntimeCompanyTarget,
RuntimeCondition, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeCompanyTerritoryAccess, RuntimeCompanyTerritoryTrackPieceCount, RuntimeCondition,
RuntimeEventRecordTemplate, RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimeLocomotiveCatalogEntry, RuntimePackedEventCollectionSummary,
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary,
RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayer,
@ -67,17 +68,18 @@ pub use runtime::{
RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimeTrackPieceCounts, RuntimeTrain,
RuntimeWorldFinanceNeighborhoodCandidate, RuntimeWorldIssueState, RuntimeWorldRestoreState, 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_assigned_share_pool, runtime_company_annual_finance_state, runtime_company_annual_stock_repurchase_state,
runtime_company_average_live_bond_coupon, runtime_company_book_value_per_share, runtime_company_assigned_share_pool, runtime_company_average_live_bond_coupon,
runtime_company_credit_rating, runtime_company_investor_confidence, runtime_company_book_value_per_share, runtime_company_credit_rating,
runtime_company_management_attitude, runtime_company_market_value, runtime_company_prime_rate, runtime_company_investor_confidence, runtime_company_management_attitude,
runtime_company_market_value, runtime_company_prime_rate,
runtime_company_recent_per_share_subscore, runtime_company_stat_value, runtime_company_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,
runtime_world_bond_issue_and_repayment_allowed, runtime_world_dividend_adjustment_allowed, runtime_world_bond_issue_and_repayment_allowed, runtime_world_building_density_growth_setting,
runtime_world_issue_opinion_multiplier, runtime_world_issue_opinion_term_sum_raw, runtime_world_dividend_adjustment_allowed, runtime_world_issue_opinion_multiplier,
runtime_world_issue_state, runtime_world_prime_rate_baseline, runtime_world_issue_opinion_term_sum_raw, runtime_world_issue_state,
runtime_world_stock_issue_and_buyback_allowed, runtime_world_prime_rate_baseline, runtime_world_stock_issue_and_buyback_allowed,
}; };
pub use smp::{ pub use smp::{
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane, SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION, SmpAlignedRuntimeRuleBandLane,

View file

@ -227,6 +227,37 @@ pub struct RuntimeCompanyAnnualDeepDistressState {
pub eligible_for_bankruptcy_fallback: bool, pub eligible_for_bankruptcy_fallback: bool,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCompanyAnnualStockRepurchaseState {
pub company_id: u32,
#[serde(default)]
pub annual_mode_active: Option<bool>,
#[serde(default)]
pub stock_issue_and_buyback_allowed: Option<bool>,
pub city_connection_latch: bool,
#[serde(default)]
pub building_density_growth_setting: Option<u32>,
#[serde(default)]
pub linked_chairman_profile_id: Option<u32>,
#[serde(default)]
pub linked_chairman_personality_raw_u8: Option<u8>,
#[serde(default)]
pub repurchase_batch_size: Option<u32>,
#[serde(default)]
pub repurchase_factor_basis_points: Option<i64>,
#[serde(default)]
pub current_cash: Option<i64>,
#[serde(default)]
pub stock_value_gate_cash_floor: Option<i64>,
#[serde(default)]
pub support_adjusted_share_price_scalar: Option<i64>,
#[serde(default)]
pub affordability_cash_floor: Option<i64>,
#[serde(default)]
pub unassigned_share_pool: Option<u32>,
pub eligible_for_single_batch_repurchase: 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)]
@ -1044,6 +1075,8 @@ pub struct RuntimeServiceState {
pub company_market_state: BTreeMap<u32, RuntimeCompanyMarketState>, pub company_market_state: BTreeMap<u32, RuntimeCompanyMarketState>,
#[serde(default)] #[serde(default)]
pub chairman_issue_opinion_terms_raw_i32: BTreeMap<u32, Vec<i32>>, pub chairman_issue_opinion_terms_raw_i32: BTreeMap<u32, Vec<i32>>,
#[serde(default)]
pub chairman_personality_raw_u8: BTreeMap<u32, u8>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
@ -1159,6 +1192,8 @@ pub struct RuntimeWorldRestoreState {
#[serde(default)] #[serde(default)]
pub dividend_policy_raw_u8: Option<u8>, pub dividend_policy_raw_u8: Option<u8>,
#[serde(default)] #[serde(default)]
pub building_density_growth_setting_raw_u32: Option<u32>,
#[serde(default)]
pub stock_issue_and_buyback_allowed: Option<bool>, pub stock_issue_and_buyback_allowed: Option<bool>,
#[serde(default)] #[serde(default)]
pub bond_issue_and_repayment_allowed: Option<bool>, pub bond_issue_and_repayment_allowed: Option<bool>,
@ -1953,6 +1988,14 @@ impl RuntimeState {
)); ));
} }
} }
for chairman_profile_id in self.service_state.chairman_personality_raw_u8.keys() {
if !seen_chairman_profile_ids.contains(chairman_profile_id) {
return Err(format!(
"service_state.chairman_personality_raw_u8 references unknown chairman_profile_id {}",
chairman_profile_id
));
}
}
for (player_id, vars) in &self.player_runtime_variables { for (player_id, vars) in &self.player_runtime_variables {
if !seen_player_ids.contains(player_id) { if !seen_player_ids.contains(player_id) {
return Err(format!( return Err(format!(
@ -2804,6 +2847,95 @@ pub fn runtime_world_dividend_adjustment_allowed(state: &RuntimeState) -> Option
Some(state.world_restore.dividend_policy_raw_u8? == 0) Some(state.world_restore.dividend_policy_raw_u8? == 0)
} }
pub fn runtime_world_building_density_growth_setting(state: &RuntimeState) -> Option<u32> {
state.world_restore.building_density_growth_setting_raw_u32
}
fn runtime_chairman_stock_repurchase_factor_f64(
state: &RuntimeState,
chairman_profile_id: Option<u32>,
) -> Option<f64> {
let personality_byte = chairman_profile_id
.and_then(|profile_id| {
state
.service_state
.chairman_personality_raw_u8
.get(&profile_id)
})
.copied();
let mut factor = personality_byte
.map(|byte| (f64::from(byte) * 39.0 + 300.0) / 400.0)
.unwrap_or(1.0);
if runtime_world_building_density_growth_setting(state) == Some(1) {
factor *= 1.6;
}
Some(factor)
}
pub fn runtime_company_annual_stock_repurchase_state(
state: &RuntimeState,
company_id: u32,
) -> Option<RuntimeCompanyAnnualStockRepurchaseState> {
let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?;
let company = state
.companies
.iter()
.find(|company| company.company_id == 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 support_adjusted_share_price_scalar =
runtime_company_support_adjusted_share_price_scalar_f64(state, company_id);
let repurchase_factor =
runtime_chairman_stock_repurchase_factor_f64(state, company.linked_chairman_profile_id)?;
let repurchase_factor_basis_points = runtime_round_f64_to_i64(repurchase_factor * 100.0);
let stock_value_gate_cash_floor = runtime_round_f64_to_i64(repurchase_factor * 800_000.0);
let affordability_cash_floor = support_adjusted_share_price_scalar
.and_then(|value| runtime_round_f64_to_i64(value * repurchase_factor * 1_000.0 * 1.2));
let support_adjusted_share_price_scalar =
support_adjusted_share_price_scalar.and_then(runtime_round_f64_to_i64);
let unassigned_share_pool = runtime_company_unassigned_share_pool(state, company_id);
let eligible_for_single_batch_repurchase = runtime_world_annual_finance_mode_active(state)
== Some(true)
&& runtime_world_stock_issue_and_buyback_allowed(state) == Some(true)
&& annual_finance_state.city_connection_latch
&& current_cash
.zip(stock_value_gate_cash_floor)
.is_some_and(|(value, floor)| value >= floor)
&& current_cash
.zip(affordability_cash_floor)
.is_some_and(|(value, floor)| value >= floor)
&& unassigned_share_pool.is_some_and(|value| value >= 1_000);
Some(RuntimeCompanyAnnualStockRepurchaseState {
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),
city_connection_latch: annual_finance_state.city_connection_latch,
building_density_growth_setting: runtime_world_building_density_growth_setting(state),
linked_chairman_profile_id: company.linked_chairman_profile_id,
linked_chairman_personality_raw_u8: company
.linked_chairman_profile_id
.and_then(|profile_id| {
state
.service_state
.chairman_personality_raw_u8
.get(&profile_id)
})
.copied(),
repurchase_batch_size: Some(1_000),
repurchase_factor_basis_points,
current_cash,
stock_value_gate_cash_floor,
support_adjusted_share_price_scalar,
affordability_cash_floor,
unassigned_share_pool,
eligible_for_single_batch_repurchase,
})
}
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,
@ -3767,6 +3899,7 @@ mod tests {
bond_issue_and_repayment_policy_raw_u8: None, bond_issue_and_repayment_policy_raw_u8: None,
bankruptcy_policy_raw_u8: None, bankruptcy_policy_raw_u8: None,
dividend_policy_raw_u8: None, dividend_policy_raw_u8: None,
building_density_growth_setting_raw_u32: None,
stock_issue_and_buyback_allowed: None, stock_issue_and_buyback_allowed: None,
bond_issue_and_repayment_allowed: None, bond_issue_and_repayment_allowed: None,
bankruptcy_allowed: None, bankruptcy_allowed: None,
@ -7113,6 +7246,132 @@ mod tests {
assert!(pressure_state.eligible_for_bankruptcy_fallback); assert!(pressure_state.eligible_for_bankruptcy_fallback);
} }
#[test]
fn derives_annual_stock_repurchase_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, 1_600_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),
building_density_growth_setting_raw_u32: Some(1),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 12,
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(3),
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: "Jay".to_string(),
active: true,
current_cash: 200,
linked_company_id: Some(12),
company_holdings: BTreeMap::from([(12, 14_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([(
12,
RuntimeCompanyMarketState {
outstanding_shares: 20_000,
cached_share_price_raw_u32: 20.0f32.to_bits(),
founding_year: 1835,
city_connection_latch: true,
year_stat_family_qword_bits,
..RuntimeCompanyMarketState::default()
},
)]),
chairman_personality_raw_u8: BTreeMap::from([(3, 20)]),
..RuntimeServiceState::default()
},
};
let repurchase_state = runtime_company_annual_stock_repurchase_state(&state, 12)
.expect("stock repurchase state");
assert_eq!(repurchase_state.building_density_growth_setting, Some(1));
assert_eq!(
repurchase_state.linked_chairman_personality_raw_u8,
Some(20)
);
assert_eq!(repurchase_state.repurchase_batch_size, Some(1_000));
assert_eq!(repurchase_state.repurchase_factor_basis_points, Some(432));
assert_eq!(repurchase_state.current_cash, Some(1_600_000));
assert_eq!(
repurchase_state.stock_value_gate_cash_floor,
Some(3_456_000)
);
assert_eq!(
repurchase_state.support_adjusted_share_price_scalar,
Some(20)
);
assert_eq!(repurchase_state.affordability_cash_floor, Some(103_680));
assert_eq!(repurchase_state.unassigned_share_pool, Some(5_500));
assert!(!repurchase_state.eligible_for_single_batch_repurchase);
}
#[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

@ -137,6 +137,7 @@ const RT3_SAVE_WORLD_BLOCK_STOCK_POLICY_RELATIVE_OFFSET: usize = 0x4a83;
const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87; const RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET: usize = 0x4a87;
const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b; const RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET: usize = 0x4a8b;
const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f; const RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET: usize = 0x4a8f;
const RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET: usize = 0x4c78;
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda; const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_MIRROR_RELATIVE_OFFSET: usize = 0x0bda;
const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] = const RT3_SAVE_WORLD_BLOCK_ECONOMIC_TUNING_PRIMARY_RELATIVE_OFFSETS: [usize; 6] =
[0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2]; [0x0bde, 0x0be2, 0x0be6, 0x0bea, 0x0bee, 0x0bf2];
@ -1584,6 +1585,7 @@ pub struct SmpSaveWorldFinanceNeighborhoodProbe {
pub bankruptcy_policy_raw_hex: String, pub bankruptcy_policy_raw_hex: String,
pub dividend_policy_raw_u8: u8, pub dividend_policy_raw_u8: u8,
pub dividend_policy_raw_hex: String, pub dividend_policy_raw_hex: String,
pub building_density_growth_setting_lane: SmpSaveDwordCandidate,
pub dword_candidates: Vec<SmpSaveDwordCandidate>, pub dword_candidates: Vec<SmpSaveDwordCandidate>,
pub evidence: Vec<String>, pub evidence: Vec<String>,
} }
@ -2276,6 +2278,8 @@ pub struct SmpLoadedWorldFinanceNeighborhoodState {
pub bankruptcy_policy_raw_hex: String, pub bankruptcy_policy_raw_hex: String,
pub dividend_policy_raw_u8: u8, pub dividend_policy_raw_u8: u8,
pub dividend_policy_raw_hex: String, pub dividend_policy_raw_hex: String,
pub building_density_growth_setting_raw_u32: u32,
pub building_density_growth_setting_raw_hex: String,
pub labels: Vec<String>, pub labels: Vec<String>,
pub relative_offsets: Vec<usize>, pub relative_offsets: Vec<usize>,
pub relative_offset_hex: Vec<String>, pub relative_offset_hex: Vec<String>,
@ -2345,6 +2349,8 @@ pub struct SmpLoadedChairmanProfileEntry {
#[serde(default)] #[serde(default)]
pub purchasing_power_total: i64, pub purchasing_power_total: i64,
#[serde(default)] #[serde(default)]
pub personality_byte_0x291: Option<u8>,
#[serde(default)]
pub issue_opinion_terms_raw_i32: Vec<i32>, pub issue_opinion_terms_raw_i32: Vec<i32>,
} }
@ -2457,6 +2463,8 @@ pub struct SmpSaveChairmanRecordAnalysisEntry {
pub derived_net_worth_share_price_total: Option<i64>, pub derived_net_worth_share_price_total: Option<i64>,
#[serde(default)] #[serde(default)]
pub derived_cached_purchasing_power_total: Option<i64>, pub derived_cached_purchasing_power_total: Option<i64>,
pub personality_byte_0x291: u8,
pub personality_byte_0x291_hex: String,
#[serde(default)] #[serde(default)]
pub cached_scalar_candidates: Vec<SmpSaveScalarCandidate>, pub cached_scalar_candidates: Vec<SmpSaveScalarCandidate>,
} }
@ -3255,6 +3263,10 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
&bytes, &bytes,
record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
)?; )?;
let personality_byte_0x291 = read_u8_at(
&bytes,
record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
)?;
let mut holdings_by_company = BTreeMap::new(); let mut holdings_by_company = BTreeMap::new();
for company_id in 1..=company_id_bound { for company_id in 1..=company_id_bound {
let slot_offset = record_offset let slot_offset = record_offset
@ -3293,6 +3305,8 @@ pub fn inspect_save_company_and_chairman_analysis_bytes(
derived_holdings_share_price_total, derived_holdings_share_price_total,
derived_net_worth_share_price_total, derived_net_worth_share_price_total,
derived_cached_purchasing_power_total, derived_cached_purchasing_power_total,
personality_byte_0x291,
personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"),
cached_scalar_candidates, cached_scalar_candidates,
}); });
} }
@ -3528,6 +3542,11 @@ fn derive_loaded_world_finance_neighborhood_state_from_probe(
bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(), bankruptcy_policy_raw_hex: probe.bankruptcy_policy_raw_hex.clone(),
dividend_policy_raw_u8: probe.dividend_policy_raw_u8, dividend_policy_raw_u8: probe.dividend_policy_raw_u8,
dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(), dividend_policy_raw_hex: probe.dividend_policy_raw_hex.clone(),
building_density_growth_setting_raw_u32: probe.building_density_growth_setting_lane.raw_u32,
building_density_growth_setting_raw_hex: probe
.building_density_growth_setting_lane
.raw_u32_hex
.clone(),
labels: probe labels: probe
.dword_candidates .dword_candidates
.iter() .iter()
@ -3688,6 +3707,7 @@ const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d;
const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd; const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd;
const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9; const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9;
const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1; const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1;
const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291;
const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b; const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b;
const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize = const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize =
RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT; RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT;
@ -4364,6 +4384,10 @@ fn parse_save_chairman_profile_table_probe(
bytes, bytes,
record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET, record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
)?; )?;
let personality_byte_0x291 = read_u8_at(
bytes,
record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
)?;
let cache_0 = round_f64_to_i64(read_f64_at( let cache_0 = round_f64_to_i64(read_f64_at(
bytes, bytes,
record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET, record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET,
@ -4409,6 +4433,7 @@ fn parse_save_chairman_profile_table_probe(
holdings_value_total, holdings_value_total,
net_worth_total, net_worth_total,
purchasing_power_total, purchasing_power_total,
personality_byte_0x291: Some(personality_byte_0x291),
issue_opinion_terms_raw_i32, issue_opinion_terms_raw_i32,
}); });
} }
@ -9144,6 +9169,12 @@ fn parse_save_world_finance_neighborhood_probe(
bytes, bytes,
payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET, payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET,
)?; )?;
let building_density_growth_setting_lane = build_save_dword_candidate(
bytes,
payload_offset,
"building_density_growth_setting",
RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET,
)?;
let dword_candidates = let dword_candidates =
build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?; build_save_world_finance_neighborhood_candidates(bytes, payload_offset)?;
@ -9171,6 +9202,7 @@ fn parse_save_world_finance_neighborhood_probe(
bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"), bankruptcy_policy_raw_hex: format!("0x{bankruptcy_policy_raw_u8:02x}"),
dividend_policy_raw_u8, dividend_policy_raw_u8,
dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"), dividend_policy_raw_hex: format!("0x{dividend_policy_raw_u8:02x}"),
building_density_growth_setting_lane,
dword_candidates, dword_candidates,
evidence: vec![ evidence: vec![
format!( format!(
@ -9192,6 +9224,10 @@ fn parse_save_world_finance_neighborhood_probe(
RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET, RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET,
RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET
), ),
format!(
"payload +0x{:x} carries the fixed-world building-density growth setting mirrored from `[world+0x4c7c]`, which the annual repurchase and dividend policy helpers both read directly",
RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET
),
"finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(), "finance-neighborhood candidates cover the fixed dword strip around the grounded world calendar tuple, absolute-counter, selection-context, and issue-0x37 lanes so broader finance reader closure can build on one rehosted owner surface.".to_string(),
], ],
}); });
@ -15931,6 +15967,9 @@ mod tests {
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2; bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BOND_POLICY_RELATIVE_OFFSET] = 2;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3; bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BANKRUPTCY_POLICY_RELATIVE_OFFSET] = 3;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4; bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_DIVIDEND_POLICY_RELATIVE_OFFSET] = 4;
bytes[payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET
..payload_offset + RT3_SAVE_WORLD_BLOCK_BUILDING_DENSITY_GROWTH_RELATIVE_OFFSET + 4]
.copy_from_slice(&2u32.to_le_bytes());
let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN; let next_chunk_offset = payload_offset + RT3_SAVE_WORLD_BLOCK_LEN;
bytes[next_chunk_offset..next_chunk_offset + 4] bytes[next_chunk_offset..next_chunk_offset + 4]
.copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes()); .copy_from_slice(&RT3_SAVE_WORLD_BLOCK_NEXT_CHUNK_TAG.to_le_bytes());
@ -15968,6 +16007,13 @@ mod tests {
assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03"); assert_eq!(probe.bankruptcy_policy_raw_hex, "0x03");
assert_eq!(probe.dividend_policy_raw_u8, 4); assert_eq!(probe.dividend_policy_raw_u8, 4);
assert_eq!(probe.dividend_policy_raw_hex, "0x04"); assert_eq!(probe.dividend_policy_raw_hex, "0x04");
assert_eq!(probe.building_density_growth_setting_lane.raw_u32, 2);
assert_eq!(
probe
.building_density_growth_setting_lane
.relative_offset_hex,
"0x4c78"
);
assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1); assert_eq!(probe.current_calendar_tuple_word_lane.value_i32, 1);
assert_eq!( assert_eq!(
probe.current_calendar_tuple_word_2_lane.relative_offset_hex, probe.current_calendar_tuple_word_2_lane.relative_offset_hex,
@ -16773,6 +16819,8 @@ mod tests {
bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET bytes[record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET
..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4] ..record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET + 4]
.copy_from_slice(&linked.to_le_bytes()); .copy_from_slice(&linked.to_le_bytes());
bytes[record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET] =
(index as u8) + 10;
bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET bytes[record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET
..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8] ..record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET + 8]
.copy_from_slice(&cache0.to_le_bytes()); .copy_from_slice(&cache0.to_le_bytes());
@ -16857,10 +16905,12 @@ mod tests {
assert_eq!(table.entries[0].current_cash, -107644); assert_eq!(table.entries[0].current_cash, -107644);
assert_eq!(table.entries[0].holdings_value_total, 252508); assert_eq!(table.entries[0].holdings_value_total, 252508);
assert_eq!(table.entries[0].purchasing_power_total, 144864); assert_eq!(table.entries[0].purchasing_power_total, 144864);
assert_eq!(table.entries[0].personality_byte_0x291, Some(10));
assert_eq!(table.entries[1].profile_id, 2); assert_eq!(table.entries[1].profile_id, 2);
assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000)); assert_eq!(table.entries[1].company_holdings.get(&2), Some(&9000));
assert_eq!(table.entries[1].holdings_value_total, 822000); assert_eq!(table.entries[1].holdings_value_total, 822000);
assert_eq!(table.entries[1].purchasing_power_total, 1_009_282); assert_eq!(table.entries[1].purchasing_power_total, 1_009_282);
assert_eq!(table.entries[1].personality_byte_0x291, Some(11));
} }
#[test] #[test]

View file

@ -3,7 +3,7 @@ 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_unassigned_share_pool, 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 {
@ -55,6 +55,7 @@ pub struct RuntimeSummary {
pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option<u8>, pub world_restore_bond_issue_and_repayment_policy_raw_u8: Option<u8>,
pub world_restore_bankruptcy_policy_raw_u8: Option<u8>, pub world_restore_bankruptcy_policy_raw_u8: Option<u8>,
pub world_restore_dividend_policy_raw_u8: Option<u8>, pub world_restore_dividend_policy_raw_u8: Option<u8>,
pub world_restore_building_density_growth_setting_raw_u32: Option<u32>,
pub world_restore_stock_issue_and_buyback_allowed: Option<bool>, pub world_restore_stock_issue_and_buyback_allowed: Option<bool>,
pub world_restore_bond_issue_and_repayment_allowed: Option<bool>, pub world_restore_bond_issue_and_repayment_allowed: Option<bool>,
pub world_restore_bankruptcy_allowed: Option<bool>, pub world_restore_bankruptcy_allowed: Option<bool>,
@ -111,6 +112,18 @@ pub struct RuntimeSummary {
pub selected_company_deep_distress_cash_floor: Option<i64>, pub selected_company_deep_distress_cash_floor: Option<i64>,
pub selected_company_deep_distress_net_profit_floor: Option<i64>, pub selected_company_deep_distress_net_profit_floor: Option<i64>,
pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option<bool>, pub selected_company_deep_distress_eligible_for_bankruptcy_fallback: Option<bool>,
pub selected_company_stock_repurchase_city_connection_latch: Option<bool>,
pub selected_company_stock_repurchase_building_density_growth_setting: Option<u32>,
pub selected_company_stock_repurchase_linked_chairman_profile_id: Option<u32>,
pub selected_company_stock_repurchase_linked_chairman_personality_raw_u8: Option<u8>,
pub selected_company_stock_repurchase_batch_size: Option<u32>,
pub selected_company_stock_repurchase_factor_basis_points: Option<i64>,
pub selected_company_stock_repurchase_current_cash: Option<i64>,
pub selected_company_stock_repurchase_stock_value_gate_cash_floor: Option<i64>,
pub selected_company_stock_repurchase_support_adjusted_share_price_scalar: 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_eligible_for_single_batch: 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,
@ -207,6 +220,10 @@ impl RuntimeSummary {
let selected_company_deep_distress_state = state let selected_company_deep_distress_state = state
.selected_company_id .selected_company_id
.and_then(|company_id| runtime_company_annual_deep_distress_state(state, company_id)); .and_then(|company_id| runtime_company_annual_deep_distress_state(state, company_id));
let selected_company_stock_repurchase_state =
state.selected_company_id.and_then(|company_id| {
runtime_company_annual_stock_repurchase_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(),
@ -294,6 +311,9 @@ impl RuntimeSummary {
.bond_issue_and_repayment_policy_raw_u8, .bond_issue_and_repayment_policy_raw_u8,
world_restore_bankruptcy_policy_raw_u8: state.world_restore.bankruptcy_policy_raw_u8, world_restore_bankruptcy_policy_raw_u8: state.world_restore.bankruptcy_policy_raw_u8,
world_restore_dividend_policy_raw_u8: state.world_restore.dividend_policy_raw_u8, world_restore_dividend_policy_raw_u8: state.world_restore.dividend_policy_raw_u8,
world_restore_building_density_growth_setting_raw_u32: state
.world_restore
.building_density_growth_setting_raw_u32,
world_restore_stock_issue_and_buyback_allowed: state world_restore_stock_issue_and_buyback_allowed: state
.world_restore .world_restore
.stock_issue_and_buyback_allowed, .stock_issue_and_buyback_allowed,
@ -480,6 +500,56 @@ impl RuntimeSummary {
selected_company_deep_distress_state selected_company_deep_distress_state
.as_ref() .as_ref()
.map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback), .map(|pressure_state| pressure_state.eligible_for_bankruptcy_fallback),
selected_company_stock_repurchase_city_connection_latch:
selected_company_stock_repurchase_state
.as_ref()
.map(|repurchase_state| repurchase_state.city_connection_latch),
selected_company_stock_repurchase_building_density_growth_setting:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.building_density_growth_setting),
selected_company_stock_repurchase_linked_chairman_profile_id:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.linked_chairman_profile_id),
selected_company_stock_repurchase_linked_chairman_personality_raw_u8:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| {
repurchase_state.linked_chairman_personality_raw_u8
}),
selected_company_stock_repurchase_batch_size: selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.repurchase_batch_size),
selected_company_stock_repurchase_factor_basis_points:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.repurchase_factor_basis_points),
selected_company_stock_repurchase_current_cash: selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.current_cash),
selected_company_stock_repurchase_stock_value_gate_cash_floor:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.stock_value_gate_cash_floor),
selected_company_stock_repurchase_support_adjusted_share_price_scalar:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| {
repurchase_state.support_adjusted_share_price_scalar
}),
selected_company_stock_repurchase_affordability_cash_floor:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.affordability_cash_floor),
selected_company_stock_repurchase_unassigned_share_pool:
selected_company_stock_repurchase_state
.as_ref()
.and_then(|repurchase_state| repurchase_state.unassigned_share_pool),
selected_company_stock_repurchase_eligible_for_single_batch:
selected_company_stock_repurchase_state
.as_ref()
.map(|repurchase_state| repurchase_state.eligible_for_single_batch_repurchase),
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
@ -2590,4 +2660,155 @@ mod tests {
Some(true) Some(true)
); );
} }
#[test]
fn summarizes_selected_company_stock_repurchase_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();
};
write_current_value(&mut year_stat_family_qword_bits, 0x0d, 1_600_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),
building_density_growth_setting_raw_u32: Some(1),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 12,
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(3),
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(12),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![crate::RuntimeChairmanProfile {
profile_id: 3,
name: "Jay".to_string(),
active: true,
current_cash: 200,
linked_company_id: Some(12),
company_holdings: BTreeMap::from([(12, 14_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: 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 {
company_market_state: BTreeMap::from([(
12,
crate::RuntimeCompanyMarketState {
outstanding_shares: 20_000,
cached_share_price_raw_u32: 20.0f32.to_bits(),
founding_year: 1835,
city_connection_latch: true,
year_stat_family_qword_bits,
..crate::RuntimeCompanyMarketState::default()
},
)]),
chairman_personality_raw_u8: BTreeMap::from([(3, 20)]),
..RuntimeServiceState::default()
},
};
let summary = RuntimeSummary::from_state(&state);
assert_eq!(
summary.world_restore_building_density_growth_setting_raw_u32,
Some(1)
);
assert_eq!(
summary.selected_company_stock_repurchase_building_density_growth_setting,
Some(1)
);
assert_eq!(
summary.selected_company_stock_repurchase_linked_chairman_personality_raw_u8,
Some(20)
);
assert_eq!(
summary.selected_company_stock_repurchase_batch_size,
Some(1_000)
);
assert_eq!(
summary.selected_company_stock_repurchase_factor_basis_points,
Some(432)
);
assert_eq!(
summary.selected_company_stock_repurchase_current_cash,
Some(1_600_000)
);
assert_eq!(
summary.selected_company_stock_repurchase_stock_value_gate_cash_floor,
Some(3_456_000)
);
assert_eq!(
summary.selected_company_stock_repurchase_support_adjusted_share_price_scalar,
Some(20)
);
assert_eq!(
summary.selected_company_stock_repurchase_affordability_cash_floor,
Some(103_680)
);
assert_eq!(
summary.selected_company_stock_repurchase_unassigned_share_pool,
Some(5_500)
);
assert_eq!(
summary.selected_company_stock_repurchase_eligible_for_single_batch,
Some(false)
);
}
} }

View file

@ -138,7 +138,9 @@ The highest-value next passes are now:
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-
net-profit surface too net-profit surface too; the same owner seam now also carries the fixed-world building-density
growth setting plus the linked chairman personality byte, which is enough to run the annual
stock-repurchase gate headlessly as another pure reader
- 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

@ -231,6 +231,9 @@ bankruptcy branch now runs as a pure runtime reader over owned annual-finance st
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
deep-distress bankruptcy fallback now rides the same owner-state seam too, using the save-native deep-distress bankruptcy fallback now rides the same owner-state seam too, using the save-native
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
chairman personality byte, which is enough to rehost the annual stock-repurchase gate on owned
save/runtime state instead of another threshold-only note.
## Why This Boundary ## Why This Boundary