diff --git a/README.md b/README.md index f6c617e..3c21ee8 100644 --- a/README.md +++ b/README.md @@ -53,16 +53,19 @@ executable too: descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.` through checked-in metadata into keyed `world_flags`, while the wider locomotive availability/cost scalar bands are now split more cleanly: the boolean `0/1` availability subset can import through an overlay-backed `RuntimeState.locomotive_catalog` into -`RuntimeState.named_locomotive_availability`, while non-boolean availability payloads plus the -locomotive-cost/cargo-production/territory-access-cost families now surface as recovered, -metadata-rich parity rows with checked-in slot labels and locomotive ids where grounded, but they -still remain non-executable. -The runtime still carries the save-owned named locomotive availability table directly too: +`RuntimeState.named_locomotive_availability`, while non-boolean availability payloads still remain +parity-only. The runtime still carries the save-owned named locomotive availability table directly +too: checked-in save-slice documents can populate `RuntimeState.named_locomotive_availability`, and imported runtime effects can mutate that map through the ordinary event-service path without -needing full Trainbuy or live-locomotive parity. Explicit unmapped world-condition and -world-descriptor frontier buckets still remain where current checked-in metadata stops. Shell -purchase-flow and selected-profile parity remain out of scope. Mixed supported/unsupported real +needing full Trainbuy or live-locomotive parity. A parallel event-owned named locomotive cost map +now exists too: recovered locomotive-cost descriptors from bands `352..451` and `475..500` can +import through the same overlay-backed locomotive catalog into +`RuntimeState.named_locomotive_cost`, while cargo-production and territory-access-cost rows remain +metadata-rich parity-only families until a later slice grounds an honest landing surface. Explicit +unmapped world-condition and world-descriptor frontier buckets still remain where current +checked-in metadata stops. Shell purchase-flow, Trainbuy refresh, cached locomotive-rating +recomputation, 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-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 8602605..21a5b59 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4463,6 +4463,8 @@ mod tests { let overlay_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json", ); + let overlay_locomotive_cost_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json"); let scalar_band_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json", ); @@ -4490,6 +4492,8 @@ mod tests { ); run_runtime_summarize_fixture(&overlay_locomotive_fixture) .expect("overlay-backed locomotive availability fixture should summarize"); + run_runtime_summarize_fixture(&overlay_locomotive_cost_fixture) + .expect("overlay-backed locomotive cost fixture should summarize"); run_runtime_summarize_fixture(&scalar_band_parity_fixture) .expect("save-slice-backed recovered scalar-band parity fixture should summarize"); } diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 393995b..08cc132 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -185,6 +185,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }, @@ -357,6 +358,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }, diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index cc5f764..4d8c978 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -158,6 +158,8 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub zero_named_locomotive_availability_count: Option, #[serde(default)] + pub named_locomotive_cost_count: Option, + #[serde(default)] pub special_condition_count: Option, #[serde(default)] pub enabled_special_condition_count: Option, @@ -775,6 +777,14 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.named_locomotive_cost_count { + if actual.named_locomotive_cost_count != count { + mismatches.push(format!( + "named_locomotive_cost_count mismatch: expected {count}, got {}", + actual.named_locomotive_cost_count + )); + } + } if let Some(count) = self.special_condition_count { if actual.special_condition_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 4abb1e2..c5faf2f 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -95,6 +95,7 @@ struct SaveSliceProjection { event_runtime_records: Vec, candidate_availability: BTreeMap, named_locomotive_availability: BTreeMap, + named_locomotive_cost: BTreeMap, special_conditions: BTreeMap, } @@ -249,6 +250,7 @@ pub fn project_save_slice_to_runtime_state_import( event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, named_locomotive_availability: projection.named_locomotive_availability, + named_locomotive_cost: projection.named_locomotive_cost, special_conditions: projection.special_conditions, service_state: RuntimeServiceState::default(), }; @@ -308,6 +310,7 @@ pub fn project_save_slice_overlay_to_runtime_state_import( event_runtime_records: projection.event_runtime_records, candidate_availability: projection.candidate_availability, named_locomotive_availability: projection.named_locomotive_availability, + named_locomotive_cost: base_state.named_locomotive_cost.clone(), special_conditions: projection.special_conditions, service_state: base_state.service_state.clone(), }; @@ -636,6 +639,8 @@ fn project_save_slice_components( } } + let named_locomotive_cost = BTreeMap::new(); + for (index, note) in save_slice.notes.iter().enumerate() { metadata.insert(format!("save_slice.note.{index}"), note.clone()); } @@ -649,6 +654,7 @@ fn project_save_slice_components( event_runtime_records, candidate_availability, named_locomotive_availability, + named_locomotive_cost, special_conditions, }) } @@ -972,6 +978,10 @@ fn lower_contextual_real_grouped_effects( let mut effects = Vec::with_capacity(record.grouped_effect_rows.len()); for row in &record.grouped_effect_rows { + if let Some(effect) = lower_contextual_locomotive_cost_effect(row, company_context)? { + effects.push(effect); + continue; + } if let Some(effect) = lower_contextual_locomotive_availability_effect(row, company_context)? { effects.push(effect); @@ -1022,6 +1032,33 @@ fn lower_contextual_locomotive_availability_effect( })) } +fn lower_contextual_locomotive_cost_effect( + row: &SmpLoadedPackedEventGroupedEffectRowSummary, + company_context: &ImportRuntimeContext, +) -> Result, ImportBlocker> { + if row.parameter_family.as_deref() != Some("locomotive_cost_scalar") { + return Ok(None); + } + if row.row_shape != "scalar_assignment" { + return Ok(None); + } + let value = u32::try_from(row.raw_scalar_value).ok(); + let Some(value) = value else { + return Ok(None); + }; + let Some(locomotive_id) = row.recovered_locomotive_id else { + return Ok(None); + }; + let Some(name) = company_context + .locomotive_catalog_names_by_id + .get(&locomotive_id) + .cloned() + else { + return Err(ImportBlocker::MissingLocomotiveCatalogContext); + }; + Ok(Some(RuntimeEffect::SetNamedLocomotiveCost { name, value })) +} + fn packed_record_condition_scope_import_blocker( record: &SmpLoadedPackedEventRecordSummary, company_context: &ImportRuntimeContext, @@ -1251,6 +1288,12 @@ fn lower_condition_targets_in_effect( value: *value, } } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + RuntimeEffect::SetNamedLocomotiveCost { + name: name.clone(), + value: *value, + } + } RuntimeEffect::SetSpecialCondition { label, value } => RuntimeEffect::SetSpecialCondition { label: label.clone(), value: *value, @@ -1686,6 +1729,12 @@ fn smp_runtime_effect_to_runtime_effect( value: *value, }) } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + Ok(RuntimeEffect::SetNamedLocomotiveCost { + name: name.clone(), + value: *value, + }) + } RuntimeEffect::SetSpecialCondition { label, value } => { Ok(RuntimeEffect::SetSpecialCondition { label: label.clone(), @@ -2247,6 +2296,7 @@ fn runtime_effect_uses_condition_true_company(effect: &RuntimeEffect) -> bool { | RuntimeEffect::DeactivatePlayer { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } | RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } @@ -2325,6 +2375,7 @@ fn runtime_effect_company_target_import_blocker( | RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } | RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::ActivateEventRecord { .. } | RuntimeEffect::DeactivateEventRecord { .. } @@ -2665,6 +2716,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), } @@ -5041,6 +5093,7 @@ mod tests { ("Locomotive 10".to_string(), 0), ("Locomotive 112".to_string(), 1), ]), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -5144,7 +5197,7 @@ mod tests { } #[test] - fn keeps_recovered_locomotive_cost_rows_parity_only() { + fn blocks_recovered_locomotive_cost_rows_without_catalog_context_lower_band() { let save_slice = SmpLoadedSaveSlice { file_extension_hint: Some("gms".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()), @@ -5192,7 +5245,9 @@ mod tests { decoded_conditions: Vec::new(), decoded_actions: vec![], executable_import_ready: false, - notes: vec!["locomotive cost rows remain metadata-only".to_string()], + notes: vec![ + "scalar locomotive cost row still needs catalog context".to_string(), + ], }], }), notes: vec![], @@ -5200,7 +5255,289 @@ mod tests { let import = project_save_slice_to_runtime_state_import( &save_slice, - "packed-events-locomotive-cost-frontier", + "packed-events-locomotive-cost-frontier-lower-band", + 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_locomotive_catalog_context") + ); + } + + #[test] + fn blocks_recovered_locomotive_cost_rows_without_catalog_context() { + 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, + 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: 35, + live_record_count: 1, + live_entry_ids: vec![35], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 35, + payload_offset: Some(0x7202), + payload_len: Some(96), + 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: vec![], + 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_locomotive_cost_row(352, 250000)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "scalar locomotive cost row still needs catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "packed-events-locomotive-cost-missing-catalog", + 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_locomotive_catalog_context") + ); + } + + #[test] + fn overlays_scalar_locomotive_cost_rows_into_named_cost_effects() { + 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::new(), + selected_company_id: None, + players: Vec::new(), + selected_player_id: None, + trains: Vec::new(), + locomotive_catalog: vec![ + crate::RuntimeLocomotiveCatalogEntry { + locomotive_id: 1, + name: "Locomotive 1".to_string(), + }, + crate::RuntimeLocomotiveCatalogEntry { + locomotive_id: 101, + name: "Locomotive 101".to_string(), + }, + ], + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::from([ + ("Locomotive 1".to_string(), 100000), + ("Locomotive 101".to_string(), 200000), + ]), + 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, + named_locomotive_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: 36, + live_record_count: 1, + live_entry_ids: vec![36], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 36, + 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: vec![], + standalone_condition_row_count: 0, + standalone_condition_rows: vec![], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![2, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_cost_row(352, 250000), + real_locomotive_cost_row(475, 325000), + ], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec![ + "scalar locomotive cost rows use overlay catalog context".to_string(), + ], + }], + }), + notes: vec![], + }; + + let mut import = project_save_slice_overlay_to_runtime_state_import( + &base_state, + &save_slice, + "overlay-locomotive-cost", + 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("overlay-imported locomotive cost record should run"); + + assert_eq!( + import.state.named_locomotive_cost.get("Locomotive 1"), + Some(&250000) + ); + assert_eq!( + import.state.named_locomotive_cost.get("Locomotive 101"), + Some(&325000) + ); + } + + #[test] + fn keeps_negative_locomotive_cost_rows_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, + named_locomotive_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: 37, + live_record_count: 1, + live_entry_ids: vec![37], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 37, + payload_offset: Some(0x7202), + payload_len: Some(96), + 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: vec![], + 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_locomotive_cost_row(352, -1)], + decoded_conditions: Vec::new(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["negative locomotive cost rows remain parity-only".to_string()], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "packed-events-negative-locomotive-cost-frontier", None, ) .expect("save slice should project"); @@ -5398,6 +5735,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -8257,6 +8595,7 @@ mod tests { }], candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState { periodic_boundary_calls: 9, @@ -8426,6 +8765,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }, diff --git a/crates/rrt-runtime/src/persistence.rs b/crates/rrt-runtime/src/persistence.rs index 0b14e3c..7ce4e26 100644 --- a/crates/rrt-runtime/src/persistence.rs +++ b/crates/rrt-runtime/src/persistence.rs @@ -105,6 +105,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 76e6460..4305182 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -317,6 +317,10 @@ pub enum RuntimeEffect { name: String, value: bool, }, + SetNamedLocomotiveCost { + name: String, + value: u32, + }, SetSpecialCondition { label: String, value: u32, @@ -653,6 +657,8 @@ pub struct RuntimeState { #[serde(default)] pub named_locomotive_availability: BTreeMap, #[serde(default)] + pub named_locomotive_cost: BTreeMap, + #[serde(default)] pub special_conditions: BTreeMap, #[serde(default)] pub service_state: RuntimeServiceState, @@ -1162,6 +1168,11 @@ impl RuntimeState { return Err("named_locomotive_availability contains an empty key".to_string()); } } + for key in self.named_locomotive_cost.keys() { + if key.trim().is_empty() { + return Err("named_locomotive_cost contains an empty key".to_string()); + } + } for key in self.special_conditions.keys() { if key.trim().is_empty() { return Err("special_conditions contains an empty key".to_string()); @@ -1238,6 +1249,11 @@ fn validate_runtime_effect( return Err("name must not be empty".to_string()); } } + RuntimeEffect::SetNamedLocomotiveCost { name, .. } => { + if name.trim().is_empty() { + return Err("name must not be empty".to_string()); + } + } RuntimeEffect::SetSpecialCondition { label, .. } => { if label.trim().is_empty() { return Err("label must not be empty".to_string()); @@ -1459,6 +1475,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1516,6 +1533,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1572,6 +1590,7 @@ mod tests { }], candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1638,6 +1657,7 @@ mod tests { }], candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1735,6 +1755,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1778,6 +1799,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1821,6 +1843,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1881,6 +1904,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1931,6 +1955,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1985,6 +2010,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -2035,6 +2061,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -2091,6 +2118,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -2141,6 +2169,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -2191,6 +2220,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 052ffcb..e0f9d69 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -3489,6 +3489,7 @@ fn runtime_effect_supported_for_save_import(effect: &RuntimeEffect) -> bool { | RuntimeEffect::SetEconomicStatusCode { .. } | RuntimeEffect::SetCandidateAvailability { .. } | RuntimeEffect::SetNamedLocomotiveAvailability { .. } + | RuntimeEffect::SetNamedLocomotiveCost { .. } | RuntimeEffect::SetSpecialCondition { .. } | RuntimeEffect::ConfiscateCompanyAssets { .. } | RuntimeEffect::DeactivateCompany { .. } diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index be893b7..d95d6e5 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -495,6 +495,9 @@ fn apply_runtime_effects( .named_locomotive_availability .insert(name.clone(), u32::from(*value)); } + RuntimeEffect::SetNamedLocomotiveCost { name, value } => { + state.named_locomotive_cost.insert(name.clone(), *value); + } RuntimeEffect::SetSpecialCondition { label, value } => { state.special_conditions.insert(label.clone(), *value); } @@ -1165,6 +1168,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), } @@ -1416,6 +1420,43 @@ mod tests { assert_eq!(result.service_events[0].applied_effect_count, 2); } + #[test] + fn applies_named_locomotive_cost_effects() { + let mut state = RuntimeState { + event_runtime_records: vec![RuntimeEventRecord { + record_id: 11, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![ + RuntimeEffect::SetNamedLocomotiveCost { + name: "Big Boy".to_string(), + value: 250000, + }, + RuntimeEffect::SetNamedLocomotiveCost { + name: "GP7".to_string(), + value: 175000, + }, + ], + }], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("named locomotive cost effects should succeed"); + + assert_eq!(state.named_locomotive_cost.get("Big Boy"), Some(&250000)); + assert_eq!(state.named_locomotive_cost.get("GP7"), Some(&175000)); + assert_eq!(result.service_events[0].applied_effect_count, 2); + } + #[test] fn resolves_symbolic_company_targets() { let mut state = RuntimeState { diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index 0bb3661..3121766 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -76,6 +76,7 @@ pub struct RuntimeSummary { pub zero_candidate_availability_count: usize, pub named_locomotive_availability_count: usize, pub zero_named_locomotive_availability_count: usize, + pub named_locomotive_cost_count: usize, pub special_condition_count: usize, pub enabled_special_condition_count: usize, pub save_profile_kind: Option, @@ -595,6 +596,7 @@ impl RuntimeSummary { .values() .filter(|value| **value == 0) .count(), + named_locomotive_cost_count: state.named_locomotive_cost.len(), special_condition_count: state.special_conditions.len(), enabled_special_condition_count: state .special_conditions @@ -817,6 +819,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -916,6 +919,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -964,6 +968,7 @@ mod tests { ("GP7".to_string(), 1), ("Mikado".to_string(), 0), ]), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -972,6 +977,46 @@ mod tests { assert_eq!(summary.locomotive_catalog_count, 2); assert_eq!(summary.named_locomotive_availability_count, 3); assert_eq!(summary.zero_named_locomotive_availability_count, 2); + assert_eq!(summary.named_locomotive_cost_count, 0); + } + + #[test] + fn counts_named_locomotive_cost_entries() { + let state = 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, + players: Vec::new(), + selected_player_id: None, + trains: Vec::new(), + locomotive_catalog: Vec::new(), + territories: Vec::new(), + company_territory_track_piece_counts: Vec::new(), + company_territory_access: Vec::new(), + packed_event_collection: None, + event_runtime_records: Vec::new(), + candidate_availability: BTreeMap::new(), + named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::from([ + ("Big Boy".to_string(), 250000), + ("GP7".to_string(), 175000), + ]), + special_conditions: BTreeMap::new(), + service_state: RuntimeServiceState::default(), + }; + + let summary = RuntimeSummary::from_state(&state); + + assert_eq!(summary.named_locomotive_cost_count, 2); } #[test] @@ -1064,6 +1109,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; @@ -1142,6 +1188,7 @@ mod tests { event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), named_locomotive_availability: BTreeMap::new(), + named_locomotive_cost: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }; diff --git a/docs/README.md b/docs/README.md index 5a49b70..3d5f8a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -132,6 +132,10 @@ The highest-value next passes are now: overlay-backed locomotive catalog context: checked-in save-slice documents can populate `RuntimeState.named_locomotive_availability`, and boolean `0/1` availability descriptors can lower through `RuntimeState.locomotive_catalog` into the same ordinary event-service path +- that same overlay-backed locomotive catalog now unlocks the recovered locomotive-cost bands too: + nonnegative scalar rows from descriptors `352..451` and `475..500` can lower into the new + event-owned `RuntimeState.named_locomotive_cost` map through the ordinary runtime path, while + cargo-production and territory-access-cost rows remain metadata-rich parity-only families - 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 ba673cd..21edd16 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -88,15 +88,19 @@ Implemented today: through the ordinary event-service path without requiring Trainbuy or live locomotive-pool parity - the boolean `0/1` subset of the recovered locomotives-page availability bands can now import through an overlay-backed `RuntimeState.locomotive_catalog`; non-boolean availability payloads - and the adjacent locomotive-cost/cargo-production/access-cost families now surface as recovered - metadata-rich parity rows with checked-in slot labels and locomotive ids where grounded, but - execution for those scalar families remains deferred + still remain parity-only, but the adjacent locomotive-cost bands `352..451` and `475..500` now + import too through the same overlay-backed catalog into the event-owned + `RuntimeState.named_locomotive_cost` map when their scalar payloads are nonnegative +- cargo-production `230..240` and territory-access-cost `453` rows now remain as the primary + recovered scalar-band parity families: their labels, target masks, and slot identities are + checked in, but execution for those families is still deferred until a grounded landing surface + exists That means the next implementation work is breadth, not bootstrap. The recommended next slice is -honest landing surfaces for one or more of those recovered scalar families, plus broader real -grouped-descriptor and ordinary condition-id coverage beyond the current access, whole-game -toggle, train, player, numeric-threshold, named locomotive availability, and overlay-resolved -locomotive availability batches. +honest landing surfaces for one or more of the remaining recovered scalar families, plus broader +real grouped-descriptor and ordinary condition-id coverage beyond the current access, whole-game +toggle, train, player, numeric-threshold, named locomotive availability, and named locomotive cost +batches. Richer runtime ownership should still be added only where a later descriptor or condition family needs more than the current event-owned roster. diff --git a/fixtures/runtime/packed-event-locomotive-cost-overlay-base-snapshot.json b/fixtures/runtime/packed-event-locomotive-cost-overlay-base-snapshot.json new file mode 100644 index 0000000..8dde81e --- /dev/null +++ b/fixtures/runtime/packed-event-locomotive-cost-overlay-base-snapshot.json @@ -0,0 +1,45 @@ +{ + "format_version": 1, + "snapshot_id": "packed-event-locomotive-cost-overlay-base-snapshot", + "source": { + "description": "Base runtime snapshot supplying locomotive catalog context for descriptor-driven named locomotive cost import." + }, + "state": { + "calendar": { + "year": 1835, + "month_slot": 1, + "phase_slot": 2, + "tick_slot": 4 + }, + "world_flags": { + "base.only": true + }, + "metadata": { + "base.note": "preserve locomotive cost catalog context" + }, + "locomotive_catalog": [ + { + "locomotive_id": 1, + "name": "Locomotive 1" + }, + { + "locomotive_id": 101, + "name": "Locomotive 101" + } + ], + "named_locomotive_cost": { + "Locomotive 1": 100000, + "Locomotive 101": 200000 + }, + "event_runtime_records": [], + "candidate_availability": {}, + "named_locomotive_availability": {}, + "special_conditions": {}, + "service_state": { + "periodic_boundary_calls": 0, + "trigger_dispatch_counts": {}, + "total_event_record_services": 0, + "dirty_rerun_count": 0 + } + } +} diff --git a/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json b/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json new file mode 100644 index 0000000..822dddd --- /dev/null +++ b/fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json @@ -0,0 +1,82 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-locomotive-cost-overlay-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture backed by an overlay import document so recovered scalar locomotive cost descriptors execute against captured catalog context." + }, + "state_import_path": "packed-event-locomotive-cost-overlay.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "calendar": { + "year": 1835, + "month_slot": 1, + "phase_slot": 2, + "tick_slot": 4 + }, + "calendar_projection_is_placeholder": false, + "locomotive_catalog_count": 2, + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 1, + "packed_event_parity_only_record_count": 1, + "event_runtime_record_count": 1, + "named_locomotive_cost_count": 2, + "total_event_record_service_count": 1, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "metadata": { + "base.note": "preserve locomotive cost catalog context", + "save_slice.import_projection": "overlay-runtime-restore-v1" + }, + "named_locomotive_cost": { + "Locomotive 1": 250000, + "Locomotive 101": 325000 + }, + "packed_event_collection": { + "live_entry_ids": [41], + "records": [ + { + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "import_outcome": "imported", + "grouped_effect_rows": [ + { + "descriptor_id": 352, + "recovered_locomotive_id": 1 + }, + { + "descriptor_id": 475, + "recovered_locomotive_id": 101 + } + ] + } + ] + }, + "event_runtime_records": [ + { + "record_id": 41, + "service_count": 1, + "effects": [ + { + "kind": "set_named_locomotive_cost", + "name": "Locomotive 1", + "value": 250000 + }, + { + "kind": "set_named_locomotive_cost", + "name": "Locomotive 101", + "value": 325000 + } + ] + } + ] + } +} diff --git a/fixtures/runtime/packed-event-locomotive-cost-overlay-save-slice.json b/fixtures/runtime/packed-event-locomotive-cost-overlay-save-slice.json new file mode 100644 index 0000000..105969a --- /dev/null +++ b/fixtures/runtime/packed-event-locomotive-cost-overlay-save-slice.json @@ -0,0 +1,140 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-locomotive-cost-overlay-save-slice", + "source": { + "description": "Tracked save-slice document proving recovered scalar locomotive cost descriptors import through overlay-backed catalog context.", + "original_save_filename": "captured-locomotive-cost-overlay.gms", + "original_save_sha256": "locomotive-cost-overlay-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "recovered locomotive cost descriptors 352 and 475 import through the ordinary runtime path when overlay catalog context resolves their ids" + ] + }, + "save_slice": { + "file_extension_hint": "gms", + "container_profile_family": "rt3-classic-save-container-v1", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "trailer_family": null, + "bridge_family": null, + "profile": null, + "candidate_availability_table": null, + "named_locomotive_availability_table": null, + "special_conditions_table": null, + "event_runtime_collection": { + "source_kind": "packed-event-runtime-collection", + "mechanism_family": "classic-save-rehydrate-v1", + "mechanism_confidence": "grounded", + "container_profile_family": "rt3-classic-save-container-v1", + "metadata_tag_offset": 29952, + "records_tag_offset": 30208, + "close_tag_offset": 30976, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 41, + "live_record_count": 1, + "live_entry_ids": [41], + "decoded_record_count": 1, + "imported_runtime_record_count": 1, + "records": [ + { + "record_index": 0, + "live_entry_id": 41, + "payload_offset": 30240, + "payload_len": 120, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 7, + "one_shot": false, + "compact_control": { + "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] + }, + "text_bands": [], + "standalone_condition_row_count": 0, + "standalone_condition_rows": [], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [2, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 352, + "descriptor_label": "Locomotive 1 Cost", + "target_mask_bits": 8, + "parameter_family": "locomotive_cost_scalar", + "opcode": 3, + "raw_scalar_value": 250000, + "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", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Locomotive 1 Cost to 250000", + "recovered_locomotive_id": 1, + "locomotive_name": null, + "notes": [ + "locomotive cost descriptor maps to live locomotive id 1" + ] + }, + { + "group_index": 0, + "row_index": 1, + "descriptor_id": 475, + "descriptor_label": "Locomotive 101 Cost", + "target_mask_bits": 8, + "parameter_family": "locomotive_cost_scalar", + "opcode": 3, + "raw_scalar_value": 325000, + "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", + "semantic_family": "scalar_assignment", + "semantic_preview": "Set Locomotive 101 Cost to 325000", + "recovered_locomotive_id": 101, + "locomotive_name": null, + "notes": [ + "locomotive cost descriptor maps to live locomotive id 101" + ] + } + ], + "decoded_conditions": [], + "decoded_actions": [ + { + "kind": "set_named_locomotive_cost", + "name": "Locomotive 1", + "value": 250000 + }, + { + "kind": "set_named_locomotive_cost", + "name": "Locomotive 101", + "value": 325000 + } + ], + "executable_import_ready": false, + "notes": [ + "scalar locomotive cost rows use overlay catalog context" + ] + } + ] + }, + "notes": [ + "overlay-backed locomotive cost effect sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-locomotive-cost-overlay.json b/fixtures/runtime/packed-event-locomotive-cost-overlay.json new file mode 100644 index 0000000..7e9fd55 --- /dev/null +++ b/fixtures/runtime/packed-event-locomotive-cost-overlay.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "import_id": "packed-event-locomotive-cost-overlay", + "source": { + "description": "Overlay import document combining a tracked base snapshot with a tracked recovered locomotive-cost save slice." + }, + "base_snapshot_path": "packed-event-locomotive-cost-overlay-base-snapshot.json", + "save_slice_path": "packed-event-locomotive-cost-overlay-save-slice.json" +} diff --git a/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json index b10a463..af49076 100644 --- a/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json @@ -26,11 +26,12 @@ "packed_event_imported_runtime_record_count": 0, "packed_event_parity_only_record_count": 3, "packed_event_unsupported_record_count": 0, + "packed_event_blocked_missing_locomotive_catalog_context_count": 1, "packed_event_blocked_missing_condition_context_count": 0, "packed_event_blocked_territory_condition_scope_count": 0, "packed_event_blocked_missing_compact_control_count": 0, "packed_event_blocked_unmapped_real_descriptor_count": 0, - "packed_event_blocked_unmapped_world_descriptor_count": 3, + "packed_event_blocked_unmapped_world_descriptor_count": 2, "packed_event_blocked_structural_only_count": 0, "event_runtime_record_count": 0, "total_company_cash": 0 @@ -61,7 +62,7 @@ { "decode_status": "parity_only", "payload_family": "real_packed_v1", - "import_outcome": "blocked_unmapped_world_descriptor", + "import_outcome": "blocked_missing_locomotive_catalog_context", "grouped_effect_rows": [ { "descriptor_id": 352, diff --git a/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice.json b/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice.json index 54594c8..c0e6154 100644 --- a/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice.json +++ b/fixtures/runtime/packed-event-world-scalar-band-parity-save-slice.json @@ -146,7 +146,7 @@ "decoded_actions": [], "executable_import_ready": false, "notes": [ - "recovered locomotive cost metadata is now checked in, but execution is still deferred" + "recovered locomotive cost metadata is now checked in, but scalar rows still need overlay-backed locomotive catalog context to import" ] }, {