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

@ -21,6 +21,10 @@ still exercise the same runtime without a parallel packed executor. The first gr
chairman-profile runtime slice now exists too: overlay-backed selected-chairman context plus the chairman-profile runtime slice now exists too: overlay-backed selected-chairman context plus the
hidden grouped target-subject lane let those same real descriptors `1` and `14` execute on hidden grouped target-subject lane let those same real descriptors `1` and `14` execute on
selected-chairman scope, while wider chairman target scopes remain explicit parity. The first grounded selected-chairman scope, while wider chairman target scopes remain explicit parity. The first grounded
chairman and governance condition batch is grounded too: selected-chairman cash / holdings / net
worth / purchasing-power thresholds and company book-value-per-share thresholds now import through
the normal event-service path, while wider chairman target scopes and the still-unpinned investor-
confidence / management-attitude condition ids remain explicit frontier. The first grounded
condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and
the first ordinary nonnegative condition batch now executes too: numeric-threshold company the first ordinary nonnegative condition batch now executes too: numeric-threshold company
finance, company track, aggregate territory track, and company-territory track rows can import finance, company track, aggregate territory track, and company-territory track rows can import

View file

@ -4495,6 +4495,14 @@ mod tests {
let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json", "../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json",
); );
let chairman_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json");
let company_governance_condition_overlay_fixture = PathBuf::from(env!(
"CARGO_MANIFEST_DIR"
))
.join(
"../../fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json",
);
run_runtime_summarize_fixture(&parity_fixture) run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize"); .expect("save-slice-backed parity fixture should summarize");
@ -4544,6 +4552,10 @@ mod tests {
.expect("save-slice-backed chairman missing-context fixture should summarize"); .expect("save-slice-backed chairman missing-context fixture should summarize");
run_runtime_summarize_fixture(&chairman_scope_parity_fixture) run_runtime_summarize_fixture(&chairman_scope_parity_fixture)
.expect("save-slice-backed chairman scope parity fixture should summarize"); .expect("save-slice-backed chairman scope parity fixture should summarize");
run_runtime_summarize_fixture(&chairman_condition_overlay_fixture)
.expect("overlay-backed chairman condition fixture should summarize");
run_runtime_summarize_fixture(&company_governance_condition_overlay_fixture)
.expect("overlay-backed company governance condition fixture should summarize");
} }
#[test] #[test]

View file

@ -351,6 +351,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),

View file

@ -78,6 +78,12 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)] #[serde(default)]
pub selected_chairman_profile_id: Option<u32>, pub selected_chairman_profile_id: Option<u32>,
#[serde(default)] #[serde(default)]
pub linked_chairman_company_count: Option<usize>,
#[serde(default)]
pub company_takeover_cooldown_count: Option<usize>,
#[serde(default)]
pub company_merger_cooldown_count: Option<usize>,
#[serde(default)]
pub train_count: Option<usize>, pub train_count: Option<usize>,
#[serde(default)] #[serde(default)]
pub active_train_count: Option<usize>, pub active_train_count: Option<usize>,
@ -473,6 +479,30 @@ impl ExpectedRuntimeSummary {
)); ));
} }
} }
if let Some(count) = self.linked_chairman_company_count {
if actual.linked_chairman_company_count != count {
mismatches.push(format!(
"linked_chairman_company_count mismatch: expected {count}, got {}",
actual.linked_chairman_company_count
));
}
}
if let Some(count) = self.company_takeover_cooldown_count {
if actual.company_takeover_cooldown_count != count {
mismatches.push(format!(
"company_takeover_cooldown_count mismatch: expected {count}, got {}",
actual.company_takeover_cooldown_count
));
}
}
if let Some(count) = self.company_merger_cooldown_count {
if actual.company_merger_cooldown_count != count {
mismatches.push(format!(
"company_merger_cooldown_count mismatch: expected {count}, got {}",
actual.company_merger_cooldown_count
));
}
}
if let Some(count) = self.train_count { if let Some(count) = self.train_count {
if actual.train_count != count { if actual.train_count != count {
mismatches.push(format!( mismatches.push(format!(

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> { fn real_grouped_rows() -> Vec<crate::SmpLoadedPackedEventGroupedEffectRowSummary> {
vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary { vec![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
group_index: 0, group_index: 0,
@ -4814,6 +4824,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { crate::RuntimeCompany {
company_id: 2, company_id: 2,
@ -4825,6 +4841,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(1),
@ -5023,6 +5045,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { crate::RuntimeCompany {
company_id: 2, company_id: 2,
@ -5034,6 +5062,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { crate::RuntimeCompany {
company_id: 3, company_id: 3,
@ -5045,6 +5079,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(3),
@ -6638,6 +6678,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
@ -6799,6 +6845,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
territories: vec![crate::RuntimeTerritory { territories: vec![crate::RuntimeTerritory {
@ -7102,6 +7154,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
..state() ..state()
@ -7500,6 +7558,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
..state() ..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] #[test]
fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() { fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() {
let base_state = state(); let base_state = state();
@ -9058,6 +9491,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { crate::RuntimeCompany {
company_id: 7, company_id: 7,
@ -9069,6 +9508,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
@ -9272,6 +9717,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
..state() ..state()
@ -9377,6 +9828,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { crate::RuntimeCompany {
company_id: 7, company_id: 7,
@ -9388,6 +9845,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
@ -9533,6 +9996,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
trains: vec![ trains: vec![
@ -9767,6 +10236,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
trains: vec![RuntimeTrain { trains: vec![RuntimeTrain {
@ -9886,6 +10361,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
..state() ..state()
@ -10004,6 +10485,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),
@ -10190,6 +10677,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(42),
players: Vec::new(), players: Vec::new(),

View file

@ -33,6 +33,18 @@ pub struct RuntimeCompany {
#[serde(default)] #[serde(default)]
pub controller_kind: RuntimeCompanyControllerKind, pub controller_kind: RuntimeCompanyControllerKind,
#[serde(default)] #[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, pub track_piece_counts: RuntimeTrackPieceCounts,
} }
@ -235,6 +247,9 @@ pub enum RuntimeCompanyMetric {
TotalDebt, TotalDebt,
CreditRating, CreditRating,
PrimeRate, PrimeRate,
BookValuePerShare,
InvestorConfidence,
ManagementAttitude,
TrackPiecesTotal, TrackPiecesTotal,
TrackPiecesSingle, TrackPiecesSingle,
TrackPiecesDouble, 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 let Some(selected_player_id) = self.selected_player_id {
if !seen_player_ids.contains(&selected_player_id) { if !seen_player_ids.contains(&selected_player_id) {
return Err(format!( return Err(format!(
@ -1772,6 +1827,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
RuntimeCompany { RuntimeCompany {
@ -1783,6 +1844,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Unknown,
}, },
], ],
@ -1895,6 +1962,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
@ -1956,6 +2029,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Unknown,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2129,6 +2208,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(2), selected_company_id: Some(2),
@ -2177,6 +2262,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: Some(1), selected_company_id: Some(1),
@ -2225,6 +2316,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2290,6 +2387,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2345,6 +2448,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2404,6 +2513,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2459,6 +2574,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2520,6 +2641,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2575,6 +2702,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}], }],
selected_company_id: None, selected_company_id: None,
@ -2607,4 +2740,122 @@ mod tests {
assert!(state.validate().is_err()); 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 sha2::{Digest, Sha256};
use crate::{ use crate::{
RuntimeCargoClass, RuntimeChairmanTarget, RuntimeCompanyConditionTestScope, RuntimeCargoClass, RuntimeChairmanMetric, RuntimeChairmanTarget,
RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition, RuntimeConditionComparator, RuntimeCompanyConditionTestScope, RuntimeCompanyMetric, RuntimeCompanyTarget, RuntimeCondition,
RuntimeEffect, RuntimeEventRecordTemplate, RuntimePlayerConditionTestScope, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecordTemplate,
RuntimePlayerTarget, RuntimeTerritoryMetric, RuntimeTerritoryTarget, RuntimeTrackMetric, RuntimePlayerConditionTestScope, RuntimePlayerTarget, RuntimeTerritoryMetric,
RuntimeTerritoryTarget, RuntimeTrackMetric,
}; };
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RealOrdinaryConditionMetric { enum RealOrdinaryConditionMetric {
Company(RuntimeCompanyMetric), Company(RuntimeCompanyMetric),
Chairman(RuntimeChairmanMetric),
Territory(RuntimeTerritoryMetric), Territory(RuntimeTerritoryMetric),
CompanyTerritory(RuntimeTrackMetric), CompanyTerritory(RuntimeTrackMetric),
} }
@ -344,6 +346,11 @@ const KNOWN_CARGO_SLOT_DEFINITIONS: [KnownCargoSlotDefinition; 11] = [
]; ];
const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; 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_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200;
const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422; const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422;
const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423; 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_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547;
const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516;
const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 38] = [
RealOrdinaryConditionMetadata { RealOrdinaryConditionMetadata {
raw_condition_id: 1802, raw_condition_id: 1802,
label: "Current Cash", label: "Current Cash",
@ -362,6 +369,34 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [
RuntimeCompanyMetric::CurrentCash, 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 { RealOrdinaryConditionMetadata {
raw_condition_id: 951, raw_condition_id: 951,
label: "Total Debt", label: "Total Debt",
@ -383,6 +418,13 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [
RuntimeCompanyMetric::PrimeRate, 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 { RealOrdinaryConditionMetadata {
raw_condition_id: 2293, raw_condition_id: 2293,
label: "Company Track Pieces", label: "Company Track Pieces",
@ -3258,6 +3300,18 @@ fn decode_real_condition_row(
value, 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)) => { RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
negative_sentinel_scope negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63) .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( fn real_grouped_effect_descriptor_metadata(
descriptor_id: u32, descriptor_id: u32,
) -> Option<RealGroupedEffectDescriptorMetadata> { ) -> Option<RealGroupedEffectDescriptorMetadata> {
@ -10397,6 +10465,102 @@ mod tests {
assert_eq!(access_cost.label, "Access Rights Cost:"); 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] #[test]
fn looks_up_checked_in_world_flag_descriptor_metadata() { fn looks_up_checked_in_world_flag_descriptor_metadata() {
let metadata = let metadata =

View file

@ -405,6 +405,14 @@ fn apply_runtime_effects(
state.selected_chairman_profile_id = None; state.selected_chairman_profile_id = None;
} }
if let Some(linked_company_id) = linked_company_id { 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 { for other in &mut state.chairman_profiles {
if other.profile_id != profile_id if other.profile_id != profile_id
&& other.linked_company_id == Some(linked_company_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::TotalDebt => company.debt as i64,
RuntimeCompanyMetric::CreditRating => company.credit_rating_score.unwrap_or(0), RuntimeCompanyMetric::CreditRating => company.credit_rating_score.unwrap_or(0),
RuntimeCompanyMetric::PrimeRate => company.prime_rate.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::TrackPiecesTotal => i64::from(company.track_piece_counts.total),
RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single), RuntimeCompanyMetric::TrackPiecesSingle => i64::from(company.track_piece_counts.single),
RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double), RuntimeCompanyMetric::TrackPiecesDouble => i64::from(company.track_piece_counts.double),
@ -1402,10 +1413,11 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, CalendarPoint, RuntimeChairmanMetric, RuntimeChairmanProfile, RuntimeChairmanTarget,
RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePlayer, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeCondition,
RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory, RuntimeTerritoryTarget, RuntimeConditionComparator, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate,
RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState, RuntimePlayer, RuntimeSaveProfileState, RuntimeServiceState, RuntimeTerritory,
RuntimeTerritoryTarget, RuntimeTrackPieceCounts, RuntimeTrain, RuntimeWorldRestoreState,
}; };
fn state() -> RuntimeState { fn state() -> RuntimeState {
@ -1430,6 +1442,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, selected_company_id: None,
players: Vec::new(), players: Vec::new(),
@ -1613,6 +1631,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -1624,6 +1648,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { event_runtime_records: vec![RuntimeEventRecord {
@ -1824,6 +1854,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -1835,6 +1871,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(1),
@ -1970,6 +2012,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -1981,6 +2029,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 3, company_id: 3,
@ -1992,6 +2046,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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![ event_runtime_records: vec![
@ -2068,6 +2128,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: Some(8), 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), selected_company_id: Some(1),
event_runtime_records: vec![RuntimeEventRecord { event_runtime_records: vec![RuntimeEventRecord {
@ -2148,6 +2214,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -2159,6 +2231,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { event_runtime_records: vec![RuntimeEventRecord {
@ -2203,6 +2281,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -2214,6 +2298,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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![ territories: vec![
@ -2588,6 +2678,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { event_runtime_records: vec![RuntimeEventRecord {
record_id: 30, record_id: 30,
@ -2982,6 +3078,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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 { RuntimeCompany {
company_id: 2, company_id: 2,
@ -2993,6 +3095,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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), selected_company_id: Some(1),
@ -3114,4 +3222,280 @@ mod tests {
assert!(!state.trains[1].retired); assert!(!state.trains[1].retired);
assert!(!state.trains[2].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 chairman_profile_count: usize,
pub active_chairman_profile_count: usize, pub active_chairman_profile_count: usize,
pub selected_chairman_profile_id: Option<u32>, 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 train_count: usize,
pub active_train_count: usize, pub active_train_count: usize,
pub retired_train_count: usize, pub retired_train_count: usize,
@ -181,6 +184,21 @@ impl RuntimeSummary {
.filter(|profile| profile.active) .filter(|profile| profile.active)
.count(), .count(),
selected_chairman_profile_id: state.selected_chairman_profile_id, 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(), train_count: state.trains.len(),
active_train_count: state.trains.iter().filter(|train| train.active).count(), active_train_count: state.trains.iter().filter(|train| train.active).count(),
retired_train_count: state.trains.iter().filter(|train| train.retired).count(), retired_train_count: state.trains.iter().filter(|train| train.retired).count(),
@ -934,6 +952,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true, active: true,
available_track_laying_capacity: None, 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, controller_kind: RuntimeCompanyControllerKind::Human,
}, },
RuntimeCompany { RuntimeCompany {
@ -945,6 +969,12 @@ mod tests {
track_piece_counts: RuntimeTrackPieceCounts::default(), track_piece_counts: RuntimeTrackPieceCounts::default(),
active: false, active: false,
available_track_laying_capacity: Some(7), 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, controller_kind: RuntimeCompanyControllerKind::Ai,
}, },
], ],

View file

@ -87,6 +87,11 @@ The highest-value next passes are now:
- the first chairman-targeted real grouped rows now execute too through that same path when the - the first chairman-targeted real grouped rows now execute too through that same path when the
hidden grouped target-subject lane resolves to selected-chairman scope; broader chairman target hidden grouped target-subject lane resolves to selected-chairman scope; broader chairman target
scopes stay parity-only under `blocked_chairman_target_scope` scopes stay parity-only under `blocked_chairman_target_scope`
- chairman runtime ownership is broader now too: selected-chairman condition rows for chairman
cash, holdings value, net worth, and purchasing power import through the same service path, and
the first grounded company governance condition family now executes too via book-value-per-share
thresholds; wider chairman target ordinals and the still-unpinned investor-confidence /
management-attitude condition ids remain frontier
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask, - widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
and normalized effect semantics are all grounded, not just after row framing is parsed and normalized effect semantics are all grounded, not just after row framing is parsed
- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` - the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`

View file

@ -46,6 +46,13 @@ Implemented today:
`1` = `Player Cash` and `14` = `Deactivate Player` now also import and execute through the `1` = `Player Cash` and `14` = `Deactivate Player` now also import and execute through the
hidden grouped target-subject lane when it resolves to selected-chairman scope, while broader hidden grouped target-subject lane when it resolves to selected-chairman scope, while broader
chairman target scopes remain explicit parity on `blocked_chairman_target_scope` chairman target scopes remain explicit parity on `blocked_chairman_target_scope`
- chairman governance state is broader now too: companies carry explicit chairman links plus
book-value-per-share, investor-confidence, management-attitude, and takeover/merger cooldown
lanes, and the first grounded chairman/control-transfer ordinary-condition batch now imports and
executes through the same path for selected-chairman cash / holdings / net-worth /
purchasing-power thresholds plus company book-value-per-share thresholds; wider chairman target
ordinals and the still-unpinned investor-confidence / management-attitude condition ids remain
parity frontier
- a minimal event-owned train surface and an opaque economic-status lane now exist in runtime - a minimal event-owned train surface and an opaque economic-status lane now exist in runtime
state, and real descriptors `8` = `Economic Status`, `9` = `Confiscate All`, and `15` = state, and real descriptors `8` = `Economic Status`, `9` = `Confiscate All`, and `15` =
`Retire Train` now import and execute through the ordinary runtime path when overlay context `Retire Train` now import and execute through the ordinary runtime path when overlay context
@ -116,10 +123,11 @@ Implemented today:
That means the next implementation work is breadth, not bootstrap. The recommended next slice is That means the next implementation work is breadth, not bootstrap. The recommended next slice is
broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, broader real grouped-descriptor and ordinary condition-id coverage beyond the current access,
whole-game toggle, train, player, chairman selected-scope, numeric-threshold, named locomotive whole-game toggle, train, player, chairman selected-scope grouped effects, grounded
availability, named locomotive cost, world scalar override, and world-scalar condition batches. chairman/governance conditions, numeric-threshold, named locomotive availability, named locomotive
Richer runtime ownership should still be added only where a later descriptor or condition family cost, world scalar override, and world-scalar condition batches. Richer runtime ownership should
needs more than the current event-owned roster. still be added only where a later descriptor or condition family needs more than the current
event-owned roster.
## Why This Boundary ## Why This Boundary

View file

@ -0,0 +1,57 @@
{
"format_version": 1,
"fixture_id": "packed-event-chairman-condition-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving grounded chairman metric conditions import and execute through selected-chairman context."
},
"state_import_path": "packed-event-chairman-condition-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 6
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"chairman_profile_count": 2,
"active_chairman_profile_count": 2,
"selected_chairman_profile_id": 1,
"linked_chairman_company_count": 2,
"company_takeover_cooldown_count": 1,
"company_merger_cooldown_count": 1,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"world_flag_count": 10,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"world_flags": {
"world.chairman_condition_imported": true
},
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "chairman_numeric_threshold",
"target": {
"kind": "selected_chairman"
},
"metric": "current_cash",
"comparator": "eq",
"value": 500
}
]
}
]
}
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-chairman-condition-overlay",
"source": {
"description": "Overlay import combining chairman runtime context with a grounded chairman-metric condition sample."
},
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
"save_slice_path": "packed-event-chairman-condition-save-slice.json"
}

View file

@ -0,0 +1,190 @@
{
"format_version": 1,
"save_slice_id": "packed-event-chairman-condition-save-slice",
"source": {
"description": "Tracked save-slice document proving grounded chairman metric conditions import and execute through selected-chairman context.",
"original_save_filename": "captured-chairman-condition.gms",
"original_save_sha256": "chairman-condition-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"pins the first grounded chairman ordinary-condition family"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"named_locomotive_availability_table": null,
"cargo_catalog": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 71,
"live_record_count": 1,
"live_entry_ids": [
71
],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 71,
"payload_offset": 29186,
"payload_len": 136,
"decode_status": "executable",
"payload_family": "real_packed_v1",
"trigger_kind": 6,
"active": null,
"marks_collection_dirty": null,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 1,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [
0,
1,
2,
3
],
"grouped_scope_checkboxes_0x7ff": [
1,
0,
1,
0
],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [
-1,
10,
-1,
22
]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2218,
"subtype": 4,
"flag_bytes": [
244,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"candidate_name": null,
"comparator": "eq",
"metric": "Player Cash",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Player Cash == 500",
"requires_candidate_name_binding": false,
"notes": []
}
],
"negative_sentinel_scope": {
"company_test_scope": "disabled",
"player_test_scope": "selected_player_only",
"territory_scope_selector_is_0x63": false,
"source_row_indexes": [
0
]
},
"grouped_effect_row_counts": [
1,
0,
0,
0
],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 110,
"descriptor_label": "Disable Stock Buying and Selling",
"target_mask_bits": 8,
"parameter_family": "world_flag_toggle",
"opcode": 0,
"raw_scalar_value": 1,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "bool_toggle",
"semantic_family": "bool_toggle",
"semantic_preview": "Set Disable Stock Buying and Selling to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "chairman_numeric_threshold",
"target": {
"kind": "selected_chairman"
},
"metric": "current_cash",
"comparator": "eq",
"value": 500
}
],
"decoded_actions": [
{
"kind": "set_world_flag",
"key": "world.chairman_condition_imported",
"value": true
}
],
"executable_import_ready": true,
"notes": [
"chairman metric condition gates a world-side effect"
]
}
]
},
"notes": [
"real chairman ordinary-condition sample"
]
}
}

View file

@ -25,6 +25,12 @@
"credit_rating_score": 650, "credit_rating_score": 650,
"prime_rate": 5, "prime_rate": 5,
"controller_kind": "human", "controller_kind": "human",
"linked_chairman_profile_id": 1,
"book_value_per_share": 2620,
"investor_confidence": 37,
"management_attitude": 58,
"takeover_cooldown_year": 1839,
"merger_cooldown_year": 1838,
"track_piece_counts": { "track_piece_counts": {
"total": 20, "total": 20,
"single": 5, "single": 5,
@ -41,6 +47,12 @@
"credit_rating_score": 480, "credit_rating_score": 480,
"prime_rate": 6, "prime_rate": 6,
"controller_kind": "ai", "controller_kind": "ai",
"linked_chairman_profile_id": 2,
"book_value_per_share": 1400,
"investor_confidence": 22,
"management_attitude": 31,
"takeover_cooldown_year": null,
"merger_cooldown_year": null,
"track_piece_counts": { "track_piece_counts": {
"total": 8, "total": 8,
"single": 2, "single": 2,

View file

@ -0,0 +1,72 @@
{
"format_version": 1,
"fixture_id": "packed-event-company-governance-condition-overlay-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture proving grounded company governance conditions import and execute."
},
"state_import_path": "packed-event-company-governance-condition-overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_source": "base-snapshot-preserved",
"calendar_projection_is_placeholder": false,
"company_count": 2,
"chairman_profile_count": 2,
"active_chairman_profile_count": 2,
"selected_chairman_profile_id": 1,
"linked_chairman_company_count": 2,
"company_takeover_cooldown_count": 1,
"company_merger_cooldown_count": 1,
"packed_event_collection_present": true,
"packed_event_record_count": 1,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 1,
"event_runtime_record_count": 1,
"world_flag_count": 10,
"total_event_record_service_count": 1,
"total_trigger_dispatch_count": 1
},
"expected_state_fragment": {
"world_flags": {
"world.book_value_condition_imported": true
},
"packed_event_collection": {
"records": [
{
"import_outcome": "imported",
"decoded_conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "selected_company"
},
"metric": "book_value_per_share",
"comparator": "eq",
"value": 2620
}
]
}
]
},
"event_runtime_records": [
{
"conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "selected_company"
},
"metric": "book_value_per_share",
"comparator": "eq",
"value": 2620
}
]
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"format_version": 1,
"import_id": "packed-event-company-governance-condition-overlay",
"source": {
"description": "Overlay import combining company governance runtime context with a grounded book-value-per-share condition sample."
},
"base_snapshot_path": "packed-event-chairman-overlay-base-snapshot.json",
"save_slice_path": "packed-event-company-governance-condition-save-slice.json"
}

View file

@ -0,0 +1,190 @@
{
"format_version": 1,
"save_slice_id": "packed-event-company-governance-condition-save-slice",
"source": {
"description": "Tracked save-slice document proving grounded company governance conditions import and execute.",
"original_save_filename": "captured-company-governance-condition.gms",
"original_save_sha256": "company-governance-condition-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"pins the first grounded company control-transfer condition family"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"named_locomotive_availability_table": null,
"cargo_catalog": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 72,
"live_record_count": 1,
"live_entry_ids": [
72
],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 72,
"payload_offset": 29186,
"payload_len": 136,
"decode_status": "executable",
"payload_family": "real_packed_v1",
"trigger_kind": 7,
"active": null,
"marks_collection_dirty": null,
"one_shot": false,
"compact_control": {
"mode_byte_0x7ef": 6,
"primary_selector_0x7f0": 99,
"grouped_mode_0x7f4": 2,
"one_shot_header_0x7f5": 1,
"modifier_flag_0x7f9": 1,
"modifier_flag_0x7fa": 0,
"grouped_target_scope_ordinals_0x7fb": [
0,
1,
2,
3
],
"grouped_scope_checkboxes_0x7ff": [
1,
0,
1,
0
],
"summary_toggle_0x800": 1,
"grouped_territory_selectors_0x80f": [
-1,
10,
-1,
22
]
},
"text_bands": [],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [
{
"row_index": 0,
"raw_condition_id": 2620,
"subtype": 4,
"flag_bytes": [
60,
10,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"candidate_name": null,
"comparator": "eq",
"metric": "Book Value Per Share",
"semantic_family": "numeric_threshold",
"semantic_preview": "Test Book Value Per Share == 2620",
"requires_candidate_name_binding": false,
"notes": []
}
],
"negative_sentinel_scope": {
"company_test_scope": "selected_company_only",
"player_test_scope": "disabled",
"territory_scope_selector_is_0x63": false,
"source_row_indexes": [
0
]
},
"grouped_effect_row_counts": [
1,
0,
0,
0
],
"grouped_effect_rows": [
{
"group_index": 0,
"row_index": 0,
"descriptor_id": 110,
"descriptor_label": "Disable Stock Buying and Selling",
"target_mask_bits": 8,
"parameter_family": "world_flag_toggle",
"opcode": 0,
"raw_scalar_value": 1,
"value_byte_0x09": 0,
"value_dword_0x0d": 0,
"value_byte_0x11": 0,
"value_byte_0x12": 0,
"value_word_0x14": 0,
"value_word_0x16": 0,
"row_shape": "bool_toggle",
"semantic_family": "bool_toggle",
"semantic_preview": "Set Disable Stock Buying and Selling to TRUE",
"locomotive_name": null,
"notes": []
}
],
"decoded_conditions": [
{
"kind": "company_numeric_threshold",
"target": {
"kind": "condition_true_company"
},
"metric": "book_value_per_share",
"comparator": "eq",
"value": 2620
}
],
"decoded_actions": [
{
"kind": "set_world_flag",
"key": "world.book_value_condition_imported",
"value": true
}
],
"executable_import_ready": true,
"notes": [
"book value per share condition gates a world-side effect"
]
}
]
},
"notes": [
"real company-governance ordinary-condition sample"
]
}
}