Write company governance effects through owner state

This commit is contained in:
Jan Petykiewicz 2026-04-18 05:59:27 -07:00
commit 41418b4044
3 changed files with 383 additions and 17 deletions

View file

@ -6813,7 +6813,9 @@ mod tests {
)
.expect("overlay import should project");
assert_eq!(import.state.companies, base_state.companies);
let mut expected_companies = base_state.companies.clone();
expected_companies[1].investor_confidence = 38;
assert_eq!(import.state.companies, expected_companies);
assert_eq!(import.state.selected_company_id, Some(1));
assert_eq!(import.state.chairman_profiles, base_state.chairman_profiles);
assert_eq!(import.state.selected_chairman_profile_id, Some(1));

View file

@ -2280,14 +2280,33 @@ impl RuntimeState {
let book_value_per_share =
runtime_company_direct_float_field_value_f64(self, company.company_id, 0x32f)
.and_then(runtime_round_f64_to_i64);
(company.company_id, current_cash, book_value_per_share)
let prime_rate = runtime_company_prime_rate(self, company.company_id);
let investor_confidence =
runtime_company_investor_confidence(self, company.company_id);
let management_attitude =
runtime_company_management_attitude(self, company.company_id);
(
company.company_id,
current_cash,
book_value_per_share,
prime_rate,
investor_confidence,
management_attitude,
)
})
.collect::<Vec<_>>();
for company in &mut self.companies {
if let Some((_, current_cash, book_value_per_share)) = company_refresh
if let Some((
_,
current_cash,
book_value_per_share,
prime_rate,
investor_confidence,
management_attitude,
)) = company_refresh
.iter()
.find(|(company_id, _, _)| *company_id == company.company_id)
.find(|(company_id, _, _, _, _, _)| *company_id == company.company_id)
{
if let Some(current_cash) = current_cash {
company.current_cash = *current_cash;
@ -2295,6 +2314,15 @@ impl RuntimeState {
if let Some(book_value_per_share) = book_value_per_share {
company.book_value_per_share = *book_value_per_share;
}
if let Some(prime_rate) = prime_rate {
company.prime_rate = Some(*prime_rate);
}
if let Some(investor_confidence) = investor_confidence {
company.investor_confidence = *investor_confidence;
}
if let Some(management_attitude) = management_attitude {
company.management_attitude = *management_attitude;
}
}
}
@ -6243,7 +6271,10 @@ mod tests {
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
world_restore: RuntimeWorldRestoreState {
issue_37_value: Some(5.0f32.to_bits()),
..RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
companies: vec![RuntimeCompany {
company_id: 1,
@ -6292,9 +6323,20 @@ mod tests {
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState {
world_issue_opinion_base_terms_raw_i32: {
let mut terms = vec![0; 0x3b];
terms[RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 100;
terms
},
company_market_state: BTreeMap::from([(
1,
RuntimeCompanyMarketState {
cached_share_price_raw_u32: 37.0f32.to_bits(),
issue_opinion_terms_raw_i32: {
let mut terms = vec![0; 0x3b];
terms[RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize] = 58;
terms
},
direct_control_transfer_float_fields_raw_u32: BTreeMap::from([(
0x32f,
2620.0f32.to_bits(),
@ -6311,6 +6353,9 @@ mod tests {
assert_eq!(state.companies[0].current_cash, 275);
assert_eq!(state.companies[0].book_value_per_share, 2620);
assert_eq!(state.companies[0].prime_rate, Some(6));
assert_eq!(state.companies[0].investor_confidence, 37);
assert_eq!(state.companies[0].management_attitude, 58);
}
#[test]

View file

@ -318,6 +318,107 @@ fn service_post_company_stat_delta(
}
}
fn service_set_company_direct_float_field(
state: &mut RuntimeState,
company_id: u32,
field_offset: u32,
value: f64,
) -> bool {
if !value.is_finite() {
return false;
}
let Some(market_state) = state
.service_state
.company_market_state
.get_mut(&company_id)
else {
return false;
};
market_state
.direct_control_transfer_float_fields_raw_u32
.insert(field_offset, (value as f32).to_bits());
true
}
fn service_set_company_cached_share_price(
state: &mut RuntimeState,
company_id: u32,
value: f64,
) -> bool {
if !value.is_finite() {
return false;
}
let Some(market_state) = state
.service_state
.company_market_state
.get_mut(&company_id)
else {
return false;
};
market_state.cached_share_price_raw_u32 = (value as f32).to_bits();
true
}
fn service_set_company_issue_opinion_total(
state: &mut RuntimeState,
company_id: u32,
issue_id: u32,
target_total: i64,
) -> bool {
let current_total = crate::runtime::runtime_world_issue_opinion_term_sum_raw(
state,
issue_id,
state
.companies
.iter()
.find(|company| company.company_id == company_id)
.and_then(|company| company.linked_chairman_profile_id),
Some(company_id),
None,
)
.unwrap_or(0);
let Some(market_state) = state
.service_state
.company_market_state
.get_mut(&company_id)
else {
return false;
};
let issue_index = issue_id as usize;
if market_state.issue_opinion_terms_raw_i32.len() <= issue_index {
market_state
.issue_opinion_terms_raw_i32
.resize(issue_index + 1, 0);
}
let prior_company_term = i64::from(market_state.issue_opinion_terms_raw_i32[issue_index]);
let next_company_term = prior_company_term.saturating_add(target_total - current_total);
let Ok(next_company_term_i32) = i32::try_from(next_company_term) else {
return false;
};
market_state.issue_opinion_terms_raw_i32[issue_index] = next_company_term_i32;
true
}
fn service_set_company_prime_rate_target(
state: &mut RuntimeState,
company_id: u32,
value: i64,
) -> bool {
let Some(baseline) = crate::runtime::runtime_world_prime_rate_baseline(state) else {
return false;
};
let target_raw_sum = ((value as f64 - baseline) * 100.0).round();
if !target_raw_sum.is_finite() {
return false;
}
service_set_company_issue_opinion_total(
state,
company_id,
crate::RUNTIME_WORLD_ISSUE_PRIME_RATE,
target_raw_sum as i64,
)
}
fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -> bool {
let Some(bankruptcy_year) = state
.world_restore
@ -385,6 +486,25 @@ fn service_apply_company_bankruptcy(state: &mut RuntimeState, company_id: u32) -
company_mutated = true;
}
if let Some(current_cash) = crate::runtime::runtime_company_stat_value(
state,
company_id,
crate::RuntimeCompanyStatSelector {
family_id: crate::RUNTIME_COMPANY_STAT_FAMILY_CONTROL_TRANSFER,
slot_id: crate::RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
},
) {
if current_cash != 0 {
company_mutated |= service_post_company_stat_delta(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
-(current_cash as f64),
false,
);
}
}
company_mutated
}
@ -1200,7 +1320,7 @@ fn apply_runtime_effects(
state,
company_id,
RUNTIME_COMPANY_STAT_SLOT_CURRENT_CASH,
value.saturating_sub(prior_cash),
value.saturating_sub(prior_cash) as f64,
false,
) {
let company = state
@ -1260,6 +1380,43 @@ fn apply_runtime_effects(
} => {
let company_ids = resolve_company_target_ids(state, target, condition_context)?;
for company_id in company_ids {
let mut applied_through_owner_state = false;
match metric {
RuntimeCompanyMetric::CreditRating => {}
RuntimeCompanyMetric::PrimeRate => {
applied_through_owner_state =
service_set_company_prime_rate_target(state, company_id, *value);
}
RuntimeCompanyMetric::BookValuePerShare => {
applied_through_owner_state = service_set_company_direct_float_field(
state,
company_id,
0x32f,
*value as f64,
);
}
RuntimeCompanyMetric::InvestorConfidence => {
applied_through_owner_state = service_set_company_cached_share_price(
state,
company_id,
*value as f64,
);
}
RuntimeCompanyMetric::ManagementAttitude => {
applied_through_owner_state = service_set_company_issue_opinion_total(
state,
company_id,
crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE,
i64::from(*value),
);
}
_ => {
return Err(format!(
"unsupported governance metric {:?} in company governance effect",
metric
));
}
}
let company = state
.companies
.iter_mut()
@ -1274,24 +1431,27 @@ fn apply_runtime_effects(
company.credit_rating_score = Some(*value);
}
RuntimeCompanyMetric::PrimeRate => {
company.prime_rate = Some(*value);
if !applied_through_owner_state {
company.prime_rate = Some(*value);
}
}
RuntimeCompanyMetric::BookValuePerShare => {
company.book_value_per_share = *value;
if !applied_through_owner_state {
company.book_value_per_share = *value;
}
}
RuntimeCompanyMetric::InvestorConfidence => {
company.investor_confidence = *value;
if !applied_through_owner_state {
company.investor_confidence = *value;
}
}
RuntimeCompanyMetric::ManagementAttitude => {
company.management_attitude = *value;
if !applied_through_owner_state {
company.management_attitude = *value;
}
}
_ => {
return Err(format!(
"unsupported governance metric {:?} in company governance effect",
metric
));
}
}
_ => unreachable!(),
};
mutated_company_ids.insert(company_id);
}
}
@ -4356,6 +4516,165 @@ mod tests {
);
}
#[test]
fn set_company_governance_scalar_updates_owner_state_backed_metrics() {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 100,
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,
}],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 11,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![
RuntimeEffect::SetCompanyGovernanceScalar {
target: RuntimeCompanyTarget::Ids { ids: vec![1] },
metric: RuntimeCompanyMetric::PrimeRate,
value: 6,
},
RuntimeEffect::SetCompanyGovernanceScalar {
target: RuntimeCompanyTarget::Ids { ids: vec![1] },
metric: RuntimeCompanyMetric::BookValuePerShare,
value: 2620,
},
RuntimeEffect::SetCompanyGovernanceScalar {
target: RuntimeCompanyTarget::Ids { ids: vec![1] },
metric: RuntimeCompanyMetric::InvestorConfidence,
value: 37,
},
RuntimeEffect::SetCompanyGovernanceScalar {
target: RuntimeCompanyTarget::Ids { ids: vec![1] },
metric: RuntimeCompanyMetric::ManagementAttitude,
value: 58,
},
],
}],
service_state: RuntimeServiceState {
world_issue_opinion_base_terms_raw_i32: {
let mut terms = vec![0; 0x3b];
terms[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize] = 0;
terms
},
company_market_state: BTreeMap::from([(
1,
crate::RuntimeCompanyMarketState {
issue_opinion_terms_raw_i32: vec![0; 0x3b],
..crate::RuntimeCompanyMarketState::default()
},
)]),
..RuntimeServiceState::default()
},
calendar: crate::CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 0,
},
world_flags: BTreeMap::new(),
save_profile: crate::RuntimeSaveProfileState::default(),
world_restore: crate::RuntimeWorldRestoreState {
issue_37_value: Some(5.0f32.to_bits()),
..crate::RuntimeWorldRestoreState::default()
},
metadata: BTreeMap::new(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
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,
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(),
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("governance effect should apply through owner state");
assert_eq!(state.companies[0].prime_rate, Some(6));
assert_eq!(state.companies[0].book_value_per_share, 2620);
assert_eq!(state.companies[0].investor_confidence, 37);
assert_eq!(state.companies[0].management_attitude, 58);
assert_eq!(
crate::runtime::runtime_company_prime_rate(&state, 1),
Some(6)
);
assert_eq!(
crate::runtime::runtime_company_book_value_per_share(&state, 1),
Some(2620)
);
assert_eq!(
crate::runtime::runtime_company_investor_confidence(&state, 1),
Some(37)
);
assert_eq!(
crate::runtime::runtime_company_management_attitude(&state, 1),
Some(58)
);
assert_eq!(
state.service_state.company_market_state[&1]
.direct_control_transfer_float_fields_raw_u32
.get(&0x32f)
.copied(),
Some(2620.0f32.to_bits())
);
assert_eq!(
state.service_state.company_market_state[&1].cached_share_price_raw_u32,
37.0f32.to_bits()
);
assert_eq!(
state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32
[crate::RUNTIME_WORLD_ISSUE_PRIME_RATE as usize],
100
);
assert_eq!(
state.service_state.company_market_state[&1].issue_opinion_terms_raw_i32
[crate::RUNTIME_WORLD_ISSUE_MANAGEMENT_ATTITUDE as usize],
58
);
}
#[test]
fn applies_named_locomotive_availability_effects() {
let mut state = RuntimeState {