Recover real packed event descriptor semantics
This commit is contained in:
parent
f918d0c4f7
commit
eb6c4833af
9 changed files with 719 additions and 120 deletions
|
|
@ -6,10 +6,9 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
|
||||
use crate::{
|
||||
CalendarPoint, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect,
|
||||
RuntimeEventRecord, RuntimeEventRecordTemplate,
|
||||
RuntimePackedEventCompactControlSummary,
|
||||
RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary,
|
||||
RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary,
|
||||
RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary,
|
||||
RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary,
|
||||
RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary,
|
||||
RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState,
|
||||
RuntimeWorldRestoreState, SmpLoadedPackedEventRecordSummary,
|
||||
SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice,
|
||||
|
|
@ -133,10 +132,9 @@ impl ImportCompanyContext {
|
|||
.collect(),
|
||||
selected_company_id: state.selected_company_id,
|
||||
has_complete_controller_context: !state.companies.is_empty()
|
||||
&& state
|
||||
.companies
|
||||
.iter()
|
||||
.all(|company| company.controller_kind != RuntimeCompanyControllerKind::Unknown),
|
||||
&& state.companies.iter().all(|company| {
|
||||
company.controller_kind != RuntimeCompanyControllerKind::Unknown
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -546,7 +544,9 @@ fn project_packed_event_collection(
|
|||
let mut imported_runtime_records = Vec::new();
|
||||
let mut imported_record_ids = BTreeSet::new();
|
||||
for record in &summary.records {
|
||||
if let Some(import_result) = smp_packed_record_to_runtime_event_record(record, company_context) {
|
||||
if let Some(import_result) =
|
||||
smp_packed_record_to_runtime_event_record(record, company_context)
|
||||
{
|
||||
let runtime_record = import_result?;
|
||||
imported_record_ids.insert(record.live_entry_id);
|
||||
imported_runtime_records.push(runtime_record);
|
||||
|
|
@ -684,6 +684,9 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
|
|||
group_index: row.group_index,
|
||||
row_index: row.row_index,
|
||||
descriptor_id: row.descriptor_id,
|
||||
descriptor_label: row.descriptor_label.clone(),
|
||||
target_mask_bits: row.target_mask_bits,
|
||||
parameter_family: row.parameter_family.clone(),
|
||||
opcode: row.opcode,
|
||||
raw_scalar_value: row.raw_scalar_value,
|
||||
value_byte_0x09: row.value_byte_0x09,
|
||||
|
|
@ -693,6 +696,8 @@ fn runtime_packed_event_grouped_effect_row_summary_from_smp(
|
|||
value_word_0x14: row.value_word_0x14,
|
||||
value_word_0x16: row.value_word_0x16,
|
||||
row_shape: row.row_shape.clone(),
|
||||
semantic_family: row.semantic_family.clone(),
|
||||
semantic_preview: row.semantic_preview.clone(),
|
||||
locomotive_name: row.locomotive_name.clone(),
|
||||
notes: row.notes.clone(),
|
||||
}
|
||||
|
|
@ -702,14 +707,18 @@ fn smp_packed_record_to_runtime_event_record(
|
|||
record: &SmpLoadedPackedEventRecordSummary,
|
||||
company_context: &ImportCompanyContext,
|
||||
) -> Option<Result<RuntimeEventRecord, String>> {
|
||||
if record.decode_status == "unsupported_framing" || record.payload_family == "real_packed_v1" {
|
||||
if record.decode_status == "unsupported_framing" {
|
||||
return None;
|
||||
}
|
||||
if record.payload_family == "real_packed_v1" && record.decoded_actions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let effects = match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
|
||||
Ok(effects) => effects,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let effects =
|
||||
match smp_runtime_effects_to_runtime_effects(&record.decoded_actions, company_context) {
|
||||
Ok(effects) => effects,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some((|| {
|
||||
let trigger_kind = record.trigger_kind.ok_or_else(|| {
|
||||
|
|
@ -718,24 +727,9 @@ fn smp_packed_record_to_runtime_event_record(
|
|||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let active = record.active.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing active flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let marks_collection_dirty = record.marks_collection_dirty.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing dirty flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let one_shot = record.one_shot.ok_or_else(|| {
|
||||
format!(
|
||||
"packed event record {} is missing one_shot flag",
|
||||
record.live_entry_id
|
||||
)
|
||||
})?;
|
||||
let active = record.active.unwrap_or(true);
|
||||
let marks_collection_dirty = record.marks_collection_dirty.unwrap_or(false);
|
||||
let one_shot = record.one_shot.unwrap_or(false);
|
||||
Ok(RuntimeEventRecordTemplate {
|
||||
record_id: record.live_entry_id,
|
||||
trigger_kind,
|
||||
|
|
@ -767,6 +761,16 @@ fn smp_runtime_effect_to_runtime_effect(
|
|||
key: key.clone(),
|
||||
value: *value,
|
||||
}),
|
||||
RuntimeEffect::SetCompanyCash { target, value } => {
|
||||
if company_target_import_blocker(target, company_context).is_none() {
|
||||
Ok(RuntimeEffect::SetCompanyCash {
|
||||
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 {
|
||||
|
|
@ -908,6 +912,13 @@ fn determine_packed_event_import_outcome(
|
|||
if record.compact_control.is_none() {
|
||||
return "blocked_missing_compact_control".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();
|
||||
}
|
||||
|
|
@ -934,14 +945,14 @@ fn runtime_effect_company_target_import_blocker(
|
|||
company_context: &ImportCompanyContext,
|
||||
) -> Option<CompanyTargetImportBlocker> {
|
||||
match effect {
|
||||
RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
RuntimeEffect::SetCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyCash { target, .. }
|
||||
| RuntimeEffect::AdjustCompanyDebt { target, .. } => {
|
||||
company_target_import_blocker(target, company_context)
|
||||
}
|
||||
RuntimeEffect::AppendEventRecord { record } => record
|
||||
.effects
|
||||
.iter()
|
||||
.find_map(|nested| runtime_effect_company_target_import_blocker(nested, company_context)),
|
||||
RuntimeEffect::AppendEventRecord { record } => record.effects.iter().find_map(|nested| {
|
||||
runtime_effect_company_target_import_blocker(nested, company_context)
|
||||
}),
|
||||
RuntimeEffect::SetWorldFlag { .. }
|
||||
| RuntimeEffect::SetCandidateAvailability { .. }
|
||||
| RuntimeEffect::SetSpecialCondition { .. }
|
||||
|
|
@ -998,15 +1009,11 @@ fn real_record_company_target_import_blocker(
|
|||
fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str {
|
||||
match blocker {
|
||||
CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context",
|
||||
CompanyTargetImportBlocker::MissingSelectionContext => {
|
||||
"blocked_missing_selection_context"
|
||||
}
|
||||
CompanyTargetImportBlocker::MissingSelectionContext => "blocked_missing_selection_context",
|
||||
CompanyTargetImportBlocker::MissingCompanyRoleContext => {
|
||||
"blocked_missing_company_role_context"
|
||||
}
|
||||
CompanyTargetImportBlocker::MissingConditionContext => {
|
||||
"blocked_missing_condition_context"
|
||||
}
|
||||
CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1391,6 +1398,9 @@ mod tests {
|
|||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id: 2,
|
||||
descriptor_label: Some("Company Cash".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_finance_scalar".to_string()),
|
||||
opcode: 8,
|
||||
raw_scalar_value: 7,
|
||||
value_byte_0x09: 1,
|
||||
|
|
@ -1400,6 +1410,8 @@ mod tests {
|
|||
value_word_0x14: 24,
|
||||
value_word_0x16: 36,
|
||||
row_shape: "multivalue_scalar".to_string(),
|
||||
semantic_family: Some("multivalue_scalar".to_string()),
|
||||
semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()),
|
||||
locomotive_name: Some("Mikado".to_string()),
|
||||
notes: vec!["grouped effect row carries locomotive-name side string".to_string()],
|
||||
}]
|
||||
|
|
@ -1420,8 +1432,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn real_compact_control_without_symbolic_company_scope(
|
||||
) -> crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
fn real_compact_control_without_symbolic_company_scope()
|
||||
-> crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
crate::SmpLoadedPackedEventCompactControlSummary {
|
||||
mode_byte_0x7ef: 6,
|
||||
primary_selector_0x7f0: 0x63,
|
||||
|
|
@ -2128,12 +2140,9 @@ mod tests {
|
|||
notes: vec![],
|
||||
};
|
||||
|
||||
let import = project_save_slice_to_runtime_state_import(
|
||||
&save_slice,
|
||||
"symbolic-blockers",
|
||||
None,
|
||||
)
|
||||
.expect("standalone projection should succeed");
|
||||
let import =
|
||||
project_save_slice_to_runtime_state_import(&save_slice, "symbolic-blockers", None)
|
||||
.expect("standalone projection should succeed");
|
||||
|
||||
assert!(import.state.event_runtime_records.is_empty());
|
||||
let outcomes = import
|
||||
|
|
@ -2299,7 +2308,10 @@ 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![],
|
||||
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
|
||||
target: RuntimeCompanyTarget::ConditionTrueCompany,
|
||||
value: 7,
|
||||
}],
|
||||
executable_import_ready: false,
|
||||
notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()],
|
||||
}],
|
||||
|
|
@ -2342,7 +2354,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context() {
|
||||
fn leaves_real_records_with_condition_relative_company_scope_blocked_missing_condition_context()
|
||||
{
|
||||
let save_slice = SmpLoadedSaveSlice {
|
||||
file_extension_hint: Some("gms".to_string()),
|
||||
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
|
||||
|
|
@ -2490,6 +2503,150 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_real_company_cash_descriptor_into_executable_runtime_record() {
|
||||
let base_state = RuntimeState {
|
||||
calendar: CalendarPoint {
|
||||
year: 1845,
|
||||
month_slot: 2,
|
||||
phase_slot: 1,
|
||||
tick_slot: 3,
|
||||
},
|
||||
world_flags: BTreeMap::new(),
|
||||
save_profile: RuntimeSaveProfileState::default(),
|
||||
world_restore: RuntimeWorldRestoreState::default(),
|
||||
metadata: BTreeMap::new(),
|
||||
companies: vec![crate::RuntimeCompany {
|
||||
company_id: 42,
|
||||
controller_kind: RuntimeCompanyControllerKind::Human,
|
||||
current_cash: 500,
|
||||
debt: 20,
|
||||
}],
|
||||
selected_company_id: Some(42),
|
||||
packed_event_collection: None,
|
||||
event_runtime_records: Vec::new(),
|
||||
candidate_availability: 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,
|
||||
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: 9,
|
||||
live_record_count: 1,
|
||||
live_entry_ids: vec![9],
|
||||
decoded_record_count: 1,
|
||||
imported_runtime_record_count: 0,
|
||||
records: vec![crate::SmpLoadedPackedEventRecordSummary {
|
||||
record_index: 0,
|
||||
live_entry_id: 9,
|
||||
payload_offset: Some(0x7202),
|
||||
payload_len: Some(133),
|
||||
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![crate::SmpLoadedPackedEventGroupedEffectRowSummary {
|
||||
group_index: 0,
|
||||
row_index: 0,
|
||||
descriptor_id: 2,
|
||||
descriptor_label: Some("Company Cash".to_string()),
|
||||
target_mask_bits: Some(0x01),
|
||||
parameter_family: Some("company_finance_scalar".to_string()),
|
||||
opcode: 8,
|
||||
raw_scalar_value: 250,
|
||||
value_byte_0x09: 1,
|
||||
value_dword_0x0d: 12,
|
||||
value_byte_0x11: 2,
|
||||
value_byte_0x12: 3,
|
||||
value_word_0x14: 24,
|
||||
value_word_0x16: 36,
|
||||
row_shape: "multivalue_scalar".to_string(),
|
||||
semantic_family: Some("multivalue_scalar".to_string()),
|
||||
semantic_preview: Some(
|
||||
"Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(),
|
||||
),
|
||||
locomotive_name: Some("Mikado".to_string()),
|
||||
notes: vec![
|
||||
"grouped effect row carries locomotive-name side string".to_string(),
|
||||
],
|
||||
}],
|
||||
decoded_actions: vec![RuntimeEffect::SetCompanyCash {
|
||||
target: RuntimeCompanyTarget::SelectedCompany,
|
||||
value: 250,
|
||||
}],
|
||||
executable_import_ready: false,
|
||||
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(),
|
||||
],
|
||||
}],
|
||||
}),
|
||||
notes: vec![],
|
||||
};
|
||||
|
||||
let mut import = project_save_slice_overlay_to_runtime_state_import(
|
||||
&base_state,
|
||||
&save_slice,
|
||||
"real-company-cash-overlay",
|
||||
None,
|
||||
)
|
||||
.expect("overlay import should project");
|
||||
|
||||
assert_eq!(import.state.event_runtime_records.len(), 1);
|
||||
assert_eq!(
|
||||
import
|
||||
.state
|
||||
.packed_event_collection
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.records[0].import_outcome.as_deref()),
|
||||
Some("imported")
|
||||
);
|
||||
|
||||
execute_step_command(
|
||||
&mut import.state,
|
||||
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
|
||||
)
|
||||
.expect("real company-cash descriptor should execute through the normal trigger path");
|
||||
|
||||
assert_eq!(import.state.companies[0].current_cash, 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_save_slice_events_onto_base_company_context() {
|
||||
let base_state = RuntimeState {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue