use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::{ CalendarPoint, RuntimeCompanyConditionTestScope, RuntimeCompanyControllerKind, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventNegativeSentinelScopeSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimePlayerConditionTestScope, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedSaveSlice, }; pub const STATE_DUMP_FORMAT_VERSION: u32 = 1; pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1; pub const OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION: u32 = 1; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeStateDumpSource { #[serde(default)] pub description: Option, #[serde(default)] pub source_binary: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeStateDumpDocument { pub format_version: u32, pub dump_id: String, #[serde(default)] pub source: RuntimeStateDumpSource, pub state: RuntimeState, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeSaveSliceDocumentSource { #[serde(default)] pub description: Option, #[serde(default)] pub original_save_filename: Option, #[serde(default)] pub original_save_sha256: Option, #[serde(default)] pub notes: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeSaveSliceDocument { pub format_version: u32, pub save_slice_id: String, #[serde(default)] pub source: RuntimeSaveSliceDocumentSource, pub save_slice: SmpLoadedSaveSlice, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeOverlayImportDocumentSource { #[serde(default)] pub description: Option, #[serde(default)] pub notes: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeOverlayImportDocument { pub format_version: u32, pub import_id: String, #[serde(default)] pub source: RuntimeOverlayImportDocumentSource, pub base_snapshot_path: String, pub save_slice_path: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeStateImport { pub import_id: String, pub description: Option, pub state: RuntimeState, } #[derive(Debug)] struct SaveSliceProjection { world_flags: BTreeMap, save_profile: RuntimeSaveProfileState, world_restore: RuntimeWorldRestoreState, metadata: BTreeMap, packed_event_collection: Option, event_runtime_records: Vec, candidate_availability: BTreeMap, special_conditions: BTreeMap, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SaveSliceProjectionMode { Standalone, Overlay, } #[derive(Debug, Clone, PartialEq, Eq)] struct ImportCompanyContext { known_company_ids: BTreeSet, selected_company_id: Option, has_complete_controller_context: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum CompanyTargetImportBlocker { MissingCompanyContext, MissingSelectionContext, MissingCompanyRoleContext, MissingConditionContext, CompanyConditionScopeDisabled, PlayerConditionScope, TerritoryConditionScope, } impl ImportCompanyContext { fn standalone() -> Self { Self { known_company_ids: BTreeSet::new(), selected_company_id: None, has_complete_controller_context: false, } } fn from_runtime_state(state: &RuntimeState) -> Self { Self { known_company_ids: state .companies .iter() .map(|company| company.company_id) .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 }), } } } pub fn project_save_slice_to_runtime_state_import( save_slice: &SmpLoadedSaveSlice, import_id: &str, description: Option, ) -> Result { if import_id.trim().is_empty() { return Err("import_id must not be empty".to_string()); } let projection = project_save_slice_components( save_slice, &ImportCompanyContext::standalone(), SaveSliceProjectionMode::Standalone, )?; let state = RuntimeState { calendar: CalendarPoint { year: 1830, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: projection.world_flags, save_profile: projection.save_profile, world_restore: projection.world_restore, metadata: projection.metadata, companies: Vec::new(), selected_company_id: None, packed_event_collection: projection.packed_event_collection, event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, special_conditions: projection.special_conditions, service_state: RuntimeServiceState::default(), }; state.validate()?; Ok(RuntimeStateImport { import_id: import_id.to_string(), description, state, }) } pub fn project_save_slice_overlay_to_runtime_state_import( base_state: &RuntimeState, save_slice: &SmpLoadedSaveSlice, import_id: &str, description: Option, ) -> Result { if import_id.trim().is_empty() { return Err("import_id must not be empty".to_string()); } base_state.validate()?; let company_context = ImportCompanyContext::from_runtime_state(base_state); let projection = project_save_slice_components( save_slice, &company_context, SaveSliceProjectionMode::Overlay, )?; let mut world_flags = base_state.world_flags.clone(); world_flags.retain(|key, _| !key.starts_with("save_slice.")); world_flags.extend(projection.world_flags); let mut metadata = base_state.metadata.clone(); metadata.retain(|key, _| !key.starts_with("save_slice.")); metadata.extend(projection.metadata); let state = RuntimeState { calendar: base_state.calendar, world_flags, save_profile: projection.save_profile, world_restore: projection.world_restore, metadata, companies: base_state.companies.clone(), selected_company_id: base_state.selected_company_id, packed_event_collection: projection.packed_event_collection, event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, special_conditions: projection.special_conditions, service_state: base_state.service_state.clone(), }; state.validate()?; Ok(RuntimeStateImport { import_id: import_id.to_string(), description, state, }) } fn project_save_slice_components( save_slice: &SmpLoadedSaveSlice, company_context: &ImportCompanyContext, mode: SaveSliceProjectionMode, ) -> Result { let mut world_flags = BTreeMap::new(); world_flags.insert( "save_slice.profile_present".to_string(), save_slice.profile.is_some(), ); world_flags.insert( "save_slice.candidate_availability_present".to_string(), save_slice.candidate_availability_table.is_some(), ); world_flags.insert( "save_slice.special_conditions_present".to_string(), save_slice.special_conditions_table.is_some(), ); world_flags.insert( "save_slice.event_runtime_collection_present".to_string(), save_slice.event_runtime_collection.is_some(), ); world_flags.insert( "save_slice.mechanism_confidence_grounded".to_string(), save_slice.mechanism_confidence == "grounded", ); if let Some(profile) = &save_slice.profile { world_flags.insert( "save_slice.profile_byte_0x82_nonzero".to_string(), profile.profile_byte_0x82 != 0, ); world_flags.insert( "save_slice.profile_byte_0x97_nonzero".to_string(), profile.profile_byte_0x97 != 0, ); world_flags.insert( "save_slice.profile_byte_0xc5_nonzero".to_string(), profile.profile_byte_0xc5 != 0, ); } let mut metadata = BTreeMap::new(); metadata.insert( "save_slice.import_projection".to_string(), match mode { SaveSliceProjectionMode::Standalone => "partial-runtime-restore-v1", SaveSliceProjectionMode::Overlay => "overlay-runtime-restore-v1", } .to_string(), ); metadata.insert( "save_slice.calendar_source".to_string(), match mode { SaveSliceProjectionMode::Standalone => "default-1830-placeholder", SaveSliceProjectionMode::Overlay => "base-snapshot-preserved", } .to_string(), ); metadata.insert( "save_slice.selected_year_seed_tuple_source".to_string(), "raw-lane-via-0x51d3f0".to_string(), ); metadata.insert( "save_slice.selected_year_absolute_counter_source".to_string(), "mode-adjusted-lane-via-0x51d390-0x409e80".to_string(), ); metadata.insert( "save_slice.selected_year_absolute_counter_reconstructible_from_save".to_string(), "false".to_string(), ); metadata.insert( "save_slice.disable_cargo_economy_special_condition_slot".to_string(), "30".to_string(), ); metadata.insert( "save_slice.disable_cargo_economy_special_condition_reconstructible_from_save".to_string(), "true".to_string(), ); metadata.insert( "save_slice.disable_cargo_economy_special_condition_write_side_grounded".to_string(), "true".to_string(), ); metadata.insert( "save_slice.selected_year_absolute_counter_adjustment_context".to_string(), "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" .to_string(), ); metadata.insert( "save_slice.mechanism_family".to_string(), save_slice.mechanism_family.clone(), ); metadata.insert( "save_slice.mechanism_confidence".to_string(), save_slice.mechanism_confidence.clone(), ); if let Some(family) = &save_slice.container_profile_family { metadata.insert( "save_slice.container_profile_family".to_string(), family.clone(), ); } if let Some(family) = &save_slice.trailer_family { metadata.insert("save_slice.trailer_family".to_string(), family.clone()); } if let Some(family) = &save_slice.bridge_family { metadata.insert("save_slice.bridge_family".to_string(), family.clone()); } let (packed_event_collection, event_runtime_records) = project_packed_event_collection(save_slice, company_context)?; if let Some(summary) = &save_slice.event_runtime_collection { metadata.insert( "save_slice.event_runtime_collection_source_kind".to_string(), summary.source_kind.clone(), ); metadata.insert( "save_slice.event_runtime_collection_version_hex".to_string(), summary.packed_state_version_hex.clone(), ); metadata.insert( "save_slice.event_runtime_collection_record_count".to_string(), summary.live_record_count.to_string(), ); metadata.insert( "save_slice.event_runtime_collection_decoded_record_count".to_string(), summary.decoded_record_count.to_string(), ); metadata.insert( "save_slice.event_runtime_collection_imported_runtime_record_count".to_string(), event_runtime_records.len().to_string(), ); } let save_profile = if let Some(profile) = &save_slice.profile { metadata.insert( "save_slice.profile_kind".to_string(), profile.profile_kind.clone(), ); metadata.insert( "save_slice.profile_family".to_string(), profile.profile_family.clone(), ); metadata.insert( "save_slice.packed_profile_offset".to_string(), profile.packed_profile_offset.to_string(), ); metadata.insert( "save_slice.packed_profile_len".to_string(), profile.packed_profile_len.to_string(), ); metadata.insert( "save_slice.leading_word_0_hex".to_string(), profile.leading_word_0_hex.clone(), ); metadata.insert( "save_slice.profile_byte_0x77_hex".to_string(), profile.profile_byte_0x77_hex.clone(), ); metadata.insert( "save_slice.profile_byte_0x82_hex".to_string(), profile.profile_byte_0x82_hex.clone(), ); metadata.insert( "save_slice.profile_byte_0x97_hex".to_string(), profile.profile_byte_0x97_hex.clone(), ); metadata.insert( "save_slice.profile_byte_0xc5_hex".to_string(), profile.profile_byte_0xc5_hex.clone(), ); if let Some(header_flag_word_3_hex) = &profile.header_flag_word_3_hex { metadata.insert( "save_slice.header_flag_word_3_hex".to_string(), header_flag_word_3_hex.clone(), ); } if let Some(map_path) = &profile.map_path { metadata.insert("save_slice.map_path".to_string(), map_path.clone()); } if let Some(display_name) = &profile.display_name { metadata.insert("save_slice.display_name".to_string(), display_name.clone()); } RuntimeSaveProfileState { profile_kind: Some(profile.profile_kind.clone()), profile_family: Some(profile.profile_family.clone()), map_path: profile.map_path.clone(), display_name: profile.display_name.clone(), selected_year_profile_lane: Some(profile.profile_byte_0x77), sandbox_enabled: Some(profile.profile_byte_0x82 != 0), campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), staged_profile_copy_on_restore: Some(profile.profile_byte_0x97 != 0), } } else { RuntimeSaveProfileState::default() }; let special_condition_enabled = |slot_index: u8| { save_slice.special_conditions_table.as_ref().map(|table| { table .entries .iter() .find(|entry| entry.slot_index == slot_index) .map(|entry| entry.value != 0) .unwrap_or(false) }) }; let world_restore = if let Some(profile) = &save_slice.profile { let disable_cargo_economy_special_condition_enabled = special_condition_enabled(30); RuntimeWorldRestoreState { selected_year_profile_lane: Some(profile.profile_byte_0x77), campaign_scenario_enabled: Some(profile.profile_byte_0xc5 != 0), sandbox_enabled: Some(profile.profile_byte_0x82 != 0), seed_tuple_written_from_raw_lane: Some(true), absolute_counter_requires_shell_context: Some(true), absolute_counter_reconstructible_from_save: Some(false), disable_cargo_economy_special_condition_slot: Some(30), disable_cargo_economy_special_condition_reconstructible_from_save: Some(true), disable_cargo_economy_special_condition_write_side_grounded: Some(true), disable_cargo_economy_special_condition_enabled, use_bio_accelerator_cars_enabled: special_condition_enabled(29), use_wartime_cargos_enabled: special_condition_enabled(31), disable_train_crashes_enabled: special_condition_enabled(32), disable_train_crashes_and_breakdowns_enabled: special_condition_enabled(33), ai_ignore_territories_at_startup_enabled: special_condition_enabled(34), absolute_counter_restore_kind: Some( "mode-adjusted-selected-year-lane".to_string(), ), absolute_counter_adjustment_context: Some( "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" .to_string(), ), } } else { RuntimeWorldRestoreState::default() }; let mut candidate_availability = BTreeMap::new(); if let Some(table) = &save_slice.candidate_availability_table { metadata.insert( "save_slice.candidate_table_source_kind".to_string(), table.source_kind.clone(), ); metadata.insert( "save_slice.candidate_table_semantic_family".to_string(), table.semantic_family.clone(), ); metadata.insert( "save_slice.candidate_table_entry_count".to_string(), table.observed_entry_count.to_string(), ); metadata.insert( "save_slice.candidate_table_zero_count".to_string(), table.zero_availability_count.to_string(), ); for entry in &table.entries { candidate_availability.insert(entry.text.clone(), entry.availability_dword); } } let mut special_conditions = BTreeMap::new(); if let Some(table) = &save_slice.special_conditions_table { metadata.insert( "save_slice.special_conditions_source_kind".to_string(), table.source_kind.clone(), ); metadata.insert( "save_slice.special_conditions_table_offset".to_string(), table.table_offset.to_string(), ); metadata.insert( "save_slice.special_conditions_enabled_visible_count".to_string(), table.enabled_visible_count.to_string(), ); for entry in &table.entries { if !entry.hidden { special_conditions.insert(entry.label.clone(), entry.value); } } } for (index, note) in save_slice.notes.iter().enumerate() { metadata.insert(format!("save_slice.note.{index}"), note.clone()); } Ok(SaveSliceProjection { world_flags, save_profile, world_restore, metadata, packed_event_collection, event_runtime_records, candidate_availability, special_conditions, }) } fn project_packed_event_collection( save_slice: &SmpLoadedSaveSlice, company_context: &ImportCompanyContext, ) -> Result< ( Option, Vec, ), String, > { let Some(summary) = save_slice.event_runtime_collection.as_ref() else { return Ok((None, Vec::new())); }; 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) { let runtime_record = import_result?; imported_record_ids.insert(record.live_entry_id); imported_runtime_records.push(runtime_record); } } let records = summary .records .iter() .map(|record| { runtime_packed_event_record_summary_from_smp( record, company_context, imported_record_ids.contains(&record.live_entry_id), ) }) .collect::>(); Ok(( Some(RuntimePackedEventCollectionSummary { source_kind: summary.source_kind.clone(), mechanism_family: summary.mechanism_family.clone(), mechanism_confidence: summary.mechanism_confidence.clone(), container_profile_family: summary.container_profile_family.clone(), packed_state_version: summary.packed_state_version, packed_state_version_hex: summary.packed_state_version_hex.clone(), live_id_bound: summary.live_id_bound, live_record_count: summary.live_record_count, live_entry_ids: summary.live_entry_ids.clone(), decoded_record_count: records .iter() .filter(|record| record.decode_status != "unsupported_framing") .count(), imported_runtime_record_count: imported_runtime_records.len(), records, }), imported_runtime_records, )) } fn runtime_packed_event_record_summary_from_smp( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportCompanyContext, imported: bool, ) -> RuntimePackedEventRecordSummary { let lowered_decoded_actions = lowered_record_decoded_actions(record).unwrap_or_else(|_| record.decoded_actions.clone()); RuntimePackedEventRecordSummary { record_index: record.record_index, live_entry_id: record.live_entry_id, payload_offset: record.payload_offset, payload_len: record.payload_len, decode_status: record.decode_status.clone(), payload_family: record.payload_family.clone(), trigger_kind: record.trigger_kind, active: record.active, marks_collection_dirty: record.marks_collection_dirty, one_shot: record.one_shot, compact_control: record .compact_control .as_ref() .map(runtime_packed_event_compact_control_summary_from_smp), text_bands: record .text_bands .iter() .map(runtime_packed_event_text_band_summary_from_smp) .collect(), standalone_condition_row_count: record.standalone_condition_row_count, standalone_condition_rows: record .standalone_condition_rows .iter() .map(runtime_packed_event_condition_row_summary_from_smp) .collect(), negative_sentinel_scope: record .negative_sentinel_scope .as_ref() .map(runtime_packed_event_negative_sentinel_scope_summary_from_smp), grouped_effect_row_counts: record.grouped_effect_row_counts.clone(), grouped_effect_rows: record .grouped_effect_rows .iter() .map(runtime_packed_event_grouped_effect_row_summary_from_smp) .collect(), grouped_company_targets: classify_real_grouped_company_targets(record), decoded_actions: lowered_decoded_actions, executable_import_ready: record.executable_import_ready, import_outcome: Some(determine_packed_event_import_outcome( record, company_context, imported, )), notes: record.notes.clone(), } } fn runtime_packed_event_negative_sentinel_scope_summary_from_smp( scope: &SmpLoadedPackedEventNegativeSentinelScopeSummary, ) -> RuntimePackedEventNegativeSentinelScopeSummary { RuntimePackedEventNegativeSentinelScopeSummary { company_test_scope: scope.company_test_scope, player_test_scope: scope.player_test_scope, territory_scope_selector_is_0x63: scope.territory_scope_selector_is_0x63, source_row_indexes: scope.source_row_indexes.clone(), } } fn runtime_packed_event_compact_control_summary_from_smp( control: &crate::SmpLoadedPackedEventCompactControlSummary, ) -> RuntimePackedEventCompactControlSummary { RuntimePackedEventCompactControlSummary { mode_byte_0x7ef: control.mode_byte_0x7ef, primary_selector_0x7f0: control.primary_selector_0x7f0, grouped_mode_0x7f4: control.grouped_mode_0x7f4, one_shot_header_0x7f5: control.one_shot_header_0x7f5, modifier_flag_0x7f9: control.modifier_flag_0x7f9, modifier_flag_0x7fa: control.modifier_flag_0x7fa, grouped_target_scope_ordinals_0x7fb: control.grouped_target_scope_ordinals_0x7fb.clone(), grouped_scope_checkboxes_0x7ff: control.grouped_scope_checkboxes_0x7ff.clone(), summary_toggle_0x800: control.summary_toggle_0x800, grouped_territory_selectors_0x80f: control.grouped_territory_selectors_0x80f.clone(), } } fn runtime_packed_event_text_band_summary_from_smp( band: &SmpLoadedPackedEventTextBandSummary, ) -> RuntimePackedEventTextBandSummary { RuntimePackedEventTextBandSummary { label: band.label.clone(), packed_len: band.packed_len, present: band.present, preview: band.preview.clone(), } } fn runtime_packed_event_condition_row_summary_from_smp( row: &crate::SmpLoadedPackedEventConditionRowSummary, ) -> RuntimePackedEventConditionRowSummary { RuntimePackedEventConditionRowSummary { row_index: row.row_index, raw_condition_id: row.raw_condition_id, subtype: row.subtype, flag_bytes: row.flag_bytes.clone(), candidate_name: row.candidate_name.clone(), notes: row.notes.clone(), } } fn runtime_packed_event_grouped_effect_row_summary_from_smp( row: &crate::SmpLoadedPackedEventGroupedEffectRowSummary, ) -> RuntimePackedEventGroupedEffectRowSummary { RuntimePackedEventGroupedEffectRowSummary { 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, value_dword_0x0d: row.value_dword_0x0d, value_byte_0x11: row.value_byte_0x11, value_byte_0x12: row.value_byte_0x12, 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(), } } fn smp_packed_record_to_runtime_event_record( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportCompanyContext, ) -> Option> { if record.decode_status == "unsupported_framing" { return None; } if record.payload_family == "real_packed_v1" { if record.compact_control.is_none() || !record.executable_import_ready { return None; } } let lowered_effects = match lowered_record_decoded_actions(record) { Ok(effects) => effects, Err(_) => return None, }; let effects = match smp_runtime_effects_to_runtime_effects(&lowered_effects, company_context) { Ok(effects) => effects, Err(_) => return None, }; Some((|| { let trigger_kind = record.trigger_kind.ok_or_else(|| { format!( "packed event record {} is missing trigger_kind", 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, active, marks_collection_dirty, one_shot, effects, } .into_runtime_record()) })()) } fn lowered_record_decoded_actions( record: &SmpLoadedPackedEventRecordSummary, ) -> Result, CompanyTargetImportBlocker> { if let Some(blocker) = packed_record_condition_scope_import_blocker(record) { return Err(blocker); } let Some(lowered_target) = lowered_condition_true_company_target(record) else { return Ok(record.decoded_actions.clone()); }; Ok(record .decoded_actions .iter() .map(|effect| lower_condition_true_company_target_in_effect(effect, &lowered_target)) .collect()) } fn packed_record_condition_scope_import_blocker( record: &SmpLoadedPackedEventRecordSummary, ) -> Option { if record.standalone_condition_rows.is_empty() { return None; } let negative_sentinel_row_count = record .standalone_condition_rows .iter() .filter(|row| row.raw_condition_id == -1) .count(); if negative_sentinel_row_count == 0 { return Some(CompanyTargetImportBlocker::MissingConditionContext); } if negative_sentinel_row_count != record.standalone_condition_rows.len() { return Some(CompanyTargetImportBlocker::MissingConditionContext); } let Some(scope) = record.negative_sentinel_scope.as_ref() else { return Some(CompanyTargetImportBlocker::MissingConditionContext); }; if scope.player_test_scope != RuntimePlayerConditionTestScope::Disabled { return Some(CompanyTargetImportBlocker::PlayerConditionScope); } if scope.territory_scope_selector_is_0x63 { return Some(CompanyTargetImportBlocker::TerritoryConditionScope); } if scope.company_test_scope == RuntimeCompanyConditionTestScope::Disabled { return Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled); } None } fn lowered_condition_true_company_target( record: &SmpLoadedPackedEventRecordSummary, ) -> Option { let scope = record.negative_sentinel_scope.as_ref()?; match scope.company_test_scope { RuntimeCompanyConditionTestScope::Disabled => None, RuntimeCompanyConditionTestScope::AllCompanies => Some(RuntimeCompanyTarget::AllActive), RuntimeCompanyConditionTestScope::SelectedCompanyOnly => { Some(RuntimeCompanyTarget::SelectedCompany) } RuntimeCompanyConditionTestScope::AiCompaniesOnly => { Some(RuntimeCompanyTarget::AiCompanies) } RuntimeCompanyConditionTestScope::HumanCompaniesOnly => { Some(RuntimeCompanyTarget::HumanCompanies) } } } fn lower_condition_true_company_target_in_effect( effect: &RuntimeEffect, lowered_target: &RuntimeCompanyTarget, ) -> RuntimeEffect { match effect { RuntimeEffect::SetWorldFlag { key, value } => RuntimeEffect::SetWorldFlag { key: key.clone(), value: *value, }, RuntimeEffect::SetCompanyCash { target, value } => RuntimeEffect::SetCompanyCash { target: lower_condition_true_company_target_in_company_target(target, lowered_target), value: *value, }, RuntimeEffect::DeactivateCompany { target } => RuntimeEffect::DeactivateCompany { target: lower_condition_true_company_target_in_company_target(target, lowered_target), }, RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => { RuntimeEffect::SetCompanyTrackLayingCapacity { target: lower_condition_true_company_target_in_company_target( target, lowered_target, ), value: *value, } } RuntimeEffect::AdjustCompanyCash { target, delta } => RuntimeEffect::AdjustCompanyCash { target: lower_condition_true_company_target_in_company_target(target, lowered_target), delta: *delta, }, RuntimeEffect::AdjustCompanyDebt { target, delta } => RuntimeEffect::AdjustCompanyDebt { target: lower_condition_true_company_target_in_company_target(target, lowered_target), delta: *delta, }, RuntimeEffect::SetCandidateAvailability { name, value } => { RuntimeEffect::SetCandidateAvailability { name: name.clone(), value: *value, } } RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { label: label.clone(), value: *value, }, RuntimeEffect::AppendEventRecord { record } => RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: record.record_id, trigger_kind: record.trigger_kind, active: record.active, marks_collection_dirty: record.marks_collection_dirty, one_shot: record.one_shot, effects: record .effects .iter() .map(|nested| { lower_condition_true_company_target_in_effect(nested, lowered_target) }) .collect(), }), }, RuntimeEffect::ActivateEventRecord { record_id } => RuntimeEffect::ActivateEventRecord { record_id: *record_id, }, RuntimeEffect::DeactivateEventRecord { record_id } => { RuntimeEffect::DeactivateEventRecord { record_id: *record_id, } } RuntimeEffect::RemoveEventRecord { record_id } => RuntimeEffect::RemoveEventRecord { record_id: *record_id, }, } } fn lower_condition_true_company_target_in_company_target( target: &RuntimeCompanyTarget, lowered_target: &RuntimeCompanyTarget, ) -> RuntimeCompanyTarget { match target { RuntimeCompanyTarget::ConditionTrueCompany => lowered_target.clone(), _ => target.clone(), } } fn smp_runtime_effects_to_runtime_effects( effects: &[RuntimeEffect], company_context: &ImportCompanyContext, ) -> Result, String> { effects .iter() .map(|effect| smp_runtime_effect_to_runtime_effect(effect, company_context)) .collect() } fn smp_runtime_effect_to_runtime_effect( effect: &RuntimeEffect, company_context: &ImportCompanyContext, ) -> Result { match effect { RuntimeEffect::SetWorldFlag { key, value } => Ok(RuntimeEffect::SetWorldFlag { 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::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 { target: target.clone(), delta: *delta, }) } else { Err(company_target_import_error_message(target, company_context)) } } RuntimeEffect::AdjustCompanyDebt { target, delta } => { if company_target_import_blocker(target, company_context).is_none() { Ok(RuntimeEffect::AdjustCompanyDebt { target: target.clone(), delta: *delta, }) } else { Err(company_target_import_error_message(target, company_context)) } } RuntimeEffect::SetCandidateAvailability { name, value } => { Ok(RuntimeEffect::SetCandidateAvailability { name: name.clone(), value: *value, }) } RuntimeEffect::SetSpecialCondition { label, value } => { Ok(RuntimeEffect::SetSpecialCondition { label: label.clone(), value: *value, }) } RuntimeEffect::AppendEventRecord { record } => Ok(RuntimeEffect::AppendEventRecord { record: Box::new(smp_runtime_record_template_to_runtime( record, company_context, )?), }), RuntimeEffect::ActivateEventRecord { record_id } => { Ok(RuntimeEffect::ActivateEventRecord { record_id: *record_id, }) } RuntimeEffect::DeactivateEventRecord { record_id } => { Ok(RuntimeEffect::DeactivateEventRecord { record_id: *record_id, }) } RuntimeEffect::RemoveEventRecord { record_id } => Ok(RuntimeEffect::RemoveEventRecord { record_id: *record_id, }), } } fn smp_runtime_record_template_to_runtime( record: &RuntimeEventRecordTemplate, company_context: &ImportCompanyContext, ) -> Result { Ok(RuntimeEventRecordTemplate { record_id: record.record_id, trigger_kind: record.trigger_kind, active: record.active, marks_collection_dirty: record.marks_collection_dirty, one_shot: record.one_shot, effects: smp_runtime_effects_to_runtime_effects(&record.effects, company_context)?, }) } fn company_target_import_blocker( target: &RuntimeCompanyTarget, company_context: &ImportCompanyContext, ) -> Option { match target { RuntimeCompanyTarget::AllActive => None, RuntimeCompanyTarget::Ids { ids } => { if ids.is_empty() || ids .iter() .any(|company_id| !company_context.known_company_ids.contains(company_id)) { Some(CompanyTargetImportBlocker::MissingCompanyContext) } else { None } } RuntimeCompanyTarget::HumanCompanies | RuntimeCompanyTarget::AiCompanies => { if !company_context.has_complete_controller_context { Some(CompanyTargetImportBlocker::MissingCompanyRoleContext) } else { None } } RuntimeCompanyTarget::SelectedCompany => { if company_context.selected_company_id.is_some() { None } else { Some(CompanyTargetImportBlocker::MissingSelectionContext) } } RuntimeCompanyTarget::ConditionTrueCompany => { Some(CompanyTargetImportBlocker::MissingConditionContext) } } } fn company_target_import_error_message( target: &RuntimeCompanyTarget, company_context: &ImportCompanyContext, ) -> String { match company_target_import_blocker(target, company_context) { Some(CompanyTargetImportBlocker::MissingCompanyContext) => { "packed company effect requires resolved company ids".to_string() } Some(CompanyTargetImportBlocker::MissingSelectionContext) => { "packed company effect requires selected_company_id context".to_string() } Some(CompanyTargetImportBlocker::MissingCompanyRoleContext) => { "packed company effect requires company controller role context".to_string() } Some(CompanyTargetImportBlocker::MissingConditionContext) => { "packed company effect requires condition-relative context".to_string() } Some(CompanyTargetImportBlocker::CompanyConditionScopeDisabled) => { "packed company effect disables company-side negative-sentinel condition scope" .to_string() } Some(CompanyTargetImportBlocker::PlayerConditionScope) => { "packed company effect requires player runtime ownership for negative-sentinel scope" .to_string() } Some(CompanyTargetImportBlocker::TerritoryConditionScope) => { "packed company effect requires territory runtime ownership for negative-sentinel scope" .to_string() } None => "packed company effect is importable".to_string(), } } fn determine_packed_event_import_outcome( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportCompanyContext, imported: bool, ) -> String { if imported { return "imported".to_string(); } if record.decode_status == "unsupported_framing" { return "blocked_unsupported_decode".to_string(); } if record.payload_family == "real_packed_v1" { 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_condition_scope_import_blocker(record) { return company_target_import_outcome(blocker).to_string(); } if let Some(blocker) = packed_record_company_target_import_blocker(record, company_context) { return company_target_import_outcome(blocker).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(); } "blocked_unsupported_decode".to_string() } fn packed_record_company_target_import_blocker( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportCompanyContext, ) -> Option { let lowered_effects = match lowered_record_decoded_actions(record) { Ok(effects) => effects, Err(blocker) => return Some(blocker), }; lowered_effects .iter() .find_map(|effect| runtime_effect_company_target_import_blocker(effect, company_context)) } fn runtime_effect_company_target_import_blocker( effect: &RuntimeEffect, company_context: &ImportCompanyContext, ) -> Option { match effect { RuntimeEffect::SetCompanyCash { target, .. } | RuntimeEffect::DeactivateCompany { target } | RuntimeEffect::SetCompanyTrackLayingCapacity { 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::SetWorldFlag { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } | RuntimeEffect::RemoveEventRecord { .. } => None, } } fn classify_real_grouped_company_targets( record: &SmpLoadedPackedEventRecordSummary, ) -> Vec> { let Some(control) = &record.compact_control else { return Vec::new(); }; control .grouped_target_scope_ordinals_0x7fb .iter() .enumerate() .map(|(group_index, ordinal)| { if !record .grouped_effect_rows .iter() .any(|row| row.group_index == group_index) { return None; } classify_real_grouped_company_target(*ordinal) }) .collect() } fn classify_real_grouped_company_target(ordinal: u8) -> Option { match ordinal { 0 => Some(RuntimeCompanyTarget::ConditionTrueCompany), 1 => Some(RuntimeCompanyTarget::SelectedCompany), 2 => Some(RuntimeCompanyTarget::HumanCompanies), 3 => Some(RuntimeCompanyTarget::AiCompanies), _ => None, } } fn company_target_import_outcome(blocker: CompanyTargetImportBlocker) -> &'static str { match blocker { CompanyTargetImportBlocker::MissingCompanyContext => "blocked_missing_company_context", CompanyTargetImportBlocker::MissingSelectionContext => "blocked_missing_selection_context", CompanyTargetImportBlocker::MissingCompanyRoleContext => { "blocked_missing_company_role_context" } CompanyTargetImportBlocker::MissingConditionContext => "blocked_missing_condition_context", CompanyTargetImportBlocker::CompanyConditionScopeDisabled => { "blocked_company_condition_scope_disabled" } CompanyTargetImportBlocker::PlayerConditionScope => "blocked_player_condition_scope", CompanyTargetImportBlocker::TerritoryConditionScope => "blocked_territory_condition_scope", } } pub fn validate_runtime_state_dump_document( document: &RuntimeStateDumpDocument, ) -> Result<(), String> { if document.format_version != STATE_DUMP_FORMAT_VERSION { return Err(format!( "unsupported state dump format_version {} (expected {})", document.format_version, STATE_DUMP_FORMAT_VERSION )); } if document.dump_id.trim().is_empty() { return Err("dump_id must not be empty".to_string()); } document.state.validate() } pub fn validate_runtime_save_slice_document( document: &RuntimeSaveSliceDocument, ) -> Result<(), String> { if document.format_version != SAVE_SLICE_DOCUMENT_FORMAT_VERSION { return Err(format!( "unsupported save slice document format_version {} (expected {})", document.format_version, SAVE_SLICE_DOCUMENT_FORMAT_VERSION )); } if document.save_slice_id.trim().is_empty() { return Err("save_slice_id must not be empty".to_string()); } if document .source .description .as_deref() .is_some_and(|text| text.trim().is_empty()) { return Err("save slice source.description must not be empty".to_string()); } if document .source .original_save_filename .as_deref() .is_some_and(|text| text.trim().is_empty()) { return Err("save slice source.original_save_filename must not be empty".to_string()); } if document .source .original_save_sha256 .as_deref() .is_some_and(|text| text.trim().is_empty()) { return Err("save slice source.original_save_sha256 must not be empty".to_string()); } for (index, note) in document.source.notes.iter().enumerate() { if note.trim().is_empty() { return Err(format!( "save slice source.notes[{index}] must not be empty" )); } } if document.save_slice.mechanism_family.trim().is_empty() { return Err("save_slice.mechanism_family must not be empty".to_string()); } if document.save_slice.mechanism_confidence.trim().is_empty() { return Err("save_slice.mechanism_confidence must not be empty".to_string()); } Ok(()) } pub fn validate_runtime_overlay_import_document( document: &RuntimeOverlayImportDocument, ) -> Result<(), String> { if document.format_version != OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION { return Err(format!( "unsupported overlay import document format_version {} (expected {})", document.format_version, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION )); } if document.import_id.trim().is_empty() { return Err("import_id must not be empty".to_string()); } if document .source .description .as_deref() .is_some_and(|text| text.trim().is_empty()) { return Err("overlay import source.description must not be empty".to_string()); } for (index, note) in document.source.notes.iter().enumerate() { if note.trim().is_empty() { return Err(format!( "overlay import source.notes[{index}] must not be empty" )); } } if document.base_snapshot_path.trim().is_empty() { return Err("base_snapshot_path must not be empty".to_string()); } if document.save_slice_path.trim().is_empty() { return Err("save_slice_path must not be empty".to_string()); } Ok(()) } pub fn load_runtime_save_slice_document( path: &Path, ) -> Result> { let text = std::fs::read_to_string(path)?; let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?; Ok(document) } pub fn load_runtime_overlay_import_document( path: &Path, ) -> Result> { let text = std::fs::read_to_string(path)?; let document: RuntimeOverlayImportDocument = serde_json::from_str(&text)?; Ok(document) } pub fn save_runtime_save_slice_document( path: &Path, document: &RuntimeSaveSliceDocument, ) -> Result<(), Box> { validate_runtime_save_slice_document(document) .map_err(|err| format!("invalid runtime save slice document: {err}"))?; let bytes = serde_json::to_vec_pretty(document)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, bytes)?; Ok(()) } pub fn save_runtime_overlay_import_document( path: &Path, document: &RuntimeOverlayImportDocument, ) -> Result<(), Box> { validate_runtime_overlay_import_document(document) .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; let bytes = serde_json::to_vec_pretty(document)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, bytes)?; Ok(()) } pub fn load_runtime_state_import( path: &Path, ) -> Result> { let text = std::fs::read_to_string(path)?; load_runtime_state_import_from_str_with_base( &text, path.file_stem() .and_then(|stem| stem.to_str()) .unwrap_or("runtime-state"), path.parent().unwrap_or_else(|| Path::new(".")), ) } pub fn load_runtime_state_import_from_str( text: &str, fallback_id: &str, ) -> Result> { load_runtime_state_import_from_str_with_base(text, fallback_id, Path::new(".")) } fn load_runtime_state_import_from_str_with_base( text: &str, fallback_id: &str, base_dir: &Path, ) -> Result> { if let Ok(document) = serde_json::from_str::(text) { validate_runtime_state_dump_document(&document) .map_err(|err| format!("invalid runtime state dump document: {err}"))?; return Ok(RuntimeStateImport { import_id: document.dump_id, description: document.source.description, state: document.state, }); } if let Ok(document) = serde_json::from_str::(text) { validate_runtime_save_slice_document(&document) .map_err(|err| format!("invalid runtime save slice document: {err}"))?; let mut description_parts = Vec::new(); if let Some(description) = document.source.description { description_parts.push(description); } if let Some(filename) = document.source.original_save_filename { description_parts.push(format!("source save {filename}")); } let import = project_save_slice_to_runtime_state_import( &document.save_slice, &document.save_slice_id, if description_parts.is_empty() { None } else { Some(description_parts.join(" | ")) }, )?; return Ok(import); } if let Ok(document) = serde_json::from_str::(text) { validate_runtime_overlay_import_document(&document) .map_err(|err| format!("invalid runtime overlay import document: {err}"))?; let base_snapshot_path = resolve_document_path(base_dir, &document.base_snapshot_path); let save_slice_path = resolve_document_path(base_dir, &document.save_slice_path); let snapshot = load_runtime_snapshot_document(&base_snapshot_path)?; validate_runtime_snapshot_document(&snapshot).map_err(|err| { format!( "invalid runtime snapshot {}: {err}", base_snapshot_path.display() ) })?; let save_slice_document = load_runtime_save_slice_document(&save_slice_path)?; validate_runtime_save_slice_document(&save_slice_document).map_err(|err| { format!( "invalid runtime save slice document {}: {err}", save_slice_path.display() ) })?; let mut description_parts = Vec::new(); if let Some(description) = document.source.description { description_parts.push(description); } if let Some(description) = snapshot.source.description { description_parts.push(format!("base snapshot {description}")); } if let Some(description) = save_slice_document.source.description { description_parts.push(format!("save slice {description}")); } return project_save_slice_overlay_to_runtime_state_import( &snapshot.state, &save_slice_document.save_slice, &document.import_id, if description_parts.is_empty() { None } else { Some(description_parts.join(" | ")) }, ) .map_err(Into::into); } let state: RuntimeState = serde_json::from_str(text)?; state .validate() .map_err(|err| format!("invalid runtime state: {err}"))?; Ok(RuntimeStateImport { import_id: fallback_id.to_string(), description: None, state, }) } fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf { let candidate = PathBuf::from(path); if candidate.is_absolute() { candidate } else { base_dir.join(candidate) } } #[cfg(test)] mod tests { use super::*; use crate::{StepCommand, execute_step_command}; fn state() -> RuntimeState { 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::new(), 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(), } } fn packed_text_bands() -> Vec { vec![ crate::SmpLoadedPackedEventTextBandSummary { label: "primary_text_band".to_string(), packed_len: 5, present: true, preview: "Alpha".to_string(), }, crate::SmpLoadedPackedEventTextBandSummary { label: "secondary_text_band_0".to_string(), packed_len: 0, present: false, preview: "".to_string(), }, crate::SmpLoadedPackedEventTextBandSummary { label: "secondary_text_band_1".to_string(), packed_len: 0, present: false, preview: "".to_string(), }, crate::SmpLoadedPackedEventTextBandSummary { label: "secondary_text_band_2".to_string(), packed_len: 0, present: false, preview: "".to_string(), }, crate::SmpLoadedPackedEventTextBandSummary { label: "secondary_text_band_3".to_string(), packed_len: 0, present: false, preview: "".to_string(), }, crate::SmpLoadedPackedEventTextBandSummary { label: "secondary_text_band_4".to_string(), packed_len: 0, present: false, preview: "".to_string(), }, ] } fn real_condition_rows() -> Vec { vec![crate::SmpLoadedPackedEventConditionRowSummary { row_index: 0, raw_condition_id: -1, subtype: 4, flag_bytes: vec![0x30; 25], candidate_name: Some("AutoPlant".to_string()), notes: vec!["negative sentinel-style condition row id".to_string()], }] } fn synthetic_packed_record( record_index: usize, live_entry_id: u32, effect: RuntimeEffect, ) -> crate::SmpLoadedPackedEventRecordSummary { crate::SmpLoadedPackedEventRecordSummary { record_index, live_entry_id, payload_offset: Some(0x7200 + (live_entry_id as usize * 0x20)), payload_len: Some(64), decode_status: "parity_only".to_string(), payload_family: "synthetic_harness".to_string(), trigger_kind: Some(7), active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![effect], executable_import_ready: false, notes: vec!["synthetic test record".to_string()], } } fn company_negative_sentinel_scope( company_test_scope: RuntimeCompanyConditionTestScope, ) -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { company_test_scope, player_test_scope: RuntimePlayerConditionTestScope::Disabled, territory_scope_selector_is_0x63: false, source_row_indexes: vec![0], } } fn territory_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, player_test_scope: RuntimePlayerConditionTestScope::Disabled, territory_scope_selector_is_0x63: true, source_row_indexes: vec![0], } } fn player_negative_sentinel_scope() -> crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { crate::SmpLoadedPackedEventNegativeSentinelScopeSummary { company_test_scope: RuntimeCompanyConditionTestScope::AllCompanies, player_test_scope: RuntimePlayerConditionTestScope::AllPlayers, territory_scope_selector_is_0x63: false, source_row_indexes: vec![0], } } fn real_grouped_rows() -> Vec { 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: 7, 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 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()], }] } 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, primary_selector_0x7f0: 0x63, grouped_mode_0x7f4: 2, one_shot_header_0x7f5: 1, modifier_flag_0x7f9: 1, modifier_flag_0x7fa: 0, grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], summary_toggle_0x800: 1, grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], } } fn real_compact_control_without_symbolic_company_scope() -> crate::SmpLoadedPackedEventCompactControlSummary { crate::SmpLoadedPackedEventCompactControlSummary { mode_byte_0x7ef: 6, primary_selector_0x7f0: 0x63, grouped_mode_0x7f4: 2, one_shot_header_0x7f5: 1, modifier_flag_0x7f9: 1, modifier_flag_0x7fa: 0, grouped_target_scope_ordinals_0x7fb: vec![8, 9, 10, 11], grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], summary_toggle_0x800: 1, grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], } } #[test] fn loads_dump_document() { let text = serde_json::to_string(&RuntimeStateDumpDocument { format_version: STATE_DUMP_FORMAT_VERSION, dump_id: "dump-smoke".to_string(), source: RuntimeStateDumpSource { description: Some("test dump".to_string()), source_binary: None, }, state: state(), }) .expect("dump should serialize"); let import = load_runtime_state_import_from_str(&text, "fallback").expect("dump should load"); assert_eq!(import.import_id, "dump-smoke"); assert_eq!(import.description.as_deref(), Some("test dump")); } #[test] fn loads_bare_runtime_state() { let text = serde_json::to_string(&state()).expect("state should serialize"); let import = load_runtime_state_import_from_str(&text, "fallback").expect("state should load"); assert_eq!(import.import_id, "fallback"); assert!(import.description.is_none()); } #[test] fn validates_and_roundtrips_save_slice_document() { let document = RuntimeSaveSliceDocument { format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, save_slice_id: "save-slice-smoke".to_string(), source: RuntimeSaveSliceDocumentSource { description: Some("test save slice".to_string()), original_save_filename: Some("smoke.gms".to_string()), original_save_sha256: Some("deadbeef".to_string()), notes: vec!["captured fixture".to_string()], }, save_slice: crate::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: None, notes: vec![], }, }; assert!(validate_runtime_save_slice_document(&document).is_ok()); let nonce = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system time should be after epoch") .as_nanos(); let path = std::env::temp_dir().join(format!("rrt-save-slice-doc-{nonce}.json")); save_runtime_save_slice_document(&path, &document).expect("save slice doc should save"); let loaded = load_runtime_save_slice_document(&path).expect("save slice doc should load"); assert_eq!(document, loaded); let _ = std::fs::remove_file(path); } #[test] fn loads_save_slice_document_as_runtime_state_import() { let text = serde_json::to_string(&RuntimeSaveSliceDocument { format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, save_slice_id: "save-slice-import".to_string(), source: RuntimeSaveSliceDocumentSource { description: Some("test save slice import".to_string()), original_save_filename: Some("import.gms".to_string()), original_save_sha256: None, notes: vec![], }, save_slice: crate::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: None, notes: vec![], }, }) .expect("save slice doc should serialize"); let import = load_runtime_state_import_from_str(&text, "fallback") .expect("save slice document should load as runtime import"); assert_eq!(import.import_id, "save-slice-import"); assert_eq!( import .state .metadata .get("save_slice.import_projection") .map(String::as_str), Some("partial-runtime-restore-v1") ); } #[test] fn projects_save_slice_into_runtime_state_import() { let save_slice = SmpLoadedSaveSlice { file_extension_hint: Some("gms".to_string()), container_profile_family: Some("rt3-105-save-container-v1".to_string()), mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), mechanism_confidence: "mixed".to_string(), trailer_family: Some("rt3-105-save-trailer-v1".to_string()), bridge_family: Some("rt3-105-save-post-span-bridge-v1".to_string()), profile: Some(crate::SmpLoadedProfile { profile_kind: "rt3-105-packed-profile".to_string(), profile_family: "rt3-105-save-container-v1".to_string(), packed_profile_offset: 0x73c0, packed_profile_len: 0x108, packed_profile_len_hex: "0x108".to_string(), leading_word_0: 3, leading_word_0_hex: "0x00000003".to_string(), header_flag_word_3: Some(0x01000000), header_flag_word_3_hex: Some("0x01000000".to_string()), map_path: Some("Alternate USA.gmp".to_string()), display_name: Some("Alternate USA".to_string()), profile_byte_0x77: 0x07, profile_byte_0x77_hex: "0x07".to_string(), profile_byte_0x82: 0x4d, profile_byte_0x82_hex: "0x4d".to_string(), profile_byte_0x97: 0x00, profile_byte_0x97_hex: "0x00".to_string(), profile_byte_0xc5: 0x00, profile_byte_0xc5_hex: "0x00".to_string(), }), candidate_availability_table: Some(crate::SmpLoadedCandidateAvailabilityTable { source_kind: "save-bridge-secondary-block".to_string(), semantic_family: "scenario-named-candidate-availability-table".to_string(), header_offset: 0x6a70, entries_offset: 0x6ad1, entries_end_offset: 0x73b7, observed_entry_count: 2, zero_availability_count: 1, zero_availability_names: vec!["Uranium Mine".to_string()], footer_progress_hex_words: vec!["0x000032dc".to_string(), "0x00003714".to_string()], entries: vec![ crate::SmpRt3105SaveNameTableEntry { index: 0, offset: 0x6ad1, text: "AutoPlant".to_string(), availability_dword: 1, availability_dword_hex: "0x00000001".to_string(), trailer_word: 1, trailer_word_hex: "0x00000001".to_string(), }, crate::SmpRt3105SaveNameTableEntry { index: 1, offset: 0x6af3, text: "Uranium Mine".to_string(), availability_dword: 0, availability_dword_hex: "0x00000000".to_string(), trailer_word: 0, trailer_word_hex: "0x00000000".to_string(), }, ], }), special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable { source_kind: "save-fixed-special-conditions-range".to_string(), table_offset: 0x0d64, table_len: 36 * 4, enabled_visible_count: 0, enabled_visible_labels: vec![], entries: vec![ crate::SmpSpecialConditionEntry { slot_index: 30, hidden: false, label_id: 3722, help_id: 3723, label: "Disable Cargo Economy".to_string(), value: 0, value_hex: "0x00000000".to_string(), }, crate::SmpSpecialConditionEntry { slot_index: 35, hidden: true, label_id: 3, help_id: 3, label: "Hidden sentinel".to_string(), value: 1, value_hex: "0x00000001".to_string(), }, ], }), event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), mechanism_family: "rt3-105-save-post-span-bridge-v1".to_string(), mechanism_confidence: "mixed".to_string(), container_profile_family: Some("rt3-105-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: 5, live_record_count: 3, live_entry_ids: vec![1, 3, 5], decoded_record_count: 0, imported_runtime_record_count: 0, records: vec![ crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 1, payload_offset: None, payload_len: None, decode_status: "unsupported_framing".to_string(), payload_family: "unsupported_framing".to_string(), trigger_kind: None, active: None, marks_collection_dirty: None, one_shot: None, compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, notes: vec!["test".to_string()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 1, live_entry_id: 3, payload_offset: None, payload_len: None, decode_status: "unsupported_framing".to_string(), payload_family: "unsupported_framing".to_string(), trigger_kind: None, active: None, marks_collection_dirty: None, one_shot: None, compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, notes: vec!["test".to_string()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 2, live_entry_id: 5, payload_offset: None, payload_len: None, decode_status: "unsupported_framing".to_string(), payload_family: "unsupported_framing".to_string(), trigger_kind: None, active: None, marks_collection_dirty: None, one_shot: None, compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, notes: vec!["test".to_string()], }, ], }), notes: vec!["packed profile recovered".to_string()], }; let import = project_save_slice_to_runtime_state_import( &save_slice, "save-import-smoke", Some("test save import".to_string()), ) .expect("save slice should project"); assert_eq!(import.import_id, "save-import-smoke"); assert_eq!( import .state .metadata .get("save_slice.map_path") .map(String::as_str), Some("Alternate USA.gmp") ); assert_eq!( import.state.save_profile.selected_year_profile_lane, Some(0x07) ); assert_eq!(import.state.save_profile.sandbox_enabled, Some(true)); assert_eq!( import.state.world_restore.selected_year_profile_lane, Some(0x07) ); assert_eq!(import.state.world_restore.sandbox_enabled, Some(true)); assert_eq!( import.state.world_restore.campaign_scenario_enabled, Some(false) ); assert_eq!( import.state.world_restore.seed_tuple_written_from_raw_lane, Some(true) ); assert_eq!( import .state .world_restore .absolute_counter_requires_shell_context, Some(true) ); assert_eq!( import .state .world_restore .absolute_counter_reconstructible_from_save, Some(false) ); assert_eq!( import .state .world_restore .disable_cargo_economy_special_condition_slot, Some(30) ); assert_eq!( import .state .world_restore .disable_cargo_economy_special_condition_reconstructible_from_save, Some(true) ); assert_eq!( import .state .world_restore .disable_cargo_economy_special_condition_write_side_grounded, Some(true) ); assert_eq!( import .state .world_restore .disable_cargo_economy_special_condition_enabled, Some(false) ); assert_eq!( import.state.world_restore.use_bio_accelerator_cars_enabled, Some(false) ); assert_eq!( import.state.world_restore.use_wartime_cargos_enabled, Some(false) ); assert_eq!( import.state.world_restore.disable_train_crashes_enabled, Some(false) ); assert_eq!( import .state .world_restore .disable_train_crashes_and_breakdowns_enabled, Some(false) ); assert_eq!( import .state .world_restore .ai_ignore_territories_at_startup_enabled, Some(false) ); assert_eq!( import .state .world_restore .absolute_counter_restore_kind .as_deref(), Some("mode-adjusted-selected-year-lane") ); assert_eq!( import .state .world_restore .absolute_counter_adjustment_context .as_deref(), Some( "editor-map-mode,shell-selected-year-adjust-policy-0x9d26-0x9d28,save-special-condition-disable-cargo-economy-slot-30" ) ); assert_eq!( import.state.save_profile.map_path.as_deref(), Some("Alternate USA.gmp") ); assert_eq!( import.state.candidate_availability.get("Uranium Mine"), Some(&0) ); assert_eq!( import.state.special_conditions.get("Disable Cargo Economy"), Some(&0) ); assert_eq!( import .state .world_flags .get("save_slice.profile_byte_0x82_nonzero"), Some(&true) ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.live_record_count), Some(3) ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.live_entry_ids.clone()), Some(vec![1, 3, 5]) ); assert!(import.state.event_runtime_records.is_empty()); } #[test] fn projects_executable_packed_records_into_runtime_and_services_follow_on() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 1, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(64), decode_status: "executable".to_string(), payload_family: "synthetic_harness".to_string(), trigger_kind: Some(7), active: Some(true), marks_collection_dirty: Some(true), one_shot: Some(false), compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: vec![], negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 1, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![ RuntimeEffect::SetWorldFlag { key: "from_packed_root".to_string(), value: true, }, RuntimeEffect::AppendEventRecord { record: Box::new(RuntimeEventRecordTemplate { record_id: 99, trigger_kind: 0x0a, active: true, marks_collection_dirty: false, one_shot: false, effects: vec![RuntimeEffect::SetSpecialCondition { label: "Imported Follow-On".to_string(), value: 1, }], }), }, ], executable_import_ready: true, notes: vec!["decoded test record".to_string()], }], }), notes: vec![], }; let mut import = project_save_slice_to_runtime_state_import( &save_slice, "packed-events-exec", Some("test packed event import".to_string()), ) .expect("save slice should project"); assert_eq!(import.state.event_runtime_records.len(), 1); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.imported_runtime_record_count), Some(1) ); let result = execute_step_command( &mut import.state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("trigger service should succeed"); assert_eq!(result.final_summary.event_runtime_record_count, 2); assert_eq!(result.final_summary.total_event_record_service_count, 2); assert_eq!(result.final_summary.total_trigger_dispatch_count, 2); assert_eq!(result.final_summary.dirty_rerun_count, 1); assert_eq!( import.state.world_flags.get("from_packed_root"), Some(&true) ); assert_eq!( import.state.special_conditions.get("Imported Follow-On"), Some(&1) ); assert_eq!(import.state.event_runtime_records[0].service_count, 1); assert_eq!(import.state.event_runtime_records[1].record_id, 99); assert_eq!(import.state.event_runtime_records[1].service_count, 1); } #[test] fn leaves_parity_only_packed_records_out_of_runtime_event_records() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(48), decode_status: "parity_only".to_string(), payload_family: "synthetic_harness".to_string(), trigger_kind: Some(7), active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, delta: 50, }], executable_import_ready: false, notes: vec!["decoded but not importable".to_string()], }], }), notes: vec![], }; let import = project_save_slice_to_runtime_state_import( &save_slice, "packed-events-parity-only", None, ) .expect("save slice should project"); assert!(import.state.event_runtime_records.is_empty()); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.decoded_record_count), Some(1) ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.imported_runtime_record_count), Some(0) ); assert_eq!( import .state .packed_event_collection .as_ref() .and_then(|summary| summary.records[0].import_outcome.as_deref()), Some("blocked_missing_company_context") ); } #[test] fn classifies_symbolic_company_target_blockers_for_standalone_import() { 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: 12, live_record_count: 3, live_entry_ids: vec![10, 11, 12], decoded_record_count: 3, imported_runtime_record_count: 0, records: vec![ synthetic_packed_record( 0, 10, RuntimeEffect::AdjustCompanyCash { target: RuntimeCompanyTarget::SelectedCompany, delta: 1, }, ), synthetic_packed_record( 1, 11, RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::HumanCompanies, delta: 2, }, ), synthetic_packed_record( 2, 12, RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::ConditionTrueCompany, delta: 3, }, ), ], }), notes: vec![], }; 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 .state .packed_event_collection .as_ref() .expect("packed event collection should be present") .records .iter() .map(|record| record.import_outcome.clone()) .collect::>(); assert_eq!( outcomes, vec![ Some("blocked_missing_selection_context".to_string()), Some("blocked_missing_company_role_context".to_string()), Some("blocked_missing_condition_context".to_string()), ] ); } #[test] fn overlays_symbolic_company_targets_into_executable_runtime_records() { let base_state = RuntimeState { companies: vec![ crate::RuntimeCompany { company_id: 1, 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), ..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: 22, live_record_count: 2, live_entry_ids: vec![21, 22], decoded_record_count: 2, imported_runtime_record_count: 0, records: vec![ synthetic_packed_record( 0, 21, RuntimeEffect::AdjustCompanyCash { target: RuntimeCompanyTarget::SelectedCompany, delta: 15, }, ), synthetic_packed_record( 1, 22, RuntimeEffect::AdjustCompanyDebt { target: RuntimeCompanyTarget::AiCompanies, delta: 4, }, ), ], }), notes: vec![], }; let mut import = project_save_slice_overlay_to_runtime_state_import( &base_state, &save_slice, "symbolic-overlay", None, ) .expect("overlay projection should succeed"); assert_eq!(import.state.event_runtime_records.len(), 2); let outcomes = import .state .packed_event_collection .as_ref() .expect("packed event collection should be present") .records .iter() .map(|record| record.import_outcome.clone()) .collect::>(); assert_eq!( outcomes, vec![Some("imported".to_string()), Some("imported".to_string())] ); execute_step_command( &mut import.state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("symbolic overlay dispatch should succeed"); assert_eq!(import.state.companies[0].current_cash, 115); assert_eq!(import.state.companies[1].debt, 24); } #[test] fn leaves_real_records_without_compact_control_blocked_missing_compact_control() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(96), decode_status: "parity_only".to_string(), payload_family: "real_packed_v1".to_string(), trigger_kind: None, active: None, marks_collection_dirty: None, one_shot: None, compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::AllCompanies, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), 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()], }], }), notes: vec![], }; let import = project_save_slice_to_runtime_state_import( &save_slice, "packed-events-structural-only", 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_missing_compact_control") ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.records[0].standalone_condition_rows.len()), Some(1) ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.records[0].grouped_effect_rows.len()), Some(1) ); } #[test] fn lowers_negative_sentinel_company_scopes_into_runtime_company_targets() { let base_state = RuntimeState { companies: vec![ crate::RuntimeCompany { company_id: 1, 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, }, crate::RuntimeCompany { company_id: 3, controller_kind: RuntimeCompanyControllerKind::Human, current_cash: 70, debt: 30, active: true, available_track_laying_capacity: None, }, ], selected_company_id: Some(3), ..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: 11, live_record_count: 5, live_entry_ids: vec![7, 8, 9, 10, 11], decoded_record_count: 5, imported_runtime_record_count: 0, records: vec![ crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::AllCompanies, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), 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()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 1, live_entry_id: 8, payload_offset: Some(0x7282), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::SelectedCompanyOnly, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::ConditionTrueCompany, value: 8, }], executable_import_ready: true, notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 2, live_entry_id: 9, payload_offset: Some(0x7302), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::AiCompaniesOnly, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::ConditionTrueCompany, value: 9, }], executable_import_ready: true, notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 3, live_entry_id: 10, payload_offset: Some(0x7382), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::HumanCompaniesOnly, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::ConditionTrueCompany, value: 10, }], executable_import_ready: true, notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], }, crate::SmpLoadedPackedEventRecordSummary { record_index: 4, live_entry_id: 11, payload_offset: Some(0x7402), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(company_negative_sentinel_scope( RuntimeCompanyConditionTestScope::Disabled, )), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), decoded_actions: vec![RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::ConditionTrueCompany, value: 11, }], executable_import_ready: true, 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, "packed-events-real-descriptor-frontier", None, ) .expect("save slice should project"); assert_eq!(import.state.event_runtime_records.len(), 4); assert_eq!( import .state .packed_event_collection .as_ref() .and_then(|summary| summary.records[0].compact_control.as_ref()) .map(|control| control.mode_byte_0x7ef), Some(6) ); let effects = import .state .event_runtime_records .iter() .map(|record| record.effects[0].clone()) .collect::>(); assert_eq!( effects, vec![ RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::AllActive, value: 7, }, RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::SelectedCompany, value: 8, }, RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::AiCompanies, value: 9, }, RuntimeEffect::SetCompanyCash { target: RuntimeCompanyTarget::HumanCompanies, value: 10, }, ] ); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| { summary .records .iter() .map(|record| record.import_outcome.clone()) .collect::>() }), Some(vec![ Some("imported".to_string()), Some("imported".to_string()), Some("imported".to_string()), Some("imported".to_string()), Some("blocked_company_condition_scope_disabled".to_string()), ]) ); } #[test] fn blocks_negative_sentinel_player_scope_until_player_runtime_exists() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(player_negative_sentinel_scope()), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), 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()], }], }), notes: vec![], }; let import = project_save_slice_to_runtime_state_import( &save_slice, "negative-sentinel-player-scope", 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_player_condition_scope") ); } #[test] fn blocks_negative_sentinel_territory_scope_until_territory_runtime_exists() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: Some(territory_negative_sentinel_scope()), grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), 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()], }], }), notes: vec![], }; let import = project_save_slice_to_runtime_state_import( &save_slice, "negative-sentinel-territory-scope", 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_territory_condition_scope") ); } #[test] fn leaves_real_records_with_unclassified_scope_blocked_unmapped_real_descriptor() { 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(133), 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(true), compact_control: Some(real_compact_control_without_symbolic_company_scope()), text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), negative_sentinel_scope: None, grouped_effect_row_counts: vec![1, 0, 0, 0], grouped_effect_rows: real_grouped_rows(), 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, "packed-events-real-descriptor-frontier", 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_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, active: true, available_track_laying_capacity: None, }], 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![], negative_sentinel_scope: None, 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: 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(), ], }], }), 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_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![], negative_sentinel_scope: None, 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![], negative_sentinel_scope: None, 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![], negative_sentinel_scope: None, 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![], negative_sentinel_scope: None, 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 { calendar: CalendarPoint { year: 1845, month_slot: 2, phase_slot: 1, tick_slot: 3, }, world_flags: BTreeMap::from([("base.only".to_string(), true)]), save_profile: RuntimeSaveProfileState::default(), world_restore: RuntimeWorldRestoreState::default(), metadata: BTreeMap::from([("base.note".to_string(), "kept".to_string())]), 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), packed_event_collection: None, event_runtime_records: vec![RuntimeEventRecord { record_id: 1, trigger_kind: 1, active: true, service_count: 0, marks_collection_dirty: false, one_shot: false, has_fired: false, effects: vec![], }], candidate_availability: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState { periodic_boundary_calls: 9, trigger_dispatch_counts: BTreeMap::new(), total_event_record_services: 4, dirty_rerun_count: 2, }, }; 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: 42, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(48), decode_status: "parity_only".to_string(), payload_family: "synthetic_harness".to_string(), trigger_kind: Some(7), active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, delta: 50, }], executable_import_ready: false, notes: vec!["needs company context".to_string()], }], }), notes: vec![], }; let mut import = project_save_slice_overlay_to_runtime_state_import( &base_state, &save_slice, "overlay-smoke", Some("overlay test".to_string()), ) .expect("overlay import should project"); assert_eq!(import.state.calendar, base_state.calendar); assert_eq!(import.state.companies, base_state.companies); assert_eq!(import.state.service_state, base_state.service_state); assert_eq!(import.state.event_runtime_records.len(), 1); assert_eq!( import .state .packed_event_collection .as_ref() .map(|summary| summary.imported_runtime_record_count), Some(1) ); assert_eq!( import .state .packed_event_collection .as_ref() .and_then(|summary| summary.records[0].import_outcome.as_deref()), Some("imported") ); assert_eq!( import .state .metadata .get("save_slice.import_projection") .map(String::as_str), Some("overlay-runtime-restore-v1") ); assert_eq!( import.state.metadata.get("base.note").map(String::as_str), Some("kept") ); assert_eq!(import.state.world_flags.get("base.only"), Some(&true)); execute_step_command( &mut import.state, &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, ) .expect("overlay-imported company-targeted record should run"); assert_eq!(import.state.companies[0].current_cash, 550); } #[test] fn loads_overlay_import_document_with_relative_paths() { let nonce = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system time should be after epoch") .as_nanos(); let fixture_dir = std::env::temp_dir().join(format!("rrt-overlay-import-{nonce}")); std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created"); let snapshot_path = fixture_dir.join("base.json"); let save_slice_path = fixture_dir.join("slice.json"); let overlay_path = fixture_dir.join("overlay.json"); let snapshot = crate::RuntimeSnapshotDocument { format_version: crate::SNAPSHOT_FORMAT_VERSION, snapshot_id: "base".to_string(), source: crate::RuntimeSnapshotSource { source_fixture_id: None, description: Some("base snapshot".to_string()), }, state: RuntimeState { calendar: CalendarPoint { year: 1835, month_slot: 1, phase_slot: 2, tick_slot: 4, }, 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: 100, debt: 0, active: true, available_track_laying_capacity: None, }], 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(), }, }; crate::save_runtime_snapshot_document(&snapshot_path, &snapshot) .expect("snapshot should save"); let save_slice_document = RuntimeSaveSliceDocument { format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION, save_slice_id: "slice".to_string(), source: RuntimeSaveSliceDocumentSource::default(), 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: 7, live_record_count: 1, live_entry_ids: vec![7], decoded_record_count: 1, imported_runtime_record_count: 0, records: vec![crate::SmpLoadedPackedEventRecordSummary { record_index: 0, live_entry_id: 7, payload_offset: Some(0x7202), payload_len: Some(48), decode_status: "parity_only".to_string(), payload_family: "synthetic_harness".to_string(), trigger_kind: Some(7), active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], negative_sentinel_scope: None, grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: vec![], decoded_actions: vec![RuntimeEffect::AdjustCompanyCash { target: crate::RuntimeCompanyTarget::Ids { ids: vec![42] }, delta: 50, }], executable_import_ready: false, notes: vec!["needs company context".to_string()], }], }), notes: vec![], }, }; save_runtime_save_slice_document(&save_slice_path, &save_slice_document) .expect("save slice should save"); let overlay = RuntimeOverlayImportDocument { format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, import_id: "overlay-relative".to_string(), source: RuntimeOverlayImportDocumentSource { description: Some("relative overlay".to_string()), notes: vec![], }, base_snapshot_path: "base.json".to_string(), save_slice_path: "slice.json".to_string(), }; save_runtime_overlay_import_document(&overlay_path, &overlay) .expect("overlay document should save"); let import = load_runtime_state_import(&overlay_path).expect("overlay runtime import should load"); assert_eq!(import.import_id, "overlay-relative"); assert_eq!(import.state.event_runtime_records.len(), 1); assert_eq!(import.state.companies[0].company_id, 42); let _ = std::fs::remove_file(snapshot_path); let _ = std::fs::remove_file(save_slice_path); let _ = std::fs::remove_file(overlay_path); let _ = std::fs::remove_dir(fixture_dir); } }