From 7abd582aea15f4d67d712503f9e3f27f7cc624e7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 19 Apr 2026 03:12:53 -0700 Subject: [PATCH] Load placed-structure triplets into save slices --- crates/rrt-cli/src/main.rs | 1 + crates/rrt-runtime/src/import.rs | 202 +++++++++++++++++++++++++++ crates/rrt-runtime/src/lib.rs | 3 +- crates/rrt-runtime/src/smp.rs | 225 +++++++++++++++++++++++++++++++ docs/rehost-queue.md | 3 + 5 files changed, 433 insertions(+), 1 deletion(-) diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 9ccf640..6e41874 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -5783,6 +5783,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec!["exported for test".to_string()], diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 199fa84..981710e 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -1439,6 +1439,39 @@ fn project_save_slice_components( ) }; + if let Some(collection) = &save_slice.placed_structure_collection { + metadata.insert( + "save_slice.placed_structure_collection_source_kind".to_string(), + collection.source_kind.clone(), + ); + metadata.insert( + "save_slice.placed_structure_collection_semantic_family".to_string(), + collection.semantic_family.clone(), + ); + metadata.insert( + "save_slice.placed_structure_collection_entry_count".to_string(), + collection.observed_entry_count.to_string(), + ); + metadata.insert( + "save_slice.placed_structure_collection_farm_growth_stage_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count() + .to_string(), + ); + metadata.insert( + "save_slice.placed_structure_collection_nondefault_status_count".to_string(), + collection + .entries + .iter() + .filter(|entry| entry.profile_status_kind != "unset") + .count() + .to_string(), + ); + } + let named_locomotive_cost = BTreeMap::new(); let all_cargo_price_override = None; let named_cargo_price_overrides = BTreeMap::new(); @@ -5471,6 +5504,42 @@ mod tests { } } + fn save_placed_structure_collection() -> crate::SmpLoadedPlacedStructureCollection { + crate::SmpLoadedPlacedStructureCollection { + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), + observed_entry_count: 2, + entries: vec![ + crate::SmpLoadedPlacedStructureEntry { + record_index: 0, + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_status_kind: "farm_growth_stage_bucket".to_string(), + farm_growth_stage_index: Some(4), + profile_companion_byte_u8: Some(0), + profile_companion_byte_hex: Some("0x00".to_string()), + }, + crate::SmpLoadedPlacedStructureEntry { + record_index: 1, + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_payload_dword: 0x00005dc1, + profile_payload_dword_hex: "0x00005dc1".to_string(), + profile_status_kind: "opaque_nondefault".to_string(), + farm_growth_stage_index: None, + profile_companion_byte_u8: Some(7), + profile_companion_byte_hex: Some("0x07".to_string()), + }, + ], + } + } + fn save_chairman_profile_table() -> crate::SmpLoadedChairmanProfileTable { crate::SmpLoadedChairmanProfileTable { source_kind: "tracked-save-slice-chairman-profile-table".to_string(), @@ -5970,6 +6039,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec![], @@ -6017,6 +6087,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec![], @@ -6246,6 +6317,7 @@ mod tests { }), company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: Some(crate::SmpLoadedSpecialConditionsTable { source_kind: "save-fixed-special-conditions-range".to_string(), table_offset: 0x0d64, @@ -6793,6 +6865,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec![], @@ -6823,6 +6896,73 @@ mod tests { ); } + #[test] + fn projects_placed_structure_collection_metadata_from_save_slice() { + 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, + named_locomotive_availability_table: None, + locomotive_catalog: None, + cargo_catalog: None, + world_issue_37_state: None, + world_economic_tuning_state: None, + world_finance_neighborhood_state: None, + world_locomotive_policy_state: None, + company_roster: None, + chairman_profile_table: None, + placed_structure_collection: Some(save_placed_structure_collection()), + special_conditions_table: None, + event_runtime_collection: None, + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "save-native-placed-structures", + None, + ) + .expect("save slice should project"); + + assert_eq!( + import + .state + .metadata + .get("save_slice.placed_structure_collection_source_kind") + .map(String::as_str), + Some("save-placed-structure-record-triplets") + ); + assert_eq!( + import + .state + .metadata + .get("save_slice.placed_structure_collection_entry_count") + .map(String::as_str), + Some("2") + ); + assert_eq!( + import + .state + .metadata + .get("save_slice.placed_structure_collection_farm_growth_stage_count") + .map(String::as_str), + Some("1") + ); + assert_eq!( + import + .state + .metadata + .get("save_slice.placed_structure_collection_nondefault_status_count") + .map(String::as_str), + Some("2") + ); + } + #[test] fn overlay_replaces_base_company_and_chairman_context_from_save_slice() { let base_state = RuntimeState { @@ -6881,6 +7021,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec![], @@ -7052,6 +7193,7 @@ mod tests { selected_chairman_profile_id: Some(1), entries: Vec::new(), }), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: None, notes: vec![], @@ -7101,6 +7243,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7233,6 +7376,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7343,6 +7487,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7479,6 +7624,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7576,6 +7722,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7741,6 +7888,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -7996,6 +8144,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8086,6 +8235,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8200,6 +8350,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8287,6 +8438,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8377,6 +8529,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8480,6 +8633,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8599,6 +8753,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8689,6 +8844,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8856,6 +9012,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -8968,6 +9125,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9057,6 +9215,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9146,6 +9305,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9309,6 +9469,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9417,6 +9578,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9504,6 +9666,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9601,6 +9764,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9707,6 +9871,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9821,6 +9986,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -9924,6 +10090,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10013,6 +10180,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10172,6 +10340,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10341,6 +10510,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10458,6 +10628,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10556,6 +10727,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10681,6 +10853,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10800,6 +10973,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -10909,6 +11083,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11014,6 +11189,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11133,6 +11309,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11237,6 +11414,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11323,6 +11501,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11414,6 +11593,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11510,6 +11690,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11606,6 +11787,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11718,6 +11900,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11821,6 +12004,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -11971,6 +12155,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -12120,6 +12305,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -12660,6 +12846,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -12854,6 +13041,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -12993,6 +13181,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13131,6 +13320,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: Some(save_company_roster()), chairman_profile_table: Some(save_chairman_profile_table()), + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13270,6 +13460,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13393,6 +13584,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13591,6 +13783,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13697,6 +13890,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13805,6 +13999,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -13984,6 +14179,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14146,6 +14342,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14249,6 +14446,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14385,6 +14583,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14509,6 +14708,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14707,6 +14907,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), @@ -14915,6 +15116,7 @@ mod tests { world_locomotive_policy_state: None, company_roster: None, chairman_profile_table: None, + placed_structure_collection: None, special_conditions_table: None, event_runtime_collection: Some(crate::SmpLoadedEventRuntimeCollectionSummary { source_kind: "packed-event-runtime-collection".to_string(), diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index e6829d8..7d0ee2d 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -109,7 +109,8 @@ pub use smp::{ SmpLoadedNamedLocomotiveAvailabilityTable, SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventNegativeSentinelScopeSummary, SmpLoadedPackedEventRecordSummary, - SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedSaveSlice, + SmpLoadedPackedEventTextBandSummary, SmpLoadedPlacedStructureCollection, + SmpLoadedPlacedStructureEntry, SmpLoadedProfile, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLoadedWorldEconomicTuningState, SmpLoadedWorldFinanceNeighborhoodState, SmpLoadedWorldIssue37State, SmpLocomotivePolicyFieldObservation, SmpLocomotivePolicyFloatAlignmentCandidate, diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 28f76fe..d86e156 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -1928,6 +1928,33 @@ pub struct SmpSavePlacedStructureRecordTripletProbe { pub evidence: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPlacedStructureEntry { + pub record_index: usize, + pub primary_name: String, + pub secondary_name: String, + pub policy_trailing_word: u16, + pub policy_trailing_word_hex: String, + pub profile_payload_dword: u32, + pub profile_payload_dword_hex: String, + pub profile_status_kind: String, + #[serde(default)] + pub farm_growth_stage_index: Option, + #[serde(default)] + pub profile_companion_byte_u8: Option, + #[serde(default)] + pub profile_companion_byte_hex: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPlacedStructureCollection { + pub source_kind: String, + pub semantic_family: String, + pub observed_entry_count: usize, + #[serde(default)] + pub entries: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SmpSavePlacedStructureDynamicSideBufferProbe { pub profile_family: String, @@ -4038,6 +4065,8 @@ pub struct SmpLoadedSaveSlice { pub company_roster: Option, #[serde(default)] pub chairman_profile_table: Option, + #[serde(default)] + pub placed_structure_collection: Option, pub special_conditions_table: Option, pub event_runtime_collection: Option, pub notes: Vec, @@ -6830,6 +6859,33 @@ pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport { inspect_bundle_bytes(bytes, None) } +fn derive_loaded_placed_structure_collection_from_probe( + probe: &SmpSavePlacedStructureRecordTripletProbe, +) -> SmpLoadedPlacedStructureCollection { + SmpLoadedPlacedStructureCollection { + source_kind: probe.source_kind.clone(), + semantic_family: "scenario-save-placed-structure-triplet-collection".to_string(), + observed_entry_count: probe.record_count, + entries: probe + .entries + .iter() + .map(|entry| SmpLoadedPlacedStructureEntry { + record_index: entry.record_index, + primary_name: entry.primary_name.clone(), + secondary_name: entry.secondary_name.clone(), + policy_trailing_word: entry.policy_trailing_word, + policy_trailing_word_hex: entry.policy_trailing_word_hex.clone(), + profile_payload_dword: entry.profile_payload_dword, + profile_payload_dword_hex: entry.profile_payload_dword_hex.clone(), + profile_status_kind: entry.profile_status_kind.clone(), + farm_growth_stage_index: entry.farm_growth_stage_index, + profile_companion_byte_u8: entry.profile_companion_byte_u8, + profile_companion_byte_hex: entry.profile_companion_byte_hex.clone(), + }) + .collect(), + } +} + pub fn load_save_slice_file(path: &Path) -> Result> { let inspection = inspect_smp_file(path)?; load_save_slice_from_report(&inspection) @@ -6974,6 +7030,10 @@ pub fn load_save_slice_from_report( ) }) }); + let placed_structure_collection = report + .save_placed_structure_record_triplet_probe + .as_ref() + .map(derive_loaded_placed_structure_collection_from_probe); let special_conditions_table = report .special_conditions_probe @@ -7144,6 +7204,22 @@ pub fn load_save_slice_from_report( probe.entries.first().map(|entry| entry.profile_status_kind.as_str()) )); } + if let Some(collection) = &placed_structure_collection { + let farm_growth_stage_count = collection + .entries + .iter() + .filter(|entry| entry.farm_growth_stage_index.is_some()) + .count(); + let opaque_status_count = collection + .entries + .iter() + .filter(|entry| entry.profile_status_kind != "unset") + .count(); + notes.push(format!( + "Save-slice projection now carries {} loaded placed-structure triplet rows as first-class context, with {} farm growth-stage rows and {} non-default footer-status rows.", + collection.observed_entry_count, farm_growth_stage_count, opaque_status_count + )); + } if let Some(probe) = &placed_structure_dynamic_side_buffer_probe { let dominant_pattern = probe.compact_prefix_pattern_summaries.first(); let payload_envelope_summary = probe.payload_envelope_summary.as_ref(); @@ -7240,6 +7316,7 @@ pub fn load_save_slice_from_report( world_locomotive_policy_state, company_roster, chairman_profile_table, + placed_structure_collection, special_conditions_table, event_runtime_collection: report.event_runtime_collection_summary.clone(), notes, @@ -25240,6 +25317,154 @@ mod tests { ); } + #[test] + fn loads_placed_structure_collection_from_report() { + let mut report = inspect_smp_bytes(&[]); + let classic_probe = SmpClassicRehydrateProfileProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + progress_32dc_offset: 0x76e8, + progress_3714_offset: 0x76ec, + progress_3715_offset: 0x77f8, + packed_profile_offset: 0x76f0, + packed_profile_len: 0x108, + packed_profile_len_hex: "0x108".to_string(), + packed_profile_block: SmpClassicPackedProfileBlock { + relative_len: 0x108, + relative_len_hex: "0x108".to_string(), + leading_word_0: 3, + leading_word_0_hex: "0x00000003".to_string(), + trailing_zero_word_count_after_leading_word: 3, + map_path_offset: 0x13, + map_path: Some("British Isles.gmp".to_string()), + display_name_offset: 0x46, + display_name: Some("British Isles".to_string()), + profile_byte_0x77: 0, + profile_byte_0x77_hex: "0x00".to_string(), + profile_byte_0x82: 0, + profile_byte_0x82_hex: "0x00".to_string(), + profile_byte_0x97: 0, + profile_byte_0x97_hex: "0x00".to_string(), + profile_byte_0xc5: 0, + profile_byte_0xc5_hex: "0x00".to_string(), + stable_nonzero_words: vec![], + }, + ascii_runs: vec![], + }; + report.classic_rehydrate_profile_probe = Some(classic_probe.clone()); + report.save_load_summary = build_save_load_summary( + Some("gms"), + Some(&SmpContainerProfile { + profile_family: "rt3-classic-save-container-v1".to_string(), + profile_evidence: vec![], + is_known_profile: true, + }), + None, + None, + Some(&classic_probe), + None, + None, + ); + report.save_placed_structure_record_triplet_probe = + Some(SmpSavePlacedStructureRecordTripletProbe { + profile_family: "rt3-classic-save-container-v1".to_string(), + source_kind: "save-placed-structure-record-triplets".to_string(), + semantic_family: "scenario-save-placed-structure-record-triplets".to_string(), + records_tag_offset: 0x3600, + close_tag_offset: 0x3800, + record_count: 2, + entries: vec![ + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 0, + primary_name: "FarmCorn".to_string(), + secondary_name: "FarmSet".to_string(), + name_tag_relative_offset: 0, + policy_tag_relative_offset: 0x10, + profile_tag_relative_offset: 0x2e, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x10, + policy_f32_lane_0: 1.0, + policy_f32_lane_1: 2.0, + policy_f32_lane_2: 3.0, + policy_f32_lane_3: 4.0, + policy_f32_lane_4: 5.0, + policy_reserved_dword: 0, + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_open_marker: 0x00005dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "FarmCorn".to_string(), + profile_repeated_secondary_name: "FarmSet".to_string(), + profile_footer_relative_offset: 0x08, + profile_footer_relative_offset_hex: "0x8".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x00".to_string()], + profile_companion_byte_u8: Some(0), + profile_companion_byte_hex: Some("0x00".to_string()), + profile_payload_dword: 0, + profile_payload_dword_hex: "0x00000000".to_string(), + profile_sentinel_i32: 4, + profile_status_kind: "farm_growth_stage_bucket".to_string(), + farm_growth_stage_index: Some(4), + profile_close_marker: 0x00005dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + SmpSavePlacedStructureRecordTripletEntryProbe { + record_index: 1, + primary_name: "StationA".to_string(), + secondary_name: "StationSetA".to_string(), + name_tag_relative_offset: 0x40, + policy_tag_relative_offset: 0x50, + profile_tag_relative_offset: 0x6e, + policy_chunk_len: 0x1a, + profile_chunk_len: 0x10, + policy_f32_lane_0: 0.0, + policy_f32_lane_1: 0.0, + policy_f32_lane_2: 0.0, + policy_f32_lane_3: 0.0, + policy_f32_lane_4: 0.0, + policy_reserved_dword: 0, + policy_trailing_word: 1, + policy_trailing_word_hex: "0x0001".to_string(), + profile_open_marker: 0x00005dc1, + profile_open_marker_hex: "0x00005dc1".to_string(), + profile_repeated_primary_name: "StationA".to_string(), + profile_repeated_secondary_name: "StationSetA".to_string(), + profile_footer_relative_offset: 0x08, + profile_footer_relative_offset_hex: "0x8".to_string(), + profile_pre_footer_padding_len: 1, + profile_pre_footer_padding_hex_bytes: vec!["0x07".to_string()], + profile_companion_byte_u8: Some(7), + profile_companion_byte_hex: Some("0x07".to_string()), + profile_payload_dword: 0x00005dc1, + profile_payload_dword_hex: "0x00005dc1".to_string(), + profile_sentinel_i32: 0, + profile_status_kind: "opaque_nondefault".to_string(), + farm_growth_stage_index: None, + profile_close_marker: 0x00005dc2, + profile_close_marker_hex: "0x00005dc2".to_string(), + }, + ], + evidence: vec![], + }); + + let slice = load_save_slice_from_report(&report).expect("classic save slice"); + let collection = slice + .placed_structure_collection + .expect("placed structure collection should project"); + assert_eq!( + collection.source_kind, + "save-placed-structure-record-triplets" + ); + assert_eq!(collection.observed_entry_count, 2); + assert_eq!(collection.entries[0].primary_name, "FarmCorn"); + assert_eq!(collection.entries[0].farm_growth_stage_index, Some(4)); + assert_eq!(collection.entries[1].profile_companion_byte_u8, Some(7)); + assert!(slice.notes.iter().any(|line| { + line.contains("placed-structure triplet rows as first-class context") + && line.contains("2") + })); + } + #[test] fn loads_rt3_105_save_slice_from_report() { let mut report = inspect_smp_bytes(&[]); diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 3135be5..7a12c89 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -77,6 +77,9 @@ Working rule: to drive the proximity scan - whether the acquisition branch can be rehosted as a shellless sibling beside the already grounded annual-finance helper + - the save-side `0x36b1/0x36b2/0x36b3` triplet seam is now also loaded into the checked-in + save-slice model as first-class `placed_structure_collection` context, carrying stem pairs plus + grounded footer/policy status lanes instead of remaining inspection-only evidence - Direct disassembly now narrows that acquisition strip further: - `0x004014b0` scans the live placed-structure collection at `0x0062b26c` - `0x0041f6e0 -> 0x0042b2d0` is the center-cell token gate over the current region