Execute real packed event player deactivation

This commit is contained in:
Jan Petykiewicz 2026-04-15 23:24:08 -07:00
commit 991725dba8
10 changed files with 670 additions and 47 deletions

View file

@ -128,7 +128,7 @@ struct RealGroupedEffectDescriptorMetadata {
executable_in_runtime: bool,
}
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 11] = [
const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 12] = [
RealGroupedEffectDescriptorMetadata {
descriptor_id: 1,
label: "Player Cash",
@ -201,6 +201,14 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad
runtime_key: None,
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 14,
label: "Deactivate Player",
target_mask_bits: 0x02,
parameter_family: "player_lifecycle_toggle",
runtime_key: None,
executable_in_runtime: true,
},
RealGroupedEffectDescriptorMetadata {
descriptor_id: 15,
label: "Retire Train",
@ -2378,8 +2386,8 @@ fn parse_real_condition_row_summary(
let comparator = ordinary_metadata
.and_then(|_| decode_real_condition_comparator(subtype))
.map(condition_comparator_label);
let metric =
ordinary_metadata.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref));
let metric = ordinary_metadata
.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref));
let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes));
let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| {
matches!(
@ -2409,7 +2417,10 @@ fn parse_real_condition_row_summary(
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability)
) && candidate_name.is_none()
}) {
notes.push("candidate-availability condition row is missing its candidate-name side string".to_string());
notes.push(
"candidate-availability condition row is missing its candidate-name side string"
.to_string(),
);
}
Some(SmpLoadedPackedEventConditionRowSummary {
row_index,
@ -2507,7 +2518,9 @@ fn real_ordinary_condition_metric_label(
) -> String {
match metadata.kind {
RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { label }) => {
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
label,
}) => {
format!("Special Condition: {label}")
}
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => {
@ -2522,7 +2535,9 @@ fn real_ordinary_condition_metric_label(
}
}
fn real_ordinary_condition_semantic_family(metadata: RealOrdinaryConditionMetadata) -> &'static str {
fn real_ordinary_condition_semantic_family(
metadata: RealOrdinaryConditionMetadata,
) -> &'static str {
match metadata.kind {
RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold",
RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold",
@ -2685,16 +2700,17 @@ fn decode_real_condition_row(
}
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => {
negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
target: RuntimeTerritoryTarget::AllTerritories,
metric,
comparator,
value,
})
.filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::TerritoryNumericThreshold {
target: RuntimeTerritoryTarget::AllTerritories,
metric,
comparator,
value,
})
}
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(metric)) => {
negative_sentinel_scope
RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(
metric,
)) => negative_sentinel_scope
.filter(|scope| scope.territory_scope_selector_is_0x63)
.map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold {
target: RuntimeCompanyTarget::ConditionTrueCompany,
@ -2702,8 +2718,7 @@ fn decode_real_condition_row(
metric,
comparator,
value,
})
}
}),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition {
label,
}) => Some(RuntimeCondition::SpecialConditionThreshold {
@ -2711,15 +2726,14 @@ fn decode_real_condition_row(
comparator,
value,
}),
RealOrdinaryConditionKind::WorldState(
RealWorldConditionKind::CandidateAvailability,
) => row.candidate_name.as_ref().map(|name| {
RuntimeCondition::CandidateAvailabilityThreshold {
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => row
.candidate_name
.as_ref()
.map(|name| RuntimeCondition::CandidateAvailabilityThreshold {
name: name.clone(),
comparator,
value,
}
}),
}),
RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => {
Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value })
}
@ -2813,7 +2827,9 @@ fn runtime_candidate_availability_name(label: &str) -> String {
.to_string()
}
fn runtime_world_flag_key(descriptor_metadata: RealGroupedEffectDescriptorMetadata) -> Option<String> {
fn runtime_world_flag_key(
descriptor_metadata: RealGroupedEffectDescriptorMetadata,
) -> Option<String> {
descriptor_metadata.runtime_key.map(str::to_string)
}
@ -2939,6 +2955,15 @@ fn decode_real_grouped_effect_action(
return Some(RuntimeEffect::DeactivateCompany { target });
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 14
&& row.row_shape == "bool_toggle"
&& row.raw_scalar_value != 0
{
let target = real_grouped_player_target(target_scope_ordinal)?;
return Some(RuntimeEffect::DeactivatePlayer { target });
}
if descriptor_metadata.executable_in_runtime
&& descriptor_metadata.descriptor_id == 16
&& row.row_shape == "scalar_assignment"
@ -3151,6 +3176,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool {
| RuntimeEffect::SetSpecialCondition { .. }
| RuntimeEffect::ConfiscateCompanyAssets { .. }
| RuntimeEffect::DeactivateCompany { .. }
| RuntimeEffect::DeactivatePlayer { .. }
| RuntimeEffect::SetCompanyTrackLayingCapacity { .. }
| RuntimeEffect::RetireTrains { .. }
| RuntimeEffect::ActivateEventRecord { .. }
@ -8715,8 +8741,12 @@ mod tests {
#[test]
fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() {
let condition_row =
build_real_condition_row_with_threshold(REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, 0, 2, Some("Mogul"));
let condition_row = build_real_condition_row_with_threshold(
REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID,
0,
2,
Some("Mogul"),
);
let record_body = build_real_event_record(
[b"World", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
@ -8839,6 +8869,90 @@ mod tests {
assert!(metadata.executable_in_runtime);
}
#[test]
fn looks_up_checked_in_deactivate_player_descriptor_metadata() {
let metadata =
real_grouped_effect_descriptor_metadata(14).expect("descriptor metadata should exist");
assert_eq!(metadata.label, "Deactivate Player");
assert_eq!(metadata.parameter_family, "player_lifecycle_toggle");
assert_eq!(metadata.runtime_key, None);
assert!(metadata.executable_in_runtime);
}
#[test]
fn decodes_real_deactivate_player_descriptor_from_checked_in_metadata() {
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {
descriptor_id: 14,
opcode: 1,
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,
locomotive_name: None,
});
let group0_rows = vec![grouped_row];
let record_body = build_real_event_record(
[b"Players", b"", b"", b"", b"", b""],
Some(RealCompactControlSpec {
mode_byte_0x7ef: 7,
primary_selector_0x7f0: 0,
grouped_mode_0x7f4: 2,
one_shot_header_0x7f5: 0,
modifier_flag_0x7f9: 0,
modifier_flag_0x7fa: 0,
grouped_target_scope_ordinals_0x7fb: [1, 0, 0, 0],
grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0],
summary_toggle_0x800: 1,
grouped_territory_selectors_0x80f: [-1, -1, -1, -1],
}),
&[],
[&group0_rows, &[], &[], &[]],
);
let mut bytes = Vec::new();
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes());
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes());
let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for word in header_words {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(&[0x00, 0x00]);
bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes());
bytes.extend_from_slice(&record_body);
bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes());
let report = inspect_smp_bytes(&bytes);
let summary = report
.event_runtime_collection_summary
.as_ref()
.expect("event runtime collection summary should parse");
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.descriptor_label
.as_deref(),
Some("Deactivate Player")
);
assert_eq!(
summary.records[0].grouped_effect_rows[0]
.parameter_family
.as_deref(),
Some("player_lifecycle_toggle")
);
assert_eq!(
summary.records[0].decoded_actions,
vec![RuntimeEffect::DeactivatePlayer {
target: RuntimePlayerTarget::SelectedPlayer,
}]
);
assert!(summary.records[0].executable_import_ready);
}
#[test]
fn decodes_real_world_flag_descriptor_with_checked_in_runtime_key() {
let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec {