Implement real company-scoped event descriptors
This commit is contained in:
parent
eb6c4833af
commit
780e739daa
21 changed files with 1483 additions and 56 deletions
|
|
@ -710,7 +710,7 @@ fn smp_packed_record_to_runtime_event_record(
|
|||
if record.decode_status == "unsupported_framing" {
|
||||
return None;
|
||||
}
|
||||
if record.payload_family == "real_packed_v1" && record.decoded_actions.is_empty() {
|
||||
if record.payload_family == "real_packed_v1" && !record.executable_import_ready {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -771,6 +771,25 @@ fn smp_runtime_effect_to_runtime_effect(
|
|||
Err(company_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivateCompany { target } => {
|
||||
if company_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::DeactivateCompany {
|
||||
target: target.clone(),
|
||||
})
|
||||
} else {
|
||||
Err(company_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
|
||||
if company_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::SetCompanyTrackLayingCapacity {
|
||||
target: target.clone(),
|
||||
value: *value,
|
||||
})
|
||||
} else {
|
||||
Err(company_target_import_error_message(target, company_context))
|
||||
}
|
||||
}
|
||||
RuntimeEffect::AdjustCompanyCash { target, delta } => {
|
||||
if company_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::AdjustCompanyCash {
|
||||
|
|
@ -912,17 +931,14 @@ fn determine_packed_event_import_outcome(
|
|||
if record.compact_control.is_none() {
|
||||
return "blocked_missing_compact_control".to_string();
|
||||
}
|
||||
if !record.executable_import_ready {
|
||||
return "blocked_unmapped_real_descriptor".to_string();
|
||||
}
|
||||
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context)
|
||||
{
|
||||
return company_target_import_outcome(blocker).to_string();
|
||||
}
|
||||
if !record.decoded_actions.is_empty() {
|
||||
return "blocked_unsupported_decode".to_string();
|
||||
}
|
||||
if let Some(blocker) = real_record_company_target_import_blocker(record, company_context) {
|
||||
return company_target_import_outcome(blocker).to_string();
|
||||
}
|
||||
return "blocked_unmapped_real_descriptor".to_string();
|
||||
return "blocked_unsupported_decode".to_string();
|
||||
}
|
||||
if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) {
|
||||
return company_target_import_outcome(blocker).to_string();
|
||||
|
|
@ -946,6 +962,8 @@ fn runtime_effect_company_target_import_blocker(
|
|||
) -> Option<CompanyTargetImportBlocker> {
|
||||
match effect {
|
||||
RuntimeEffect::SetCompanyCash { target, .. }
|
||||
| RuntimeEffect::DeactivateCompany { target }
|
||||
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||
company_target_import_blocker(target, company_context)
|
||||
|
|
@ -996,16 +1014,6 @@ fn classify_real_grouped_company_target(ordinal: u8) -> Option<RuntimeCompanyTar
|
|||
}
|
||||
}
|
||||
|
||||
fn real_record_company_target_import_blocker(
|
||||
record: &SmpLoadedPackedEventRecordSummary,
|
||||
company_context: &ImportCompanyContext,
|
||||
) -> Option<CompanyTargetImportBlocker> {
|
||||
classify_real_grouped_company_targets(record)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find_map(|target| company_target_import_blocker(&target, company_context))
|
||||
}
|
||||
|
||||
fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str {
|
||||
match blocker {
|
||||
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
|
||||
|
|
@ -1417,6 +1425,83 @@ mod tests {
|
|||
}]
|
||||
}
|
||||
|
||||
fn real_deactivate_company_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id: 13,
|
||||
descriptor_label: Some("Deactivate Company".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_lifecycle_toggle".to_string()),
|
||||
opcode: 1,
|
||||
raw_scalar_value: if enabled { 1 } else { 0 },
|
||||
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".to_string(),
|
||||
semantic_family: Some("bool_toggle".to_string()),
|
||||
semantic_preview: Some(format!(
|
||||
"Set Deactivate Company to {}",
|
||||
if enabled { "TRUE" } else { "FALSE" }
|
||||
)),
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn real_track_capacity_row(value: i32) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id: 16,
|
||||
descriptor_label: Some("Company Track Pieces Buildable".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_build_limit_scalar".to_string()),
|
||||
opcode: 3,
|
||||
raw_scalar_value: value,
|
||||
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: "scalar_assignment".to_string(),
|
||||
semantic_family: Some("scalar_assignment".to_string()),
|
||||
semantic_preview: Some(format!(
|
||||
"Set Company Track Pieces Buildable to {value}"
|
||||
)),
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_real_grouped_row() -> crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 1,
|
||||
row_index: 0,
|
||||
descriptor_id: 8,
|
||||
descriptor_label: Some("Economic Status".to_string()),
|
||||
target_mask_bits: Some(0x08),
|
||||
parameter_family: Some("whole_game_state_enum".to_string()),
|
||||
opcode: 3,
|
||||
raw_scalar_value: 2,
|
||||
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: "scalar_assignment".to_string(),
|
||||
semantic_family: Some("scalar_assignment".to_string()),
|
||||
semantic_preview: Some("Set Economic Status to 2".to_string()),
|
||||
locomotive_name: None,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 6,
|
||||
|
|
@ -2173,12 +2258,16 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 100,
|
||||
debt: 10,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
crate::RuntimeCompany {
|
||||
company_id: 2,
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
current_cash: 50,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
],
|
||||
selected_company_id: Some(1),
|
||||
|
|
@ -2312,7 +2401,7 @@ mod tests {
|
|||
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
||||
value: 7,
|
||||
}],
|
||||
executable_import_ready: false,
|
||||
executable_import_ready: true,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
|
|
@ -2398,8 +2487,11 @@ mod tests {
|
|||
standalone_condition_rows: real_condition_rows(),
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: real_grouped_rows(),
|
||||
decoded_actions: vec![],
|
||||
executable_import_ready: false,
|
||||
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
|
||||
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
||||
value: 7,
|
||||
}],
|
||||
executable_import_ready: true,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
|
|
@ -2521,6 +2613,8 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
packed_event_collection: None,
|
||||
|
|
@ -2610,7 +2704,7 @@ mod tests {
|
|||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
value: 250,
|
||||
}],
|
||||
executable_import_ready: false,
|
||||
executable_import_ready: true,
|
||||
notes: vec![
|
||||
"decoded from grounded real 0x4e9a row framing".to_string(),
|
||||
"grouped descriptor labels and target masks come from the checked-in effect table recovered around 0x006103a0".to_string(),
|
||||
|
|
@ -2647,6 +2741,381 @@ mod tests {
|
|||
assert_eq!(import.state.companies[0].current_cash, 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_real_deactivate_company_descriptor_into_executable_runtime_record() {
|
||||
let base_state = RuntimeState {
|
||||
companies: vec![crate::RuntimeCompany {
|
||||
company_id: 42,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
..state()
|
||||
};
|
||||
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,
|
||||
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: 13,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![13],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 13,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(120),
|
||||
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(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 1,
|
||||
modifier_flag_0x7fa: 0,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 0,
|
||||
standalone_condition_rows: vec![],
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_deactivate_company_row(true)],
|
||||
decoded_actions: vec![RuntimeEffect::DeactivateCompany {
|
||||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
}],
|
||||
executable_import_ready: true,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let mut import = project_save_slice_overlay_to_runtime_state_import(
|
||||
&base_state,
|
||||
&save_slice,
|
||||
"real-deactivate-company-overlay",
|
||||
None,
|
||||
)
|
||||
.expect("overlay import should project");
|
||||
|
||||
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||
execute_step_command(
|
||||
&mut import.state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("real deactivate-company descriptor should execute");
|
||||
|
||||
assert!(!import.state.companies[0].active);
|
||||
assert_eq!(import.state.selected_company_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_real_deactivate_company_false_row_parity_only() {
|
||||
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,
|
||||
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: 14,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![14],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 14,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(120),
|
||||
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(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 1,
|
||||
modifier_flag_0x7fa: 0,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 0,
|
||||
standalone_condition_rows: vec![],
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_deactivate_company_row(false)],
|
||||
decoded_actions: vec![],
|
||||
executable_import_ready: false,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let import = project_save_slice_to_runtime_state_import(
|
||||
&save_slice,
|
||||
"real-deactivate-company-false",
|
||||
None,
|
||||
)
|
||||
.expect("save slice should project");
|
||||
|
||||
assert!(import.state.event_runtime_records.is_empty());
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||
Some("blocked_unmapped_real_descriptor")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_real_track_capacity_descriptor_into_executable_runtime_record() {
|
||||
let base_state = RuntimeState {
|
||||
companies: vec![crate::RuntimeCompany {
|
||||
company_id: 42,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
..state()
|
||||
};
|
||||
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,
|
||||
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: 16,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![16],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 16,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(120),
|
||||
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(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 1,
|
||||
modifier_flag_0x7fa: 0,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 0, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 0,
|
||||
standalone_condition_rows: vec![],
|
||||
grouped_effect_row_counts: vec![1, 0, 0, 0],
|
||||
grouped_effect_rows: vec![real_track_capacity_row(18)],
|
||||
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
|
||||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
value: Some(18),
|
||||
}],
|
||||
executable_import_ready: true,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let mut import = project_save_slice_overlay_to_runtime_state_import(
|
||||
&base_state,
|
||||
&save_slice,
|
||||
"real-track-capacity-overlay",
|
||||
None,
|
||||
)
|
||||
.expect("overlay import should project");
|
||||
|
||||
execute_step_command(
|
||||
&mut import.state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("real track-capacity descriptor should execute");
|
||||
|
||||
assert_eq!(
|
||||
import.state.companies[0].available_track_laying_capacity,
|
||||
Some(18)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_mixed_real_records_out_of_event_runtime_records() {
|
||||
let base_state = RuntimeState {
|
||||
companies: vec![crate::RuntimeCompany {
|
||||
company_id: 42,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
..state()
|
||||
};
|
||||
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,
|
||||
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: 17,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![17],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 17,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(160),
|
||||
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(crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 7,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
grouped_mode_0x7f4: 2,
|
||||
one_shot_header_0x7f5: 0,
|
||||
modifier_flag_0x7f9: 1,
|
||||
modifier_flag_0x7fa: 0,
|
||||
grouped_target_scope_ordinals_0x7fb: vec![1, 1, 1, 1],
|
||||
grouped_scope_checkboxes_0x7ff: vec![1, 1, 0, 0],
|
||||
summary_toggle_0x800: 1,
|
||||
grouped_territory_selectors_0x80f: vec![-1, -1, -1, -1],
|
||||
}),
|
||||
text_bands: packed_text_bands(),
|
||||
standalone_condition_row_count: 0,
|
||||
standalone_condition_rows: vec![],
|
||||
grouped_effect_row_counts: vec![1, 1, 0, 0],
|
||||
grouped_effect_rows: vec![
|
||||
real_track_capacity_row(18),
|
||||
unsupported_real_grouped_row(),
|
||||
],
|
||||
decoded_actions: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
|
||||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
value: Some(18),
|
||||
}],
|
||||
executable_import_ready: false,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let import = project_save_slice_overlay_to_runtime_state_import(
|
||||
&base_state,
|
||||
&save_slice,
|
||||
"mixed-real-record-overlay",
|
||||
None,
|
||||
)
|
||||
.expect("overlay import should project");
|
||||
|
||||
assert!(import.state.event_runtime_records.is_empty());
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||
Some("blocked_unmapped_real_descriptor")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_save_slice_events_onto_base_company_context() {
|
||||
let base_state = RuntimeState {
|
||||
|
|
@ -2665,6 +3134,8 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
packed_event_collection: None,
|
||||
|
|
@ -2827,6 +3298,8 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 100,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
packed_event_collection: None,
|
||||
|
|
|
|||
|
|
@ -13,11 +13,19 @@ pub enum RuntimeCompanyControllerKind {
|
|||
Ai,
|
||||
}
|
||||
|
||||
fn runtime_company_default_active() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeCompany {
|
||||
pub company_id: u32,
|
||||
pub current_cash: i64,
|
||||
pub debt: u64,
|
||||
#[serde(default = "runtime_company_default_active")]
|
||||
pub active: bool,
|
||||
#[serde(default)]
|
||||
pub available_track_laying_capacity: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub controller_kind: RuntimeCompanyControllerKind,
|
||||
}
|
||||
|
|
@ -44,6 +52,13 @@ pub enum RuntimeEffect {
|
|||
target: RuntimeCompanyTarget,
|
||||
value: i64,
|
||||
},
|
||||
DeactivateCompany {
|
||||
target: RuntimeCompanyTarget,
|
||||
},
|
||||
SetCompanyTrackLayingCapacity {
|
||||
target: RuntimeCompanyTarget,
|
||||
value: Option<u32>,
|
||||
},
|
||||
AdjustCompanyCash {
|
||||
target: RuntimeCompanyTarget,
|
||||
delta: i64,
|
||||
|
|
@ -352,10 +367,14 @@ impl RuntimeState {
|
|||
self.calendar.validate()?;
|
||||
|
||||
let mut seen_company_ids = BTreeSet::new();
|
||||
let mut active_company_ids = BTreeSet::new();
|
||||
for company in &self.companies {
|
||||
if !seen_company_ids.insert(company.company_id) {
|
||||
return Err(format!("duplicate company_id {}", company.company_id));
|
||||
}
|
||||
if company.active {
|
||||
active_company_ids.insert(company.company_id);
|
||||
}
|
||||
}
|
||||
if let Some(selected_company_id) = self.selected_company_id {
|
||||
if !seen_company_ids.contains(&selected_company_id) {
|
||||
|
|
@ -364,6 +383,12 @@ impl RuntimeState {
|
|||
selected_company_id
|
||||
));
|
||||
}
|
||||
if !active_company_ids.contains(&selected_company_id) {
|
||||
return Err(format!(
|
||||
"selected_company_id {} must reference an active company",
|
||||
selected_company_id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_record_ids = BTreeSet::new();
|
||||
|
|
@ -669,6 +694,8 @@ fn validate_runtime_effect(
|
|||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyCash { target, .. }
|
||||
| RuntimeEffect::DeactivateCompany { target }
|
||||
| RuntimeEffect::SetCompanyTrackLayingCapacity { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||
validate_company_target(target, valid_company_ids)?;
|
||||
|
|
@ -756,12 +783,16 @@ mod tests {
|
|||
company_id: 1,
|
||||
current_cash: 100,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 1,
|
||||
current_cash: 200,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
},
|
||||
],
|
||||
|
|
@ -840,6 +871,8 @@ mod tests {
|
|||
company_id: 1,
|
||||
current_cash: 100,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
}],
|
||||
selected_company_id: None,
|
||||
|
|
@ -882,6 +915,8 @@ mod tests {
|
|||
company_id: 1,
|
||||
current_cash: 100,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
}],
|
||||
selected_company_id: None,
|
||||
|
|
@ -1018,6 +1053,8 @@ mod tests {
|
|||
company_id: 1,
|
||||
current_cash: 100,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
}],
|
||||
selected_company_id: Some(2),
|
||||
|
|
@ -1030,4 +1067,36 @@ mod tests {
|
|||
|
||||
assert!(state.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_selected_company_id_that_is_inactive() {
|
||||
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,
|
||||
active: false,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
}],
|
||||
selected_company_id: Some(1),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
service_state: RuntimeServiceState::default(),
|
||||
};
|
||||
|
||||
assert!(state.validate().is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
|
|||
label: "Deactivate Company",
|
||||
target_mask_bits: 0x01,
|
||||
parameter_family: "company_lifecycle_toggle",
|
||||
executable_in_runtime: false,
|
||||
executable_in_runtime: true,
|
||||
},
|
||||
RealGroupedEffectDescriptorMetadata {
|
||||
descriptor_id: 15,
|
||||
|
|
@ -177,7 +177,7 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
|
|||
label: "Company Track Pieces Buildable",
|
||||
target_mask_bits: 0x01,
|
||||
parameter_family: "company_build_limit_scalar",
|
||||
executable_in_runtime: false,
|
||||
executable_in_runtime: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -1944,7 +1944,8 @@ fn parse_real_event_runtime_record_summary(
|
|||
.as_ref()
|
||||
.map(|control| decode_real_grouped_effect_actions(&grouped_effect_rows, control))
|
||||
.unwrap_or_default();
|
||||
let executable_import_ready = !decoded_actions.is_empty()
|
||||
let executable_import_ready = !grouped_effect_rows.is_empty()
|
||||
&& decoded_actions.len() == grouped_effect_rows.len()
|
||||
&& decoded_actions
|
||||
.iter()
|
||||
.all(runtime_effect_supported_for_save_import);
|
||||
|
|
@ -2265,6 +2266,25 @@ fn decode_real_grouped_effect_action(
|
|||
});
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
&& descriptor_metadata.descriptor_id == 13
|
||||
&& row.row_shape == "bool_toggle"
|
||||
&& row.raw_scalar_value != 0
|
||||
{
|
||||
return Some(RuntimeEffect::DeactivateCompany { target });
|
||||
}
|
||||
|
||||
if descriptor_metadata.executable_in_runtime
|
||||
&& descriptor_metadata.descriptor_id == 16
|
||||
&& row.row_shape == "scalar_assignment"
|
||||
&& row.raw_scalar_value >= 0
|
||||
{
|
||||
return Some(RuntimeEffect::SetCompanyTrackLayingCapacity {
|
||||
target,
|
||||
value: Some(row.raw_scalar_value as u32),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -2417,14 +2437,22 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
|
|||
RuntimeEffect::SetWorldFlag { .. }
|
||||
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||
| RuntimeEffect::SetSpecialCondition { .. }
|
||||
| RuntimeEffect::DeactivateCompany { .. }
|
||||
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
|
||||
| RuntimeEffect::ActivateEventRecord { .. }
|
||||
| RuntimeEffect::DeactivateEventRecord { .. }
|
||||
| RuntimeEffect::RemoveEventRecord { .. } => true,
|
||||
RuntimeEffect::SetCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||
matches!(target, RuntimeCompanyTarget::AllActive)
|
||||
}
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => matches!(
|
||||
target,
|
||||
RuntimeCompanyTarget::AllActive
|
||||
| RuntimeCompanyTarget::Ids { .. }
|
||||
| RuntimeCompanyTarget::HumanCompanies
|
||||
| RuntimeCompanyTarget::AiCompanies
|
||||
| RuntimeCompanyTarget::SelectedCompany
|
||||
| RuntimeCompanyTarget::ConditionTrueCompany
|
||||
),
|
||||
RuntimeEffect::AppendEventRecord { record } => record
|
||||
.effects
|
||||
.iter()
|
||||
|
|
@ -7513,10 +7541,10 @@ mod tests {
|
|||
.expect("event runtime collection summary should parse");
|
||||
|
||||
assert_eq!(summary.decoded_record_count, 1);
|
||||
assert_eq!(summary.imported_runtime_record_count, 0);
|
||||
assert_eq!(summary.records[0].decode_status, "parity_only");
|
||||
assert_eq!(summary.imported_runtime_record_count, 1);
|
||||
assert_eq!(summary.records[0].decode_status, "executable");
|
||||
assert_eq!(summary.records[0].payload_family, "synthetic_harness");
|
||||
assert!(!summary.records[0].executable_import_ready);
|
||||
assert!(summary.records[0].executable_import_ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -299,6 +299,41 @@ fn apply_runtime_effects(
|
|||
mutated_company_ids.insert(company_id);
|
||||
}
|
||||
}
|
||||
RuntimeEffect::DeactivateCompany { target } => {
|
||||
let company_ids = resolve_company_target_ids(state, target)?;
|
||||
for company_id in company_ids {
|
||||
let company = state
|
||||
.companies
|
||||
.iter_mut()
|
||||
.find(|company| company.company_id == company_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"missing company_id {company_id} while applying deactivate effect"
|
||||
)
|
||||
})?;
|
||||
company.active = false;
|
||||
mutated_company_ids.insert(company_id);
|
||||
if state.selected_company_id == Some(company_id) {
|
||||
state.selected_company_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
|
||||
let company_ids = resolve_company_target_ids(state, target)?;
|
||||
for company_id in company_ids {
|
||||
let company = state
|
||||
.companies
|
||||
.iter_mut()
|
||||
.find(|company| company.company_id == company_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"missing company_id {company_id} while applying track capacity effect"
|
||||
)
|
||||
})?;
|
||||
company.available_track_laying_capacity = *value;
|
||||
mutated_company_ids.insert(company_id);
|
||||
}
|
||||
}
|
||||
RuntimeEffect::AdjustCompanyCash { target, delta } => {
|
||||
let company_ids = resolve_company_target_ids(state, target)?;
|
||||
for company_id in company_ids {
|
||||
|
|
@ -429,6 +464,7 @@ fn resolve_company_target_ids(
|
|||
RuntimeCompanyTarget::AllActive => Ok(state
|
||||
.companies
|
||||
.iter()
|
||||
.filter(|company| company.active)
|
||||
.map(|company| company.company_id)
|
||||
.collect()),
|
||||
RuntimeCompanyTarget::Ids { ids } => {
|
||||
|
|
@ -458,7 +494,10 @@ fn resolve_company_target_ids(
|
|||
Ok(state
|
||||
.companies
|
||||
.iter()
|
||||
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human)
|
||||
.filter(|company| {
|
||||
company.active
|
||||
&& company.controller_kind == RuntimeCompanyControllerKind::Human
|
||||
})
|
||||
.map(|company| company.company_id)
|
||||
.collect())
|
||||
}
|
||||
|
|
@ -476,14 +515,27 @@ fn resolve_company_target_ids(
|
|||
Ok(state
|
||||
.companies
|
||||
.iter()
|
||||
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai)
|
||||
.filter(|company| {
|
||||
company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai
|
||||
})
|
||||
.map(|company| company.company_id)
|
||||
.collect())
|
||||
}
|
||||
RuntimeCompanyTarget::SelectedCompany => state
|
||||
.selected_company_id
|
||||
.map(|company_id| vec![company_id])
|
||||
.ok_or_else(|| "target requires selected_company_id context".to_string()),
|
||||
RuntimeCompanyTarget::SelectedCompany => {
|
||||
let selected_company_id = state
|
||||
.selected_company_id
|
||||
.ok_or_else(|| "target requires selected_company_id context".to_string())?;
|
||||
if state
|
||||
.companies
|
||||
.iter()
|
||||
.any(|company| company.company_id == selected_company_id && company.active)
|
||||
{
|
||||
Ok(vec![selected_company_id])
|
||||
} else {
|
||||
Err("target requires selected_company_id to reference an active company"
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
RuntimeCompanyTarget::ConditionTrueCompany => {
|
||||
Err("target requires condition-evaluation context".to_string())
|
||||
}
|
||||
|
|
@ -530,6 +582,8 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
current_cash: 10,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
selected_company_id: None,
|
||||
packed_event_collection: None,
|
||||
|
|
@ -692,12 +746,16 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
current_cash: 10,
|
||||
debt: 5,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 2,
|
||||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
current_cash: 20,
|
||||
debt: 8,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
],
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
|
|
@ -744,12 +802,16 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 10,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 2,
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
current_cash: 20,
|
||||
debt: 2,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
],
|
||||
selected_company_id: Some(1),
|
||||
|
|
@ -866,6 +928,179 @@ mod tests {
|
|||
assert!(error.contains("controller_kind"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_active_and_role_targets_exclude_inactive_companies() {
|
||||
let mut state = RuntimeState {
|
||||
companies: vec![
|
||||
RuntimeCompany {
|
||||
company_id: 1,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 10,
|
||||
debt: 1,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 2,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 20,
|
||||
debt: 2,
|
||||
active: false,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 3,
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
current_cash: 30,
|
||||
debt: 3,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
],
|
||||
event_runtime_records: vec![
|
||||
RuntimeEventRecord {
|
||||
record_id: 16,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
effects: vec![RuntimeEffect::AdjustCompanyCash {
|
||||
target: RuntimeCompanyTarget::AllActive,
|
||||
delta: 5,
|
||||
}],
|
||||
},
|
||||
RuntimeEventRecord {
|
||||
record_id: 17,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
effects: vec![RuntimeEffect::AdjustCompanyDebt {
|
||||
target: RuntimeCompanyTarget::HumanCompanies,
|
||||
delta: 4,
|
||||
}],
|
||||
},
|
||||
RuntimeEventRecord {
|
||||
record_id: 18,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
effects: vec![RuntimeEffect::AdjustCompanyDebt {
|
||||
target: RuntimeCompanyTarget::AiCompanies,
|
||||
delta: 6,
|
||||
}],
|
||||
},
|
||||
],
|
||||
..state()
|
||||
};
|
||||
|
||||
execute_step_command(
|
||||
&mut state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("active-company filtering should succeed");
|
||||
|
||||
assert_eq!(state.companies[0].current_cash, 15);
|
||||
assert_eq!(state.companies[1].current_cash, 20);
|
||||
assert_eq!(state.companies[2].current_cash, 35);
|
||||
assert_eq!(state.companies[0].debt, 5);
|
||||
assert_eq!(state.companies[1].debt, 2);
|
||||
assert_eq!(state.companies[2].debt, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deactivating_selected_company_clears_selection() {
|
||||
let mut state = RuntimeState {
|
||||
companies: vec![RuntimeCompany {
|
||||
company_id: 1,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 10,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: Some(8),
|
||||
}],
|
||||
selected_company_id: Some(1),
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 19,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
effects: vec![RuntimeEffect::DeactivateCompany {
|
||||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
}],
|
||||
}],
|
||||
..state()
|
||||
};
|
||||
|
||||
let result = execute_step_command(
|
||||
&mut state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("deactivate company effect should succeed");
|
||||
|
||||
assert!(!state.companies[0].active);
|
||||
assert_eq!(state.selected_company_id, None);
|
||||
assert_eq!(result.service_events[0].mutated_company_ids, vec![1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_track_laying_capacity_for_resolved_targets() {
|
||||
let mut state = RuntimeState {
|
||||
companies: vec![
|
||||
RuntimeCompany {
|
||||
company_id: 1,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 10,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 2,
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
current_cash: 20,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
},
|
||||
],
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 20,
|
||||
trigger_kind: 7,
|
||||
active: true,
|
||||
service_count: 0,
|
||||
marks_collection_dirty: false,
|
||||
one_shot: false,
|
||||
has_fired: false,
|
||||
effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
|
||||
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
|
||||
value: Some(14),
|
||||
}],
|
||||
}],
|
||||
..state()
|
||||
};
|
||||
|
||||
let result = execute_step_command(
|
||||
&mut state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("track capacity effect should succeed");
|
||||
|
||||
assert_eq!(state.companies[0].available_track_laying_capacity, None);
|
||||
assert_eq!(state.companies[1].available_track_laying_capacity, Some(14));
|
||||
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_condition_true_company_target_without_condition_context() {
|
||||
let mut state = RuntimeState {
|
||||
|
|
@ -941,6 +1176,8 @@ mod tests {
|
|||
controller_kind: RuntimeCompanyControllerKind::Unknown,
|
||||
current_cash: 10,
|
||||
debt: 2,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
}],
|
||||
event_runtime_records: vec![RuntimeEventRecord {
|
||||
record_id: 30,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub struct RuntimeSummary {
|
|||
pub world_restore_absolute_counter_adjustment_context: Option<String>,
|
||||
pub metadata_count: usize,
|
||||
pub company_count: usize,
|
||||
pub active_company_count: usize,
|
||||
pub packed_event_collection_present: bool,
|
||||
pub packed_event_record_count: usize,
|
||||
pub packed_event_decoded_record_count: usize,
|
||||
|
|
@ -122,6 +123,7 @@ impl RuntimeSummary {
|
|||
.clone(),
|
||||
metadata_count: state.metadata.len(),
|
||||
company_count: state.companies.len(),
|
||||
active_company_count: state.companies.iter().filter(|company| company.active).count(),
|
||||
packed_event_collection_present: state.packed_event_collection.is_some(),
|
||||
packed_event_record_count: state
|
||||
.packed_event_collection
|
||||
|
|
@ -302,7 +304,8 @@ mod tests {
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
CalendarPoint, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind,
|
||||
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||
RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState,
|
||||
};
|
||||
|
||||
|
|
@ -399,4 +402,48 @@ mod tests {
|
|||
assert_eq!(summary.packed_event_blocked_missing_company_role_context_count, 0);
|
||||
assert_eq!(summary.packed_event_blocked_missing_condition_context_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_active_companies_separately_from_total_companies() {
|
||||
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: 10,
|
||||
debt: 0,
|
||||
active: true,
|
||||
available_track_laying_capacity: None,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
},
|
||||
RuntimeCompany {
|
||||
company_id: 2,
|
||||
current_cash: 20,
|
||||
debt: 0,
|
||||
active: false,
|
||||
available_track_laying_capacity: Some(7),
|
||||
controller_kind: RuntimeCompanyControllerKind::Ai,
|
||||
},
|
||||
],
|
||||
selected_company_id: None,
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: BTreeMap::new(),
|
||||
special_conditions: BTreeMap::new(),
|
||||
service_state: RuntimeServiceState::default(),
|
||||
};
|
||||
|
||||
let summary = RuntimeSummary::from_state(&state);
|
||||
assert_eq!(summary.company_count, 2);
|
||||
assert_eq!(summary.active_company_count, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue