From a3f9a73766d38eecd19e5863c3480623edc94382 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 Apr 2026 22:19:09 -0700 Subject: [PATCH] Recover whole-game packed event descriptors --- README.md | 9 +- crates/rrt-runtime/src/import.rs | 318 ++++++++++++++++++ crates/rrt-runtime/src/smp.rs | 299 +++++++++++++++- docs/README.md | 4 + docs/runtime-rehost-plan.md | 10 +- ...vent-world-condition-gated-save-slice.json | 5 +- .../packed-event-world-parity-save-slice.json | 5 +- ...nt-world-special-condition-save-slice.json | 7 +- 8 files changed, 634 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 217a0b9..cf10d60 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,12 @@ train roster and opaque economic-status lane needed for real descriptors `8` `Ec `Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights rather than a territory-owned policy bit. Whole-game ordinary-condition execution now exists too: special-condition thresholds, candidate-availability thresholds, and economic-status-code -thresholds now gate imported runtime records through the same service path, with explicit unmapped -world-condition and world-descriptor frontier buckets where current checked-in metadata still -stops. Shell purchase-flow and selected-profile parity remain out of scope. Mixed +thresholds now gate imported runtime records through the same service path, and checked-in +whole-game descriptor metadata now drives the first real world-side effect batch too: +special-condition and candidate-availability setters import natively while world-flag rows remain +parity-only until keyed mapping is grounded. Explicit unmapped world-condition and +world-descriptor frontier buckets remain where current checked-in metadata still stops. Shell +purchase-flow and selected-profile parity remain out of scope. Mixed supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and integration tooling, but it is no longer the main execution milestone. diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index c68b82d..39af7a9 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -2739,6 +2739,85 @@ mod tests { } } + fn real_special_condition_row( + value: i32, + ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 108, + descriptor_label: Some("Use Wartime Cargos".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("special_condition_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 Use Wartime Cargos to {value}")), + locomotive_name: None, + notes: vec![], + } + } + + fn real_candidate_availability_row( + value: i32, + ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 109, + descriptor_label: Some("Turbo Diesel Availability".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("candidate_availability_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 Turbo Diesel Availability to {value}")), + locomotive_name: None, + notes: vec![], + } + } + + fn real_world_flag_row(enabled: bool) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { + crate::SmpLoadedPackedEventGroupedEffectRowSummary { + group_index: 0, + row_index: 0, + descriptor_id: 110, + descriptor_label: Some("Disable Stock Buying and Selling".to_string()), + target_mask_bits: Some(0x08), + parameter_family: Some("world_flag_toggle".to_string()), + opcode: 0, + 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 Disable Stock Buying and Selling to {}", + if enabled { "TRUE" } else { "FALSE" } + )), + locomotive_name: None, + notes: vec![], + } + } + fn real_confiscate_all_row( enabled: bool, ) -> crate::SmpLoadedPackedEventGroupedEffectRowSummary { @@ -5117,6 +5196,245 @@ mod tests { assert_eq!(import.state.world_restore.economic_status_code, Some(2)); } + #[test] + fn overlays_real_special_condition_descriptor_into_executable_runtime_record() { + let base_state = 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: 21, + live_record_count: 1, + live_entry_ids: vec![21], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 21, + 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(real_compact_control()), + 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_special_condition_row(1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetSpecialCondition { + label: "Use Wartime Cargos".to_string(), + value: 1, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "real-special-condition-overlay", + None, + ) + .expect("overlay import should project"); + + execute_step_command( + &mut import.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real special-condition descriptor should execute"); + + assert_eq!( + import.state.special_conditions.get("Use Wartime Cargos"), + Some(&1) + ); + } + + #[test] + fn overlays_real_candidate_availability_descriptor_into_executable_runtime_record() { + let base_state = 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: 1, + live_entry_ids: vec![22], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 22, + 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(real_compact_control()), + 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_candidate_availability_row(1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![RuntimeEffect::SetCandidateAvailability { + name: "Turbo Diesel".to_string(), + value: 1, + }], + executable_import_ready: true, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "whole-game descriptor labels and parameter families come from the checked-in effect table".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "real-candidate-availability-overlay", + None, + ) + .expect("overlay import should project"); + + execute_step_command( + &mut import.state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("real candidate-availability descriptor should execute"); + + assert_eq!( + import.state.candidate_availability.get("Turbo Diesel"), + Some(&1) + ); + } + + #[test] + fn leaves_real_world_flag_descriptor_parity_only_until_key_mapping_is_grounded() { + let base_state = 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: 23, + live_record_count: 1, + live_entry_ids: vec![23], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 23, + 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(real_compact_control()), + 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_world_flag_row(true)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "decoded from grounded real 0x4e9a row framing".to_string(), + "world-flag descriptor identity is checked in, but keyed runtime mapping remains parity-only".to_string(), + ], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "real-world-flag-parity-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_world_descriptor") + ); + } + #[test] fn overlays_real_confiscate_all_descriptor_into_executable_runtime_record() { let base_state = RuntimeState { diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 8d453ec..76b45c0 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -127,7 +127,7 @@ struct RealGroupedEffectDescriptorMetadata { executable_in_runtime: bool, } -const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 8] = [ +const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetadata; 11] = [ RealGroupedEffectDescriptorMetadata { descriptor_id: 1, label: "Player Cash", @@ -156,6 +156,27 @@ const REAL_GROUPED_EFFECT_DESCRIPTOR_METADATA: [RealGroupedEffectDescriptorMetad parameter_family: "whole_game_state_enum", executable_in_runtime: true, }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 108, + label: "Use Wartime Cargos", + target_mask_bits: 0x08, + parameter_family: "special_condition_scalar", + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 109, + label: "Turbo Diesel Availability", + target_mask_bits: 0x08, + parameter_family: "candidate_availability_scalar", + executable_in_runtime: true, + }, + RealGroupedEffectDescriptorMetadata { + descriptor_id: 110, + label: "Disable Stock Buying and Selling", + target_mask_bits: 0x08, + parameter_family: "world_flag_toggle", + executable_in_runtime: false, + }, RealGroupedEffectDescriptorMetadata { descriptor_id: 9, label: "Confiscate All", @@ -2421,17 +2442,8 @@ fn parse_real_grouped_effect_row_summary( let value_byte_0x12 = read_u8_at(row_bytes, 0x12)?; let value_word_0x14 = read_u16_at(row_bytes, 0x14)?; let value_word_0x16 = read_u16_at(row_bytes, 0x16)?; - let row_shape = classify_real_grouped_effect_row_shape( - opcode, - raw_scalar_value, - value_byte_0x11, - value_byte_0x12, - value_word_0x14, - value_word_0x16, - ) - .to_string(); let descriptor_metadata = real_grouped_effect_descriptor_metadata(descriptor_id); - let semantic_family = classify_real_grouped_effect_semantic_family( + let mut row_shape = classify_real_grouped_effect_row_shape( opcode, raw_scalar_value, value_byte_0x11, @@ -2440,6 +2452,28 @@ fn parse_real_grouped_effect_row_summary( value_word_0x16, ) .to_string(); + let mut semantic_family = classify_real_grouped_effect_semantic_family( + opcode, + raw_scalar_value, + value_byte_0x11, + value_byte_0x12, + value_word_0x14, + value_word_0x16, + ) + .to_string(); + if descriptor_metadata.is_some_and(|metadata| { + matches!( + metadata.parameter_family, + "special_condition_scalar" | "candidate_availability_scalar" + ) && opcode == 3 + && value_byte_0x11 == 0 + && value_byte_0x12 == 0 + && value_word_0x14 == 0 + && value_word_0x16 == 0 + }) { + row_shape = "scalar_assignment".to_string(); + semantic_family = "scalar_assignment".to_string(); + } let mut notes = Vec::new(); if locomotive_name.is_some() { @@ -2606,6 +2640,13 @@ fn build_real_grouped_effect_semantic_preview( } } +fn runtime_candidate_availability_name(label: &str) -> String { + label + .strip_suffix(" Availability") + .unwrap_or(label) + .to_string() +} + fn decode_real_grouped_effect_actions( grouped_effect_rows: &[SmpLoadedPackedEventGroupedEffectRowSummary], compact_control: &SmpLoadedPackedEventCompactControlSummary, @@ -2680,6 +2721,26 @@ fn decode_real_grouped_effect_action( }); } + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 108 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetSpecialCondition { + label: descriptor_metadata.label.to_string(), + value: row.raw_scalar_value as u32, + }); + } + + if descriptor_metadata.executable_in_runtime + && descriptor_metadata.descriptor_id == 109 + && row.row_shape == "scalar_assignment" + { + return Some(RuntimeEffect::SetCandidateAvailability { + name: runtime_candidate_availability_name(descriptor_metadata.label), + value: row.raw_scalar_value as u32, + }); + } + if descriptor_metadata.executable_in_runtime && descriptor_metadata.descriptor_id == 9 && row.row_shape == "bool_toggle" @@ -8251,6 +8312,222 @@ mod tests { ); } + #[test] + fn decodes_real_special_condition_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 108, + opcode: 3, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Use Wartime Cargos") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("special_condition_scalar") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetSpecialCondition { + label: "Use Wartime Cargos".to_string(), + value: 1, + }] + ); + assert!(summary.records[0].executable_import_ready); + } + + #[test] + fn decodes_real_candidate_availability_descriptor_from_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 109, + opcode: 3, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Turbo Diesel Availability") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("candidate_availability_scalar") + ); + assert_eq!( + summary.records[0].decoded_actions, + vec![RuntimeEffect::SetCandidateAvailability { + name: "Turbo Diesel".to_string(), + value: 1, + }] + ); + assert!(summary.records[0].executable_import_ready); + } + + #[test] + fn keeps_real_world_flag_descriptor_parity_only_with_checked_in_metadata() { + let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { + descriptor_id: 110, + opcode: 0, + raw_scalar_value: 1, + value_byte_0x09: 0, + value_dword_0x0d: 0, + value_byte_0x11: 0, + value_byte_0x12: 0, + value_word_0x14: 0, + value_word_0x16: 0, + locomotive_name: None, + }); + let group0_rows = vec![grouped_row]; + let record_body = build_real_event_record( + [b"World", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [1, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[], + [&group0_rows, &[], &[], &[]], + ); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_METADATA_TAG.to_le_bytes()); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_PACKED_STATE_VERSION.to_le_bytes()); + let header_words = [1u32, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for word in header_words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(&[0x00, 0x00]); + bytes.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_RECORDS_TAG.to_le_bytes()); + bytes.extend_from_slice(&record_body); + bytes.extend_from_slice(&EVENT_RUNTIME_COLLECTION_CLOSE_TAG.to_le_bytes()); + + let report = inspect_smp_bytes(&bytes); + let summary = report + .event_runtime_collection_summary + .as_ref() + .expect("event runtime collection summary should parse"); + + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .descriptor_label + .as_deref(), + Some("Disable Stock Buying and Selling") + ); + assert_eq!( + summary.records[0].grouped_effect_rows[0] + .parameter_family + .as_deref(), + Some("world_flag_toggle") + ); + assert!(summary.records[0].decoded_actions.is_empty()); + assert!(!summary.records[0].executable_import_ready); + } + #[test] fn decodes_negative_sentinel_scope_modifiers_and_territory_marker() { for (value, expected) in [ diff --git a/docs/README.md b/docs/README.md index 6c527b2..54b358d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,6 +101,10 @@ The highest-value next passes are now: candidate-availability thresholds, and economic-status-code thresholds now gate imported runtime records, and the packed-event frontier now reports explicit unmapped world-condition and world-descriptor buckets +- the first real whole-game grouped-descriptor batch is now metadata-driven too: checked-in + descriptor metadata covers special-condition and candidate-availability setters, while the + current world-flag family stays parity-only until keyed flag identity is grounded well enough + for execution - keep in mind that the current local `.gms` corpus still exports with no packed event collection, so real descriptor mapping needs to stay plumbing-first until better captures exist - use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index cc2c914..b0ea1af 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -52,11 +52,17 @@ Implemented today: through the same service path, and whole-game parity frontiers now report explicit unmapped world-condition and world-descriptor buckets rather than falling back to the generic ordinary or descriptor counts +- checked-in whole-game grouped-descriptor metadata now drives the first real world-side effect + batch too: real special-condition and candidate-availability setter rows now decode and import + through the ordinary runtime path, while world-flag rows remain parity-only until keyed flag + identity is grounded strongly enough for execution That means the next implementation work is breadth, not bootstrap. The recommended next slice is broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, -whole-game, train, player, and numeric-threshold batches, plus richer runtime ownership only where -a later descriptor or condition family needs more than the current event-owned roster. +whole-game, train, player, and numeric-threshold batches, with the whole-game frontier now +centered on still-unmapped world-flag families and any later state families that need stronger +checked-in descriptor or key recovery. Richer runtime ownership should still be added only where a +later descriptor or condition family needs more than the current event-owned roster. ## Why This Boundary diff --git a/fixtures/runtime/packed-event-world-condition-gated-save-slice.json b/fixtures/runtime/packed-event-world-condition-gated-save-slice.json index 2316f5c..95879b9 100644 --- a/fixtures/runtime/packed-event-world-condition-gated-save-slice.json +++ b/fixtures/runtime/packed-event-world-condition-gated-save-slice.json @@ -7,7 +7,8 @@ "original_save_sha256": "world-condition-gated-sample-sha256", "notes": [ "tracked as JSON save-slice document rather than raw .smp", - "proves whole-game ordinary conditions gate imported runtime effects" + "proves whole-game ordinary conditions gate imported runtime effects", + "whole-game grouped descriptor ids now line up with the checked-in metadata table" ] }, "save_slice": { @@ -253,7 +254,7 @@ "semantic_preview": "Set Turbo Diesel Availability to 1", "locomotive_name": null, "notes": [ - "tracked whole-game grouped-effect sample" + "checked-in whole-game grouped-effect sample" ] } ], diff --git a/fixtures/runtime/packed-event-world-parity-save-slice.json b/fixtures/runtime/packed-event-world-parity-save-slice.json index bf1efa1..0622adb 100644 --- a/fixtures/runtime/packed-event-world-parity-save-slice.json +++ b/fixtures/runtime/packed-event-world-parity-save-slice.json @@ -7,7 +7,8 @@ "original_save_sha256": "world-parity-sample-sha256", "notes": [ "tracked as JSON save-slice document rather than raw .smp", - "keeps one unmapped world descriptor and one unmapped world condition explicit" + "keeps one unmapped world descriptor and one unmapped world condition explicit", + "whole-game world-flag descriptor identity is checked in, but keyed runtime mapping remains parity-only" ] }, "save_slice": { @@ -83,7 +84,7 @@ "semantic_preview": "Set Disable Stock Buying and Selling to TRUE", "locomotive_name": null, "notes": [ - "recovered whole-game descriptor family without a checked-in executable mapping yet" + "checked-in whole-game descriptor family without a grounded executable key mapping yet" ] } ], diff --git a/fixtures/runtime/packed-event-world-special-condition-save-slice.json b/fixtures/runtime/packed-event-world-special-condition-save-slice.json index a9af772..ef0c845 100644 --- a/fixtures/runtime/packed-event-world-special-condition-save-slice.json +++ b/fixtures/runtime/packed-event-world-special-condition-save-slice.json @@ -7,7 +7,8 @@ "original_save_sha256": "world-special-condition-sample-sha256", "notes": [ "tracked as JSON save-slice document rather than raw .smp", - "proves whole-game special-condition effects import through the ordinary runtime path" + "proves whole-game special-condition effects import through the ordinary runtime path", + "grouped descriptor identity now comes from checked-in whole-game metadata rather than fixture-only placeholder handling" ] }, "save_slice": { @@ -83,7 +84,7 @@ "semantic_preview": "Set Use Wartime Cargos to 1", "locomotive_name": null, "notes": [ - "tracked world-side descriptor sample" + "checked-in whole-game descriptor metadata sample" ] } ], @@ -97,7 +98,7 @@ ], "executable_import_ready": true, "notes": [ - "tracked whole-game grouped-effect import sample" + "checked-in whole-game grouped-effect import sample" ] } ]