Broaden chairman governance event coverage

This commit is contained in:
Jan Petykiewicz 2026-04-16 16:36:48 -07:00
commit f8350a48c5
18 changed files with 1939 additions and 13 deletions

View file

@ -3335,6 +3335,16 @@ mod tests {
}
}
fn selected_chairman_negative_sentinel_scope()
-> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
crate::SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
territory_scope_selector_is_0x63: false,
source_row_indexes: vec![0],
}
}
fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0,
@ -4814,6 +4824,12 @@ mod tests {
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,
},
crate::RuntimeCompany {
company_id: 2,
@ -4825,6 +4841,12 @@ mod tests {
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,
},
],
selected_company_id: Some(1),
@ -5023,6 +5045,12 @@ mod tests {
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,
},
crate::RuntimeCompany {
company_id: 2,
@ -5034,6 +5062,12 @@ mod tests {
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,
},
crate::RuntimeCompany {
company_id: 3,
@ -5045,6 +5079,12 @@ mod tests {
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,
},
],
selected_company_id: Some(3),
@ -6638,6 +6678,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
players: Vec::new(),
@ -6799,6 +6845,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
territories: vec![crate::RuntimeTerritory {
@ -7102,6 +7154,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
..state()
@ -7500,6 +7558,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
..state()
@ -8804,6 +8868,375 @@ mod tests {
);
}
#[test]
fn overlays_selected_chairman_conditions_into_imported_runtime_records() {
let base_state = RuntimeState {
calendar: CalendarPoint {
year: 1840,
month_slot: 1,
phase_slot: 2,
tick_slot: 3,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
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: Some(1),
book_value_per_share: 2620,
investor_confidence: 37,
management_attitude: 58,
takeover_cooldown_year: Some(1839),
merger_cooldown_year: Some(1838),
}],
selected_company_id: Some(1),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: vec![
crate::RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 500,
linked_company_id: Some(1),
company_holdings: BTreeMap::new(),
holdings_value_total: 700,
net_worth_total: 1200,
purchasing_power_total: 1500,
},
crate::RuntimeChairmanProfile {
profile_id: 2,
name: "Chairman Two".to_string(),
active: true,
current_cash: 200,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 400,
net_worth_total: 600,
purchasing_power_total: 800,
},
],
selected_chairman_profile_id: Some(1),
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(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 71,
live_record_count: 1,
live_entry_ids: vec![71],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 71,
payload_offset: Some(0x7202),
payload_len: Some(136),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(6),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: vec![],
standalone_condition_row_count: 1,
standalone_condition_rows: vec![
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: 2218,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&500_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Player Cash".to_string()),
semantic_family: Some("numeric_threshold".to_string()),
semantic_preview: Some("Test Player Cash == 500".to_string()),
recovered_cargo_slot: None,
recovered_cargo_class: None,
requires_candidate_name_binding: false,
notes: vec![],
},
],
negative_sentinel_scope: Some(selected_chairman_negative_sentinel_scope()),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_world_flag_row(
110,
"Disable Stock Buying and Selling",
true,
)],
decoded_conditions: vec![RuntimeCondition::ChairmanNumericThreshold {
target: RuntimeChairmanTarget::SelectedChairman,
metric: crate::RuntimeChairmanMetric::CurrentCash,
comparator: RuntimeConditionComparator::Eq,
value: 500,
}],
decoded_actions: vec![RuntimeEffect::SetWorldFlag {
key: "world.chairman_condition_imported".to_string(),
value: true,
}],
executable_import_ready: true,
notes: vec!["chairman metric condition gates a world-side effect".to_string()],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"chairman-condition-overlay",
None,
)
.expect("overlay import should project");
assert_eq!(import.state.event_runtime_records.len(), 1);
assert_eq!(
import.state.event_runtime_records[0].conditions,
vec![RuntimeCondition::ChairmanNumericThreshold {
target: RuntimeChairmanTarget::SelectedChairman,
metric: crate::RuntimeChairmanMetric::CurrentCash,
comparator: RuntimeConditionComparator::Eq,
value: 500,
}]
);
crate::execute_step_command(
&mut import.state,
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("chairman-gated trigger should execute");
assert_eq!(
import
.state
.world_flags
.get("world.chairman_condition_imported"),
Some(&true)
);
}
#[test]
fn overlays_book_value_conditions_into_imported_runtime_records() {
let base_state = RuntimeState {
calendar: CalendarPoint {
year: 1840,
month_slot: 1,
phase_slot: 2,
tick_slot: 3,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![crate::RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
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: 2620,
investor_confidence: 37,
management_attitude: 58,
takeover_cooldown_year: Some(1839),
merger_cooldown_year: Some(1838),
}],
selected_company_id: Some(1),
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,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
let save_slice = SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
special_conditions_table: None,
event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 72,
live_record_count: 1,
live_entry_ids: vec![72],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![crate::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 72,
payload_offset: Some(0x7202),
payload_len: Some(136),
decode_status: "parity_only".to_string(),
payload_family: "real_packed_v1".to_string(),
trigger_kind: Some(7),
active: None,
marks_collection_dirty: None,
one_shot: Some(false),
compact_control: Some(real_compact_control()),
text_bands: vec![],
standalone_condition_row_count: 1,
standalone_condition_rows: vec![
crate::SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: 2620,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Book Value Per Share".to_string()),
semantic_family: Some("numeric_threshold".to_string()),
semantic_preview: Some("Test Book Value Per Share == 2620".to_string()),
recovered_cargo_slot: None,
recovered_cargo_class: None,
requires_candidate_name_binding: false,
notes: vec![],
},
],
negative_sentinel_scope: Some(company_negative_sentinel_scope(
RuntimeCompanyConditionTestScope::SelectedCompanyOnly,
)),
grouped_effect_row_counts: vec![1, 0, 0, 0],
grouped_effect_rows: vec![real_world_flag_row(
110,
"Disable Stock Buying and Selling",
true,
)],
decoded_conditions: vec![RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany,
metric: crate::RuntimeCompanyMetric::BookValuePerShare,
comparator: RuntimeConditionComparator::Eq,
value: 2620,
}],
decoded_actions: vec![RuntimeEffect::SetWorldFlag {
key: "world.book_value_condition_imported".to_string(),
value: true,
}],
executable_import_ready: true,
notes: vec![
"book value per share condition gates a world-side effect".to_string(),
],
}],
}),
notes: vec![],
};
let mut import = project_save_slice_overlay_to_runtime_state_import(
&base_state,
&save_slice,
"company-book-value-condition-overlay",
None,
)
.expect("overlay import should project");
assert_eq!(import.state.event_runtime_records.len(), 1);
assert_eq!(
import.state.event_runtime_records[0].conditions,
vec![RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::SelectedCompany,
metric: crate::RuntimeCompanyMetric::BookValuePerShare,
comparator: RuntimeConditionComparator::Eq,
value: 2620,
}]
);
crate::execute_step_command(
&mut import.state,
&crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("company-governance trigger should execute");
assert_eq!(
import
.state
.world_flags
.get("world.book_value_condition_imported"),
Some(&true)
);
}
#[test]
fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() {
let base_state = state();
@ -9058,6 +9491,12 @@ mod tests {
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,
},
crate::RuntimeCompany {
company_id: 7,
@ -9069,6 +9508,12 @@ mod tests {
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,
},
],
selected_company_id: Some(42),
@ -9272,6 +9717,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
..state()
@ -9377,6 +9828,12 @@ mod tests {
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,
},
crate::RuntimeCompany {
company_id: 7,
@ -9388,6 +9845,12 @@ mod tests {
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,
},
],
selected_company_id: Some(42),
@ -9533,6 +9996,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
trains: vec![
@ -9767,6 +10236,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
trains: vec![RuntimeTrain {
@ -9886,6 +10361,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
..state()
@ -10004,6 +10485,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
players: Vec::new(),
@ -10190,6 +10677,12 @@ mod tests {
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,
}],
selected_company_id: Some(42),
players: Vec::new(),

View file

@ -33,6 +33,18 @@ pub struct RuntimeCompany {
#[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind,
#[serde(default)]
pub linked_chairman_profile_id: Option<u32>,
#[serde(default)]
pub book_value_per_share: i64,
#[serde(default)]
pub investor_confidence: i64,
#[serde(default)]
pub management_attitude: i64,
#[serde(default)]
pub takeover_cooldown_year: Option<u32>,
#[serde(default)]
pub merger_cooldown_year: Option<u32>,
#[serde(default)]
pub track_piece_counts: RuntimeTrackPieceCounts,
}
@ -235,6 +247,9 @@ pub enum RuntimeCompanyMetric {
TotalDebt,
CreditRating,
PrimeRate,
BookValuePerShare,
InvestorConfidence,
ManagementAttitude,
TrackPiecesTotal,
TrackPiecesSingle,
TrackPiecesDouble,
@ -918,6 +933,46 @@ impl RuntimeState {
));
}
}
for company in &self.companies {
if let Some(linked_chairman_profile_id) = company.linked_chairman_profile_id {
let linked_profile = self
.chairman_profiles
.iter()
.find(|profile| profile.profile_id == linked_chairman_profile_id)
.ok_or_else(|| {
format!(
"company {} references unknown linked_chairman_profile_id {}",
company.company_id, linked_chairman_profile_id
)
})?;
if linked_profile.linked_company_id != Some(company.company_id) {
return Err(format!(
"company {} linked_chairman_profile_id {} must point back through linked_company_id",
company.company_id, linked_chairman_profile_id
));
}
}
}
for chairman in &self.chairman_profiles {
if let Some(linked_company_id) = chairman.linked_company_id {
let linked_company = self
.companies
.iter()
.find(|company| company.company_id == linked_company_id)
.ok_or_else(|| {
format!(
"chairman_profile {} references unknown linked_company_id {}",
chairman.profile_id, linked_company_id
)
})?;
if linked_company.linked_chairman_profile_id != Some(chairman.profile_id) {
return Err(format!(
"chairman_profile {} linked_company_id {} must point back through linked_chairman_profile_id",
chairman.profile_id, linked_company_id
));
}
}
}
if let Some(selected_player_id) = self.selected_player_id {
if !seen_player_ids.contains(&selected_player_id) {
return Err(format!(
@ -1772,6 +1827,12 @@ mod tests {
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::Unknown,
},
RuntimeCompany {
@ -1783,6 +1844,12 @@ mod tests {
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::Unknown,
},
],
@ -1895,6 +1962,12 @@ mod tests {
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::Unknown,
}],
selected_company_id: None,
@ -1956,6 +2029,12 @@ mod tests {
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::Unknown,
}],
selected_company_id: None,
@ -2129,6 +2208,12 @@ mod tests {
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: Some(2),
@ -2177,6 +2262,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
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: Some(1),
@ -2225,6 +2316,12 @@ mod tests {
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,
@ -2290,6 +2387,12 @@ mod tests {
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,
@ -2345,6 +2448,12 @@ mod tests {
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,
@ -2404,6 +2513,12 @@ mod tests {
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,
@ -2459,6 +2574,12 @@ mod tests {
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,
@ -2520,6 +2641,12 @@ mod tests {
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,
@ -2575,6 +2702,12 @@ mod tests {
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,
@ -2607,4 +2740,122 @@ mod tests {
assert!(state.validate().is_err());
}
#[test]
fn rejects_company_with_unknown_linked_chairman_profile() {
let 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: 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: Some(9),
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::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,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
#[test]
fn rejects_mismatched_company_chairman_back_links() {
let 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: 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: Some(1),
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: 0,
linked_company_id: None,
company_holdings: BTreeMap::new(),
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(),
cargo_production_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
};
assert!(state.validate().is_err());
}
}

View file

@ -7,10 +7,11 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator,
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope,
RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric,
RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanTarget,
RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric,
};
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
@ -232,6 +233,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealOrdinaryConditionMetric {
Company(RuntimeCompanyMetric),
Chairman(RuntimeChairmanMetric),
Territory(RuntimeTerritoryMetric),
CompanyTerritory(RuntimeTrackMetric),
}
@ -344,6 +346,11 @@ const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [
];
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435;
const REAL_CHAIRMAN_CASH_CONDITION_ID: i32 = 2218;
const REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID: i32 = 2239;
const REAL_CHAIRMAN_NET_WORTH_CONDITION_ID: i32 = 2240;
const REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID: i32 = 1247;
const REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID: i32 = 2620;
const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200;
const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422;
const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423;
@ -354,7 +361,7 @@ const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421;
const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547;
const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516;
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 38] = [
RealOrdinaryConditionMetadata {
raw_condition_id: 1802,
label: "Current Cash",
@ -362,6 +369,34 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [
RuntimeCompanyMetric::CurrentCash,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID,
label: "Player Cash",
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
RuntimeChairmanMetric::CurrentCash,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID,
label: "Player Stock Value",
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
RuntimeChairmanMetric::HoldingsValueTotal,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_CHAIRMAN_NET_WORTH_CONDITION_ID,
label: "Player Net Worth",
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
RuntimeChairmanMetric::NetWorthTotal,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID,
label: "Purchasing Power",
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(
RuntimeChairmanMetric::PurchasingPowerTotal,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 951,
label: "Total Debt",
@ -383,6 +418,13 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [
RuntimeCompanyMetric::PrimeRate,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID,
label: "Book Value Per Share",
kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(
RuntimeCompanyMetric::BookValuePerShare,
)),
},
RealOrdinaryConditionMetadata {
raw_condition_id: 2293,
label: "Company Track Pieces",
@ -3258,6 +3300,18 @@ fn decode_real_condition_row(
value,
})
}
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Chairman(metric)) => {
negative_sentinel_scope.and_then(|scope| {
real_condition_chairman_target(scope).map(|target| {
RuntimeCondition::ChairmanNumericThreshold {
target,
metric,
comparator,
value,
}
})
})
}
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63)
@ -3369,6 +3423,20 @@ fn decode_world_flag_condition(
})
}
fn real_condition_chairman_target(
scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary,
) -> Option<RuntimeChairmanTarget> {
match scope.player_test_scope {
RuntimePlayerConditionTestScope::AllPlayers => Some(RuntimeChairmanTarget::AllActive),
RuntimePlayerConditionTestScope::SelectedPlayerOnly => {
Some(RuntimeChairmanTarget::SelectedChairman)
}
RuntimePlayerConditionTestScope::Disabled
| RuntimePlayerConditionTestScope::AiPlayersOnly
| RuntimePlayerConditionTestScope::HumanPlayersOnly => None,
}
}
fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -10397,6 +10465,102 @@ mod tests {
assert_eq!(access_cost.label, "Access Rights Cost:");
}
#[test]
fn looks_up_checked_in_chairman_and_governance_condition_metadata() {
let chairman_cash = real_ordinary_condition_metadata(REAL_CHAIRMAN_CASH_CONDITION_ID)
.expect("chairman cash condition metadata should exist");
assert_eq!(chairman_cash.label, "Player Cash");
let holdings = real_ordinary_condition_metadata(REAL_CHAIRMAN_HOLDINGS_TOTAL_CONDITION_ID)
.expect("chairman holdings condition metadata should exist");
assert_eq!(holdings.label, "Player Stock Value");
let net_worth = real_ordinary_condition_metadata(REAL_CHAIRMAN_NET_WORTH_CONDITION_ID)
.expect("chairman net worth condition metadata should exist");
assert_eq!(net_worth.label, "Player Net Worth");
let purchasing_power =
real_ordinary_condition_metadata(REAL_CHAIRMAN_PURCHASING_POWER_CONDITION_ID)
.expect("chairman purchasing-power condition metadata should exist");
assert_eq!(purchasing_power.label, "Purchasing Power");
let book_value = real_ordinary_condition_metadata(REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID)
.expect("book value condition metadata should exist");
assert_eq!(book_value.label, "Book Value Per Share");
}
#[test]
fn decodes_chairman_cash_condition_from_selected_player_scope() {
let row = SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: REAL_CHAIRMAN_CASH_CONDITION_ID,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&500_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Player Cash".to_string()),
semantic_family: Some("numeric_threshold".to_string()),
semantic_preview: Some("Test Player Cash == 500".to_string()),
recovered_cargo_slot: None,
recovered_cargo_class: None,
requires_candidate_name_binding: false,
notes: vec![],
};
let negative_scope = SmpLoadedPackedEventNegativeSentinelScopeSummary {
company_test_scope: RuntimeCompanyConditionTestScope::Disabled,
player_test_scope: RuntimePlayerConditionTestScope::SelectedPlayerOnly,
territory_scope_selector_is_0x63: false,
source_row_indexes: vec![0],
};
assert_eq!(
decode_real_condition_row(&row, Some(&negative_scope)),
Some(RuntimeCondition::ChairmanNumericThreshold {
target: RuntimeChairmanTarget::SelectedChairman,
metric: RuntimeChairmanMetric::CurrentCash,
comparator: RuntimeConditionComparator::Eq,
value: 500,
})
);
}
#[test]
fn decodes_book_value_per_share_condition_to_company_metric() {
let row = SmpLoadedPackedEventConditionRowSummary {
row_index: 0,
raw_condition_id: REAL_BOOK_VALUE_PER_SHARE_CONDITION_ID,
subtype: 4,
flag_bytes: {
let mut bytes = vec![0; 25];
bytes[0..4].copy_from_slice(&2620_i32.to_le_bytes());
bytes
},
candidate_name: None,
comparator: Some("eq".to_string()),
metric: Some("Book Value Per Share".to_string()),
semantic_family: Some("numeric_threshold".to_string()),
semantic_preview: Some("Test Book Value Per Share == 2620".to_string()),
recovered_cargo_slot: None,
recovered_cargo_class: None,
requires_candidate_name_binding: false,
notes: vec![],
};
assert_eq!(
decode_real_condition_row(&row, None),
Some(RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany,
metric: RuntimeCompanyMetric::BookValuePerShare,
comparator: RuntimeConditionComparator::Eq,
value: 2620,
})
);
}
#[test]
fn looks_up_checked_in_world_flag_descriptor_metadata() {
let metadata =

View file

@ -405,6 +405,14 @@ fn apply_runtime_effects(
state.selected_chairman_profile_id = None;
}
if let Some(linked_company_id) = linked_company_id {
if let Some(company) = state
.companies
.iter_mut()
.find(|company| company.company_id == linked_company_id)
{
company.linked_chairman_profile_id = None;
mutated_company_ids.insert(linked_company_id);
}
for other in &mut state.chairman_profiles {
if other.profile_id != profile_id
&& other.linked_company_id == Some(linked_company_id)
@ -1222,6 +1230,9 @@ fn company_metric_value(company: &crate::RuntimeCompany, metric: RuntimeCompanyM
RuntimeCompanyMetric::TotalDebt => company.debt as i64,
RuntimeCompanyMetric::CreditRating => company.credit_rating_score.unwrap_or(0),
RuntimeCompanyMetric::PrimeRate => company.prime_rate.unwrap_or(0),
RuntimeCompanyMetric::BookValuePerShare => company.book_value_per_share,
RuntimeCompanyMetric::InvestorConfidence => company.investor_confidence,
RuntimeCompanyMetric::ManagementAttitude => company.management_attitude,
RuntimeCompanyMetric::TrackPiecesTotal => i64::from(company.track_piece_counts.total),
RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single),
RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double),
@ -1402,10 +1413,11 @@ mod tests {
use super::*;
use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePlayer,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget,
RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState,
CalendarPoint, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget,
RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition,
RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory,
RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState,
};
fn state() -> RuntimeState {
@ -1430,6 +1442,12 @@ mod tests {
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,
}],
selected_company_id: None,
players: Vec::new(),
@ -1613,6 +1631,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -1624,6 +1648,12 @@ mod tests {
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 {
@ -1824,6 +1854,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -1835,6 +1871,12 @@ mod tests {
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,
},
],
selected_company_id: Some(1),
@ -1970,6 +2012,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -1981,6 +2029,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
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,
},
RuntimeCompany {
company_id: 3,
@ -1992,6 +2046,12 @@ mod tests {
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![
@ -2068,6 +2128,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: Some(8),
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
}],
selected_company_id: Some(1),
event_runtime_records: vec![RuntimeEventRecord {
@ -2148,6 +2214,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -2159,6 +2231,12 @@ mod tests {
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 {
@ -2203,6 +2281,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -2214,6 +2298,12 @@ mod tests {
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,
},
],
territories: vec![
@ -2588,6 +2678,12 @@ mod tests {
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: 30,
@ -2982,6 +3078,12 @@ mod tests {
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,
},
RuntimeCompany {
company_id: 2,
@ -2993,6 +3095,12 @@ mod tests {
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,
},
],
selected_company_id: Some(1),
@ -3114,4 +3222,280 @@ mod tests {
assert!(!state.trains[1].retired);
assert!(!state.trains[2].retired);
}
#[test]
fn set_chairman_cash_supports_all_active_target() {
let mut state = RuntimeState {
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,
},
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,
},
RuntimeChairmanProfile {
profile_id: 3,
name: "Chairman Three".to_string(),
active: false,
current_cash: 30,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 93,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::SetChairmanCash {
target: RuntimeChairmanTarget::AllActive,
value: 77,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("all-active chairman cash effect should succeed");
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);
}
#[test]
fn deactivate_chairman_clears_selected_and_company_links_for_ids_target() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
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: Some(1),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 80,
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: Some(2),
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
},
],
chairman_profiles: vec![
RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 10,
linked_company_id: Some(1),
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
},
RuntimeChairmanProfile {
profile_id: 2,
name: "Chairman Two".to_string(),
active: true,
current_cash: 20,
linked_company_id: Some(2),
company_holdings: BTreeMap::new(),
holdings_value_total: 0,
net_worth_total: 0,
purchasing_power_total: 0,
},
],
selected_chairman_profile_id: Some(2),
event_runtime_records: vec![RuntimeEventRecord {
record_id: 94,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: Vec::new(),
effects: vec![RuntimeEffect::DeactivateChairman {
target: RuntimeChairmanTarget::Ids { ids: vec![2] },
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("ids-target chairman deactivation should succeed");
assert!(state.chairman_profiles[0].active);
assert!(!state.chairman_profiles[1].active);
assert_eq!(state.chairman_profiles[1].linked_company_id, None);
assert_eq!(state.selected_chairman_profile_id, None);
assert_eq!(state.companies[0].linked_chairman_profile_id, Some(1));
assert_eq!(state.companies[1].linked_chairman_profile_id, None);
}
#[test]
fn company_governance_metric_conditions_gate_execution() {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
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: 2620,
investor_confidence: 37,
management_attitude: 58,
takeover_cooldown_year: Some(1844),
merger_cooldown_year: Some(1845),
}],
selected_company_id: Some(1),
event_runtime_records: vec![RuntimeEventRecord {
record_id: 95,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: vec![RuntimeCondition::CompanyNumericThreshold {
target: RuntimeCompanyTarget::SelectedCompany,
metric: crate::RuntimeCompanyMetric::BookValuePerShare,
comparator: RuntimeConditionComparator::Eq,
value: 2620,
}],
effects: vec![RuntimeEffect::SetWorldFlag {
key: "world.book_value_gate_passed".to_string(),
value: true,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("book-value company condition should gate execution");
assert_eq!(
state.world_flags.get("world.book_value_gate_passed"),
Some(&true)
);
}
#[test]
fn chairman_metric_conditions_support_all_active_target() {
let mut state = RuntimeState {
chairman_profiles: vec![
RuntimeChairmanProfile {
profile_id: 1,
name: "Chairman One".to_string(),
active: true,
current_cash: 20,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 500,
net_worth_total: 700,
purchasing_power_total: 900,
},
RuntimeChairmanProfile {
profile_id: 2,
name: "Chairman Two".to_string(),
active: true,
current_cash: 30,
linked_company_id: None,
company_holdings: BTreeMap::new(),
holdings_value_total: 600,
net_worth_total: 800,
purchasing_power_total: 1000,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 96,
trigger_kind: 6,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
conditions: vec![RuntimeCondition::ChairmanNumericThreshold {
target: RuntimeChairmanTarget::AllActive,
metric: RuntimeChairmanMetric::PurchasingPowerTotal,
comparator: RuntimeConditionComparator::Ge,
value: 900,
}],
effects: vec![RuntimeEffect::SetWorldFlag {
key: "world.chairman_gate_passed".to_string(),
value: true,
}],
}],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 6 },
)
.expect("all-active chairman condition should gate execution");
assert_eq!(
state.world_flags.get("world.chairman_gate_passed"),
Some(&true)
);
}
}

View file

@ -36,6 +36,9 @@ pub struct RuntimeSummary {
pub chairman_profile_count: usize,
pub active_chairman_profile_count: usize,
pub selected_chairman_profile_id: Option<u32>,
pub linked_chairman_company_count: usize,
pub company_takeover_cooldown_count: usize,
pub company_merger_cooldown_count: usize,
pub train_count: usize,
pub active_train_count: usize,
pub retired_train_count: usize,
@ -181,6 +184,21 @@ impl RuntimeSummary {
.filter(|profile| profile.active)
.count(),
selected_chairman_profile_id: state.selected_chairman_profile_id,
linked_chairman_company_count: state
.companies
.iter()
.filter(|company| company.linked_chairman_profile_id.is_some())
.count(),
company_takeover_cooldown_count: state
.companies
.iter()
.filter(|company| company.takeover_cooldown_year.is_some())
.count(),
company_merger_cooldown_count: state
.companies
.iter()
.filter(|company| company.merger_cooldown_year.is_some())
.count(),
train_count: state.trains.len(),
active_train_count: state.trains.iter().filter(|train| train.active).count(),
retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
@ -934,6 +952,12 @@ mod tests {
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 {
@ -945,6 +969,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false,
available_track_laying_capacity: Some(7),
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::Ai,
},
],