Refresh chairman wealth from company market state

This commit is contained in:
Jan Petykiewicz 2026-04-17 18:51:42 -07:00
commit 26a7a34ad0
3 changed files with 343 additions and 12 deletions

View file

@ -315,6 +315,8 @@ pub fn project_save_slice_to_runtime_state_import(
..RuntimeServiceState::default()
},
};
let mut state = state;
state.refresh_derived_market_state();
state.validate()?;
Ok(RuntimeStateImport {
@ -424,6 +426,8 @@ pub fn project_save_slice_overlay_to_runtime_state_import(
..base_state.service_state.clone()
},
};
let mut state = state;
state.refresh_derived_market_state();
state.validate()?;
Ok(RuntimeStateImport {

View file

@ -1716,6 +1716,61 @@ impl RuntimeState {
Ok(())
}
pub fn refresh_derived_market_state(&mut self) {
let company_share_prices = self
.service_state
.company_market_state
.iter()
.filter_map(|(company_id, market_state)| {
rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32)
.map(|share_price| (*company_id, share_price))
})
.collect::<BTreeMap<_, _>>();
for profile in &mut self.chairman_profiles {
let preserved_threshold_adjusted_holdings_component = profile
.purchasing_power_total
.saturating_sub(profile.current_cash)
.max(0);
if let Some(holdings_value_total) = derive_runtime_chairman_holdings_share_price_total(
&profile.company_holdings,
&company_share_prices,
) {
profile.holdings_value_total = holdings_value_total;
}
profile.net_worth_total = profile
.current_cash
.saturating_add(profile.holdings_value_total);
profile.purchasing_power_total = profile
.current_cash
.saturating_add(preserved_threshold_adjusted_holdings_component)
.max(profile.net_worth_total);
}
}
}
fn rounded_cached_share_price_i64(raw_u32: u32) -> Option<i64> {
let value = f32::from_bits(raw_u32);
if !value.is_finite() {
return None;
}
if value < i64::MIN as f32 || value > i64::MAX as f32 {
return None;
}
Some(value.round() as i64)
}
fn derive_runtime_chairman_holdings_share_price_total(
holdings_by_company: &BTreeMap<u32, u32>,
company_share_prices: &BTreeMap<u32, i64>,
) -> Option<i64> {
let mut total = 0i64;
for (company_id, units) in holdings_by_company {
let share_price = *company_share_prices.get(company_id)?;
total = total.checked_add((*units as i64).checked_mul(share_price)?)?;
}
Some(total)
}
fn validate_runtime_effect(
@ -3372,4 +3427,209 @@ mod tests {
assert!(state.validate().is_err());
}
#[test]
fn refreshes_chairman_totals_from_company_market_state() {
let mut state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![
RuntimeCompany {
company_id: 1,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
controller_kind: RuntimeCompanyControllerKind::Human,
},
RuntimeCompany {
company_id: 2,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
controller_kind: RuntimeCompanyControllerKind::Human,
},
],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 100,
linked_company_id: Some(1),
company_holdings: BTreeMap::from([(1, 2), (2, 3)]),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 400,
}],
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([
(
1,
RuntimeCompanyMarketState {
cached_share_price_raw_u32: 0x41200000,
..RuntimeCompanyMarketState::default()
},
),
(
2,
RuntimeCompanyMarketState {
cached_share_price_raw_u32: 0x41a00000,
..RuntimeCompanyMarketState::default()
},
),
]),
..RuntimeServiceState::default()
},
};
state.refresh_derived_market_state();
assert_eq!(state.chairman_profiles[0].holdings_value_total, 80);
assert_eq!(state.chairman_profiles[0].net_worth_total, 180);
assert_eq!(state.chairman_profiles[0].purchasing_power_total, 400);
}
#[test]
fn refreshes_chairman_purchasing_power_when_cash_changes() {
let mut state = RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
controller_kind: RuntimeCompanyControllerKind::Human,
}],
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 50,
linked_company_id: Some(1),
company_holdings: BTreeMap::from([(1, 2)]),
holdings_value_total: 20,
net_worth_total: 70,
purchasing_power_total: 130,
}],
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([(
1,
RuntimeCompanyMarketState {
cached_share_price_raw_u32: 0x41200000,
..RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
};
state.chairman_profiles[0].current_cash = 80;
state.refresh_derived_market_state();
assert_eq!(state.chairman_profiles[0].holdings_value_total, 20);
assert_eq!(state.chairman_profiles[0].net_worth_total, 100);
assert_eq!(state.chairman_profiles[0].purchasing_power_total, 130);
}
}

View file

@ -115,6 +115,7 @@ pub fn execute_step_command(
0
}
};
state.refresh_derived_market_state();
let final_summary = RuntimeSummary::from_state(state);
Ok(StepResult {
@ -363,7 +364,14 @@ fn apply_runtime_effects(
"missing chairman profile_id {profile_id} while applying cash effect"
)
})?;
let preserved_threshold_adjusted_holdings_component = chairman
.purchasing_power_total
.saturating_sub(chairman.current_cash)
.max(0);
chairman.current_cash = *value;
chairman.purchasing_power_total = chairman
.current_cash
.saturating_add(preserved_threshold_adjusted_holdings_component);
}
}
RuntimeEffect::SetCompanyGovernanceScalar {
@ -3790,28 +3798,64 @@ mod tests {
#[test]
fn set_chairman_cash_supports_all_active_target() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
linked_chairman_profile_id: Some(1),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
RuntimeCompany {
company_id: 2,
current_cash: 0,
debt: 0,
credit_rating_score: None,
prime_rate: None,
active: true,
available_track_laying_capacity: None,
controller_kind: RuntimeCompanyControllerKind::Human,
linked_chairman_profile_id: Some(2),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
},
],
chairman_profiles: vec![
RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 10,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
linked_company_id: Some(1),
company_holdings: BTreeMap::from([(1, 2)]),
holdings_value_total: 20,
net_worth_total: 30,
purchasing_power_total: 70,
},
RuntimeChairmanProfile {
profile_id: 2,
name: "Chairman Two".to_string(),
active: true,
current_cash: 20,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
linked_company_id: Some(2),
company_holdings: BTreeMap::from([(2, 3)]),
holdings_value_total: 60,
net_worth_total: 80,
purchasing_power_total: 110,
},
RuntimeChairmanProfile {
profile_id: 3,
@ -3821,8 +3865,8 @@ mod tests {
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
net_worth_total: 30,
purchasing_power_total: 30,
},
],
event_runtime_records: vec![RuntimeEventRecord {
@ -3839,6 +3883,25 @@ mod tests {
value: 77,
}],
}],
service_state: RuntimeServiceState {
company_market_state: BTreeMap::from([
(
1,
crate::RuntimeCompanyMarketState {
cached_share_price_raw_u32: 0x41200000,
..crate::RuntimeCompanyMarketState::default()
},
),
(
2,
crate::RuntimeCompanyMarketState {
cached_share_price_raw_u32: 0x41a00000,
..crate::RuntimeCompanyMarketState::default()
},
),
]),
..RuntimeServiceState::default()
},
..state()
};
@ -3851,6 +3914,10 @@ mod tests {
assert_eq!(state.chairman_profiles[0].current_cash, 77);
assert_eq!(state.chairman_profiles[1].current_cash, 77);
assert_eq!(state.chairman_profiles[2].current_cash, 30);
assert_eq!(state.chairman_profiles[0].net_worth_total, 97);
assert_eq!(state.chairman_profiles[0].purchasing_power_total, 137);
assert_eq!(state.chairman_profiles[1].net_worth_total, 137);
assert_eq!(state.chairman_profiles[1].purchasing_power_total, 167);
}
#[test]