diff --git a/README.md b/README.md index 81479af..dadd79f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,13 @@ rows directly into `RuntimeState.named_locomotive_availability` and `RuntimeState.named_locomotive_cost` without needing overlay snapshots when the save carries enough catalog context. The remaining recovered scalar world families execute too: cargo-production slots `230..240` lower into `cargo_production_overrides`, and descriptor `453` lowers into -`world_restore.territory_access_cost`. Explicit unmapped world-condition and world-descriptor +`world_restore.territory_access_cost`. Whole-game ordinary-condition breadth now aligns with those +same world-scalar runtime surfaces too: named locomotive availability thresholds, named +locomotive cost thresholds, aggregate cargo-production thresholds, limited-track-building-amount +thresholds, and territory-access-cost thresholds all gate imported runtime records through the +same service path. Families the current checked-in metadata still does not ground, such as `All +Factory Production`, now remain explicitly visible on `blocked_unmapped_world_condition` rather +than collapsing back to generic residue. Explicit unmapped world-condition and world-descriptor frontier buckets still remain where current checked-in metadata stops, and `blocked_missing_locomotive_catalog_context` is now reserved for intentionally incomplete save-side catalog context instead of the normal save-slice path. Shell purchase-flow, Trainbuy refresh, diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index 0fb14b6..204d8be 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -4476,6 +4476,13 @@ mod tests { let world_scalar_executable_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "../../fixtures/runtime/packed-event-world-scalar-executable-save-slice-fixture.json", ); + let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json", + ); + let world_scalar_condition_parity_fixture = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json", + ); run_runtime_summarize_fixture(&parity_fixture) .expect("save-slice-backed parity fixture should summarize"); @@ -4511,6 +4518,10 @@ mod tests { .expect("save-slice-backed recovered scalar-band parity fixture should summarize"); run_runtime_summarize_fixture(&world_scalar_executable_fixture) .expect("save-slice-backed executable world-scalar fixture should summarize"); + run_runtime_summarize_fixture(&world_scalar_condition_fixture) + .expect("save-slice-backed executable world-scalar condition fixture should summarize"); + run_runtime_summarize_fixture(&world_scalar_condition_parity_fixture) + .expect("save-slice-backed parity world-scalar condition fixture should summarize"); } #[test] diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index a1708a1..07125a0 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -1544,6 +1544,42 @@ fn lower_condition_targets_in_condition( comparator: *comparator, value: *value, }, + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name, + comparator, + value, + } => RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: name.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name, + comparator, + value, + } => RuntimeCondition::NamedLocomotiveCostThreshold { + name: name.clone(), + comparator: *comparator, + value: *value, + }, + RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { + RuntimeCondition::CargoProductionTotalThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: *comparator, + value: *value, + } + } + RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: *comparator, + value: *value, + } + } RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { RuntimeCondition::EconomicStatusCodeThreshold { comparator: *comparator, @@ -1633,6 +1669,11 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { RuntimeCondition::TerritoryNumericThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } | RuntimeCondition::EconomicStatusCodeThreshold { .. } | RuntimeCondition::WorldFlagEquals { .. } => false, } @@ -2251,6 +2292,11 @@ fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { condition, RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } | RuntimeCondition::EconomicStatusCodeThreshold { .. } | RuntimeCondition::WorldFlagEquals { .. } ) @@ -2262,12 +2308,18 @@ fn ordinary_condition_row_is_world_state_family( row.metric.as_deref().is_some_and(|metric| { metric.contains("Special Condition") || metric.contains("Candidate Availability") + || metric.contains("Named Locomotive") + || metric.contains("Cargo Production") + || metric.contains("Limited Track Building Amount") + || metric.contains("Territory Access Cost") || metric.contains("Economic Status") || metric.contains("World Flag") - }) || row - .semantic_family - .as_deref() - .is_some_and(|family| family == "world_state_threshold" || family == "world_flag_equals") + }) || row.semantic_family.as_deref().is_some_and(|family| { + matches!( + family, + "world_state_threshold" | "world_scalar_threshold" | "world_flag_equals" + ) + }) } fn real_grouped_row_is_world_state_family( @@ -2334,6 +2386,11 @@ fn runtime_condition_company_target_import_blocker( .or_else(|| territory_target_import_blocker(territory, company_context)), RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } | RuntimeCondition::EconomicStatusCodeThreshold { .. } | RuntimeCondition::WorldFlagEquals { .. } => None, } @@ -2855,7 +2912,10 @@ fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf { #[cfg(test)] mod tests { use super::*; - use crate::{RuntimeTrackPieceCounts, RuntimeTrain, StepCommand, execute_step_command}; + use crate::{ + RuntimeConditionComparator, RuntimeTrackPieceCounts, RuntimeTrain, StepCommand, + execute_step_command, + }; fn state() -> RuntimeState { RuntimeState { @@ -7859,6 +7919,262 @@ mod tests { ); } + #[test] + fn imports_and_executes_world_scalar_conditions_through_runtime_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, + named_locomotive_availability_table: None, + locomotive_catalog: 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: 0x7800, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 45, + live_record_count: 2, + live_entry_ids: vec![41, 45], + decoded_record_count: 2, + imported_runtime_record_count: 2, + records: vec![ + crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 41, + payload_offset: Some(0x7200), + payload_len: Some(192), + decode_status: "executable".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + 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![5, 0, 0, 0], + grouped_effect_rows: vec![ + real_locomotive_availability_row(250, 42), + real_locomotive_cost_row(352, 250000), + real_cargo_production_row(230, 125), + real_limited_track_building_amount_row(18), + real_territory_access_cost_row(750000), + ], + decoded_conditions: vec![], + decoded_actions: vec![ + RuntimeEffect::SetNamedLocomotiveAvailabilityValue { + name: "Big Boy".to_string(), + value: 42, + }, + RuntimeEffect::SetNamedLocomotiveCost { + name: "Locomotive 1".to_string(), + value: 250000, + }, + RuntimeEffect::SetCargoProductionSlot { + slot: 1, + value: 125, + }, + RuntimeEffect::SetLimitedTrackBuildingAmount { value: 18 }, + RuntimeEffect::SetTerritoryAccessCost { value: 750000 }, + ], + executable_import_ready: true, + notes: vec!["world-scalar setup record".to_string()], + }, + crate::SmpLoadedPackedEventRecordSummary { + record_index: 1, + live_entry_id: 45, + payload_offset: Some(0x7300), + payload_len: Some(184), + decode_status: "executable".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: 5, + standalone_condition_rows: vec![ + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 0, + raw_condition_id: 2422, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&42_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Big Boy".to_string()), + comparator: Some("eq".to_string()), + metric: Some("Named Locomotive Availability: Big Boy".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Named Locomotive Availability: Big Boy == 42".to_string(), + ), + requires_candidate_name_binding: false, + notes: vec![], + }, + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 1, + raw_condition_id: 2423, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&250000_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Locomotive 1".to_string()), + comparator: Some("eq".to_string()), + metric: Some("Named Locomotive Cost: Locomotive 1".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Named Locomotive Cost: Locomotive 1 == 250000" + .to_string(), + ), + requires_candidate_name_binding: false, + notes: vec![], + }, + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 2, + raw_condition_id: 2418, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Cargo Production Total".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Cargo Production Total == 125".to_string(), + ), + requires_candidate_name_binding: false, + notes: vec![], + }, + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 3, + raw_condition_id: 2547, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&18_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Limited Track Building Amount".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Limited Track Building Amount == 18".to_string(), + ), + requires_candidate_name_binding: false, + notes: vec![], + }, + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 4, + raw_condition_id: 1516, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&750000_i32.to_le_bytes()); + bytes + }, + candidate_name: None, + comparator: Some("eq".to_string()), + metric: Some("Territory Access Cost".to_string()), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Territory Access Cost == 750000".to_string(), + ), + requires_candidate_name_binding: false, + notes: vec![], + }, + ], + negative_sentinel_scope: None, + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: vec![real_world_flag_row( + 110, + "Disable Stock Buying and Selling", + true, + )], + decoded_conditions: vec![ + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name: "Locomotive 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 250000, + }, + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ], + decoded_actions: vec![RuntimeEffect::SetWorldFlag { + key: "world.world_scalar_conditions_passed".to_string(), + value: true, + }], + executable_import_ready: true, + notes: vec!["world-scalar conditions gate a world-side effect".to_string()], + }, + ], + }), + notes: vec![], + }; + + let mut import = project_save_slice_to_runtime_state_import( + &save_slice, + "world-scalar-condition-save-slice", + None, + ) + .expect("save-slice import should project"); + + crate::execute_step_command( + &mut import.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 6 }, + ) + .expect("setup trigger should execute"); + crate::execute_step_command( + &mut import.state, + &crate::StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("gated trigger should execute"); + + assert_eq!( + import + .state + .world_flags + .get("world.world_scalar_conditions_passed"), + Some(&true) + ); + } + #[test] fn overlays_recovered_world_toggle_batch_into_executable_runtime_record() { let base_state = state(); diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index b9d964f..f4e89d8 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -244,6 +244,28 @@ pub enum RuntimeCondition { comparator: RuntimeConditionComparator, value: i64, }, + NamedLocomotiveAvailabilityThreshold { + name: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + NamedLocomotiveCostThreshold { + name: String, + comparator: RuntimeConditionComparator, + value: i64, + }, + CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, + TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator, + value: i64, + }, EconomicStatusCodeThreshold { comparator: RuntimeConditionComparator, value: i64, @@ -1375,6 +1397,17 @@ fn validate_runtime_condition( Ok(()) } } + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { name, .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { name, .. } => { + if name.trim().is_empty() { + Err("name must not be empty".to_string()) + } else { + Ok(()) + } + } + RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()), RuntimeCondition::EconomicStatusCodeThreshold { .. } => Ok(()), RuntimeCondition::WorldFlagEquals { key, .. } => { if key.trim().is_empty() { diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 1081e47..3ad3c1b 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -240,6 +240,11 @@ enum RealOrdinaryConditionMetric { enum RealWorldConditionKind { SpecialCondition { label: &'static str }, CandidateAvailability, + NamedLocomotiveAvailability, + NamedLocomotiveCost, + CargoProductionTotal, + LimitedTrackBuildingAmount, + TerritoryAccessCost, EconomicStatus, WorldFlag { key: &'static str }, } @@ -258,8 +263,13 @@ struct RealOrdinaryConditionMetadata { } const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; +const REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID: i32 = 2422; +const REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID: i32 = 2423; +const REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2418; +const REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID: i32 = 2547; +const REAL_TERRITORY_ACCESS_COST_CONDITION_ID: i32 = 1516; -const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 24] = [ +const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 29] = [ RealOrdinaryConditionMetadata { raw_condition_id: 1802, label: "Current Cash", @@ -419,6 +429,35 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 24] = [ label: "%1 Avail.", kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability), }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, + label: "Unknown Loco Available", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, + label: "Unknown Loco Cost", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Cargo Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, + label: "Limited Track Building Amount", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_TERRITORY_ACCESS_COST_CONDITION_ID, + label: "Access Rights Cost:", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost), + }, RealOrdinaryConditionMetadata { raw_condition_id: 2350, label: "Economic Status", @@ -2526,6 +2565,17 @@ fn parse_real_condition_row_summary( .to_string(), ); } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability + | RealWorldConditionKind::NamedLocomotiveCost + ) + ) && candidate_name.is_none() + }) { + notes.push("named locomotive condition row is missing its side-string binding".to_string()); + } Some(SmpLoadedPackedEventConditionRowSummary { row_index, raw_condition_id, @@ -2645,6 +2695,27 @@ fn real_ordinary_condition_metric_label( None => "Candidate Availability".to_string(), } } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ) => match candidate_name { + Some(name) => format!("Named Locomotive Availability: {name}"), + None => "Named Locomotive Availability".to_string(), + }, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => { + match candidate_name { + Some(name) => format!("Named Locomotive Cost: {name}"), + None => "Named Locomotive Cost".to_string(), + } + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { + "Cargo Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ) => "Limited Track Building Amount".to_string(), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { + "Territory Access Cost".to_string() + } RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { "Economic Status".to_string() } @@ -2662,6 +2733,13 @@ fn real_ordinary_condition_semantic_family( RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::WorldFlag { .. }) => { "world_flag_equals" } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability + | RealWorldConditionKind::NamedLocomotiveCost + | RealWorldConditionKind::CargoProductionTotal + | RealWorldConditionKind::LimitedTrackBuildingAmount + | RealWorldConditionKind::TerritoryAccessCost, + ) => "world_scalar_threshold", RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold", } } @@ -2868,6 +2946,32 @@ fn decode_real_condition_row( comparator, value, }), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::NamedLocomotiveAvailability, + ) => row.candidate_name.as_ref().map(|name| { + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: name.clone(), + comparator, + value, + } + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::NamedLocomotiveCost) => row + .candidate_name + .as_ref() + .map(|name| RuntimeCondition::NamedLocomotiveCostThreshold { + name: name.clone(), + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { + Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value }) + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::LimitedTrackBuildingAmount, + ) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::TerritoryAccessCost) => { + Some(RuntimeCondition::TerritoryAccessCostThreshold { comparator, value }) + } RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value }) } @@ -3655,6 +3759,11 @@ fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> | RuntimeCondition::CompanyTerritoryNumericThreshold { .. } | RuntimeCondition::SpecialConditionThreshold { .. } | RuntimeCondition::CandidateAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } + | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionTotalThreshold { .. } + | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } + | RuntimeCondition::TerritoryAccessCostThreshold { .. } | RuntimeCondition::EconomicStatusCodeThreshold { .. } | RuntimeCondition::WorldFlagEquals { .. } => true, } @@ -9431,6 +9540,224 @@ mod tests { ); } + #[test] + fn decodes_real_named_locomotive_availability_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, + 4, + 42, + Some("Big Boy"), + ); + 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: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + 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].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Named Locomotive Availability: Big Boy") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_scalar_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }] + ); + } + + #[test] + fn decodes_real_named_locomotive_cost_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID, + 0, + 250000, + Some("Locomotive 1"), + ); + 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: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &[condition_row], + [&[], &[], &[], &[]], + ); + + 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].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Named Locomotive Cost: Locomotive 1") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::NamedLocomotiveCostThreshold { + name: "Locomotive 1".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 250000, + }] + ); + } + + #[test] + fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() { + let condition_rows = vec![ + build_real_condition_row_with_threshold( + REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + 0, + 200, + None, + ), + build_real_condition_row_with_threshold( + REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, + 4, + 18, + None, + ), + build_real_condition_row_with_threshold( + REAL_TERRITORY_ACCESS_COST_CONDITION_ID, + 4, + 750000, + None, + ), + ]; + 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: [0, 0, 0, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, -1, -1, -1], + }), + &condition_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].standalone_condition_rows[0] + .metric + .as_deref(), + Some("Cargo Production Total") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_scalar_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![ + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Ge, + value: 200, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ] + ); + } + #[test] fn decodes_real_world_flag_condition_from_checked_in_world_condition_metadata() { let condition_row = build_real_condition_row_with_threshold(2535, 4, 1, None); @@ -9492,6 +9819,31 @@ mod tests { ); } + #[test] + fn looks_up_checked_in_world_scalar_condition_metadata() { + let availability = + real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID) + .expect("availability condition metadata should exist"); + assert_eq!(availability.label, "Unknown Loco Available"); + + let cost = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_COST_CONDITION_ID) + .expect("cost condition metadata should exist"); + assert_eq!(cost.label, "Unknown Loco Cost"); + + let cargo = real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_TOTAL_CONDITION_ID) + .expect("cargo condition metadata should exist"); + assert_eq!(cargo.label, "All Cargo Production"); + + let build_limit = + real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID) + .expect("build-limit condition metadata should exist"); + assert_eq!(build_limit.label, "Limited Track Building Amount"); + + let access_cost = real_ordinary_condition_metadata(REAL_TERRITORY_ACCESS_COST_CONDITION_ID) + .expect("territory-access-cost condition metadata should exist"); + assert_eq!(access_cost.label, "Access Rights Cost:"); + } + #[test] fn looks_up_checked_in_world_flag_descriptor_metadata() { let metadata = diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index d50b75c..c124bbe 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -722,6 +722,67 @@ fn evaluate_record_conditions( return Ok(None); } } + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name, + comparator, + value, + } => { + let actual = state + .named_locomotive_availability + .get(name) + .copied() + .map(i64::from) + .unwrap_or(0); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::NamedLocomotiveCostThreshold { + name, + comparator, + value, + } => { + let actual = state + .named_locomotive_cost + .get(name) + .copied() + .map(i64::from) + .unwrap_or(0); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { + let actual = state + .cargo_production_overrides + .values() + .copied() + .map(i64::from) + .sum::(); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value } => { + let actual = state + .world_restore + .limited_track_building_amount + .map(i64::from) + .unwrap_or(0); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } + RuntimeCondition::TerritoryAccessCostThreshold { comparator, value } => { + let actual = state + .world_restore + .territory_access_cost + .map(i64::from) + .unwrap_or(0); + if !compare_condition_value(actual, *comparator, *value) { + return Ok(None); + } + } RuntimeCondition::EconomicStatusCodeThreshold { comparator, value } => { let actual = state .world_restore @@ -2142,6 +2203,91 @@ mod tests { assert_eq!(state.world_flags.get("world_condition_failed"), None); } + #[test] + fn evaluates_world_scalar_conditions_before_effects_run() { + let mut state = RuntimeState { + world_restore: RuntimeWorldRestoreState { + limited_track_building_amount: Some(18), + territory_access_cost: Some(750000), + ..RuntimeWorldRestoreState::default() + }, + named_locomotive_availability: BTreeMap::from([(String::from("Big Boy"), 42)]), + named_locomotive_cost: BTreeMap::from([(String::from("GP7"), 175000)]), + cargo_production_overrides: BTreeMap::from([(1, 125), (2, 75)]), + event_runtime_records: vec![ + RuntimeEventRecord { + record_id: 25, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![ + RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Big Boy".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 42, + }, + RuntimeCondition::NamedLocomotiveCostThreshold { + name: "GP7".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 175000, + }, + RuntimeCondition::CargoProductionTotalThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 200, + }, + RuntimeCondition::LimitedTrackBuildingAmountThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 18, + }, + RuntimeCondition::TerritoryAccessCostThreshold { + comparator: RuntimeConditionComparator::Eq, + value: 750000, + }, + ], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_scalar_condition_passed".to_string(), + value: true, + }], + }, + RuntimeEventRecord { + record_id: 26, + trigger_kind: 7, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: vec![RuntimeCondition::NamedLocomotiveAvailabilityThreshold { + name: "Missing Loco".to_string(), + comparator: RuntimeConditionComparator::Gt, + value: 0, + }], + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world_scalar_condition_failed".to_string(), + value: true, + }], + }, + ], + ..state() + }; + + let result = execute_step_command( + &mut state, + &StepCommand::ServiceTriggerKind { trigger_kind: 7 }, + ) + .expect("world-scalar conditions should evaluate successfully"); + + assert_eq!(result.service_events[0].serviced_record_ids, vec![25]); + assert_eq!( + state.world_flags.get("world_scalar_condition_passed"), + Some(&true) + ); + assert_eq!(state.world_flags.get("world_scalar_condition_failed"), None); + } + #[test] fn one_shot_record_only_fires_once() { let mut state = RuntimeState { diff --git a/docs/README.md b/docs/README.md index 62764ab..c1243c2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -138,6 +138,12 @@ The highest-value next passes are now: - cargo-production `230..240` and territory-access-cost `453` now execute too through minimal world-side scalar landing surfaces: slot-indexed `cargo_production_overrides` and `world_restore.territory_access_cost` +- world-scalar ordinary-condition coverage now matches those runtime surfaces too: checked-in + metadata lowers named locomotive availability, named locomotive cost, aggregate cargo + production, limited-track-building-amount, and territory-access-cost rows into explicit runtime + condition gates +- the remaining world-scalar condition frontier is now narrow and explicit: unsupported families + such as `All Factory Production` stay parity-only on `blocked_unmapped_world_condition` - 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 9093341..78e7dcf 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -97,11 +97,18 @@ Implemented today: - the remaining recovered scalar world families now execute as well: cargo-production `230..240` rows lower into slot-indexed `cargo_production_overrides`, and territory-access-cost descriptor `453` lowers into `world_restore.territory_access_cost` +- world-scalar ordinary-condition coverage now aligns with those runtime surfaces too: checked-in + metadata lowers named locomotive availability, named locomotive cost, aggregate cargo + production, limited-track-building-amount, and territory-access-cost rows into explicit runtime + condition gates +- the remaining world-side condition frontier is now narrower and more honest: unsupported scalar + families such as `All Factory Production` stay visible on `blocked_unmapped_world_condition` + instead of falling back to generic placeholder ids 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 toggle, train, player, numeric-threshold, named locomotive availability, named -locomotive cost, and world scalar override batches. +locomotive cost, world scalar override, and world-scalar condition 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-world-scalar-condition-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json new file mode 100644 index 0000000..b323c99 --- /dev/null +++ b/fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json @@ -0,0 +1,43 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-world-scalar-condition-parity-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture pinning one remaining unmapped world-scalar ordinary-condition family after the grounded batch." + }, + "state_save_slice_path": "packed-event-world-scalar-condition-parity-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "packed_event_collection_present": true, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, + "packed_event_imported_runtime_record_count": 0, + "packed_event_parity_only_record_count": 1, + "packed_event_blocked_unmapped_world_descriptor_count": 1, + "event_runtime_record_count": 0, + "world_flag_count": 7, + "total_event_record_service_count": 0, + "total_trigger_dispatch_count": 1 + }, + "expected_state_fragment": { + "packed_event_collection": { + "records": [ + { + "import_outcome": "blocked_unmapped_world_descriptor", + "standalone_condition_rows": [ + { + "raw_condition_id": 2419, + "metric": "All Factory Production", + "semantic_family": "world_scalar_threshold" + } + ] + } + ] + } + } +} diff --git a/fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice.json b/fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice.json new file mode 100644 index 0000000..90307a3 --- /dev/null +++ b/fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice.json @@ -0,0 +1,125 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-world-scalar-condition-parity-save-slice", + "source": { + "description": "Tracked save-slice document pinning one remaining unmapped world-scalar condition family after the grounded batch.", + "original_save_filename": "captured-world-scalar-condition-parity.gms", + "original_save_sha256": "world-scalar-condition-parity-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "keeps the broader all-factory-production family parity-only until a stronger runtime landing surface is grounded" + ] + }, + "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": 31232, + "records_tag_offset": 31488, + "close_tag_offset": 31872, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 33, + "live_record_count": 1, + "live_entry_ids": [33], + "decoded_record_count": 1, + "imported_runtime_record_count": 0, + "records": [ + { + "record_index": 0, + "live_entry_id": 33, + "payload_offset": 31520, + "payload_len": 152, + "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": 1, + "standalone_condition_rows": [ + { + "row_index": 0, + "raw_condition_id": 2419, + "subtype": 4, + "flag_bytes": [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": null, + "metric": "All Factory Production", + "semantic_family": "world_scalar_threshold", + "semantic_preview": null, + "requires_candidate_name_binding": false, + "notes": [ + "checked-in RT3.lng label is known, but this world-scalar condition family is not yet lowered" + ] + } + ], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 110, + "descriptor_label": "Disable Stock Buying and Selling", + "target_mask_bits": 8, + "parameter_family": "world_flag_toggle", + "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, + "row_shape": "bool_toggle", + "semantic_family": "bool_toggle", + "semantic_preview": "Set Disable Stock Buying and Selling to TRUE", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [], + "decoded_actions": [ + { + "kind": "set_world_flag", + "key": "world.disable_stock_buying_and_selling", + "value": true + } + ], + "executable_import_ready": false, + "notes": [ + "unmapped world-scalar condition parity sample" + ] + } + ] + }, + "notes": [ + "world-scalar condition parity sample" + ] + } +} diff --git a/fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json b/fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json new file mode 100644 index 0000000..e789a54 --- /dev/null +++ b/fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json @@ -0,0 +1,102 @@ +{ + "format_version": 1, + "fixture_id": "packed-event-world-scalar-condition-save-slice-fixture", + "source": { + "kind": "captured-runtime", + "description": "Fixture proving grounded world-scalar ordinary conditions gate an imported whole-game effect." + }, + "state_save_slice_path": "packed-event-world-scalar-condition-save-slice.json", + "commands": [ + { + "kind": "service_trigger_kind", + "trigger_kind": 6 + }, + { + "kind": "service_trigger_kind", + "trigger_kind": 7 + } + ], + "expected_summary": { + "packed_event_collection_present": true, + "packed_event_record_count": 2, + "packed_event_decoded_record_count": 2, + "packed_event_imported_runtime_record_count": 2, + "event_runtime_record_count": 2, + "named_locomotive_availability_count": 1, + "zero_named_locomotive_availability_count": 0, + "named_locomotive_cost_count": 1, + "cargo_production_override_count": 1, + "world_restore_limited_track_building_amount": 18, + "world_restore_territory_access_cost": 750000, + "world_flag_count": 8, + "total_event_record_service_count": 2, + "total_trigger_dispatch_count": 2 + }, + "expected_state_fragment": { + "world_flags": { + "world.disable_stock_buying_and_selling": true + }, + "world_restore": { + "limited_track_building_amount": 18, + "territory_access_cost": 750000 + }, + "named_locomotive_availability": { + "Big Boy": 42 + }, + "named_locomotive_cost": { + "Locomotive 1": 250000 + }, + "cargo_production_overrides": { + "1": 125 + }, + "packed_event_collection": { + "records": [ + { + "import_outcome": "imported" + }, + { + "import_outcome": "imported", + "decoded_conditions": [ + { + "kind": "named_locomotive_availability_threshold", + "name": "Big Boy", + "comparator": "eq", + "value": 42 + }, + { + "kind": "named_locomotive_cost_threshold", + "name": "Locomotive 1", + "comparator": "eq", + "value": 250000 + }, + { + "kind": "cargo_production_total_threshold", + "comparator": "eq", + "value": 125 + }, + { + "kind": "limited_track_building_amount_threshold", + "comparator": "eq", + "value": 18 + }, + { + "kind": "territory_access_cost_threshold", + "comparator": "eq", + "value": 750000 + } + ] + } + ] + }, + "event_runtime_records": [ + { + "record_id": 41, + "service_count": 1 + }, + { + "record_id": 45, + "service_count": 1 + } + ] + } +} diff --git a/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json b/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json new file mode 100644 index 0000000..d8b0e69 --- /dev/null +++ b/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json @@ -0,0 +1,386 @@ +{ + "format_version": 1, + "save_slice_id": "packed-event-world-scalar-condition-save-slice", + "source": { + "description": "Tracked save-slice document proving grounded world-scalar ordinary conditions gate a whole-game effect.", + "original_save_filename": "captured-world-scalar-condition.gms", + "original_save_sha256": "world-scalar-condition-sample-sha256", + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "uses checked-in RT3.lng condition ids for named locomotive availability, named locomotive cost, aggregate cargo production, limited track building amount, and access rights cost" + ] + }, + "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": 31488, + "records_tag_offset": 31744, + "close_tag_offset": 32768, + "packed_state_version": 1001, + "packed_state_version_hex": "0x000003e9", + "live_id_bound": 45, + "live_record_count": 2, + "live_entry_ids": [41, 45], + "decoded_record_count": 2, + "imported_runtime_record_count": 2, + "records": [ + { + "record_index": 0, + "live_entry_id": 41, + "payload_offset": 31808, + "payload_len": 272, + "decode_status": "parity_only", + "payload_family": "real_packed_v1", + "trigger_kind": 6, + "one_shot": false, + "compact_control": { + "mode_byte_0x7ef": 6, + "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": [5, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 250, + "descriptor_label": "Unknown Loco Available", + "target_mask_bits": 8, + "parameter_family": "locomotive_availability_scalar", + "opcode": 3, + "raw_scalar_value": 42, + "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 Unknown Loco Available to 42", + "recovered_locomotive_id": 10, + "locomotive_name": null, + "notes": [ + "locomotive availability descriptor maps to live locomotive id 10" + ] + }, + { + "group_index": 0, + "row_index": 1, + "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": 2, + "descriptor_id": 230, + "descriptor_label": "Cargo Production Slot 1", + "target_mask_bits": 8, + "parameter_family": "cargo_production_scalar", + "opcode": 3, + "raw_scalar_value": 125, + "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 Cargo Production Slot 1 to 125", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [] + }, + { + "group_index": 0, + "row_index": 3, + "descriptor_id": 122, + "descriptor_label": "Limited Track Building Amount", + "target_mask_bits": 8, + "parameter_family": "world_track_build_limit_scalar", + "opcode": 3, + "raw_scalar_value": 18, + "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 Limited Track Building Amount to 18", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [] + }, + { + "group_index": 0, + "row_index": 4, + "descriptor_id": 453, + "descriptor_label": "Territory Access Cost", + "target_mask_bits": 8, + "parameter_family": "territory_access_cost_scalar", + "opcode": 3, + "raw_scalar_value": 750000, + "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 Territory Access Cost to 750000", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [], + "decoded_actions": [ + { + "kind": "set_named_locomotive_availability_value", + "name": "Big Boy", + "value": 42 + }, + { + "kind": "set_named_locomotive_cost", + "name": "Locomotive 1", + "value": 250000 + }, + { + "kind": "set_cargo_production_slot", + "slot": 1, + "value": 125 + }, + { + "kind": "set_limited_track_building_amount", + "value": 18 + }, + { + "kind": "set_territory_access_cost", + "value": 750000 + } + ], + "executable_import_ready": true, + "notes": [ + "world-scalar setup record" + ] + }, + { + "record_index": 1, + "live_entry_id": 45, + "payload_offset": 32128, + "payload_len": 224, + "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": 5, + "standalone_condition_rows": [ + { + "row_index": 0, + "raw_condition_id": 2422, + "subtype": 4, + "flag_bytes": [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": "Big Boy", + "comparator": "eq", + "metric": "Named Locomotive Availability: Big Boy", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Named Locomotive Availability: Big Boy == 42", + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + }, + { + "row_index": 1, + "raw_condition_id": 2423, + "subtype": 4, + "flag_bytes": [144, 208, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": "Locomotive 1", + "comparator": "eq", + "metric": "Named Locomotive Cost: Locomotive 1", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Named Locomotive Cost: Locomotive 1 == 250000", + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + }, + { + "row_index": 2, + "raw_condition_id": 2418, + "subtype": 4, + "flag_bytes": [125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Cargo Production Total", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Cargo Production Total == 125", + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + }, + { + "row_index": 3, + "raw_condition_id": 2547, + "subtype": 4, + "flag_bytes": [18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Limited Track Building Amount", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Limited Track Building Amount == 18", + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + }, + { + "row_index": 4, + "raw_condition_id": 1516, + "subtype": 4, + "flag_bytes": [176, 113, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "candidate_name": null, + "comparator": "eq", + "metric": "Territory Access Cost", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Territory Access Cost == 750000", + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + } + ], + "negative_sentinel_scope": null, + "grouped_effect_row_counts": [1, 0, 0, 0], + "grouped_effect_rows": [ + { + "group_index": 0, + "row_index": 0, + "descriptor_id": 110, + "descriptor_label": "Disable Stock Buying and Selling", + "target_mask_bits": 8, + "parameter_family": "world_flag_toggle", + "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, + "row_shape": "bool_toggle", + "semantic_family": "bool_toggle", + "semantic_preview": "Set Disable Stock Buying and Selling to TRUE", + "recovered_locomotive_id": null, + "locomotive_name": null, + "notes": [] + } + ], + "decoded_conditions": [ + { + "kind": "named_locomotive_availability_threshold", + "name": "Big Boy", + "comparator": "eq", + "value": 42 + }, + { + "kind": "named_locomotive_cost_threshold", + "name": "Locomotive 1", + "comparator": "eq", + "value": 250000 + }, + { + "kind": "cargo_production_total_threshold", + "comparator": "eq", + "value": 125 + }, + { + "kind": "limited_track_building_amount_threshold", + "comparator": "eq", + "value": 18 + }, + { + "kind": "territory_access_cost_threshold", + "comparator": "eq", + "value": 750000 + } + ], + "decoded_actions": [ + { + "kind": "set_world_flag", + "key": "world.disable_stock_buying_and_selling", + "value": true + } + ], + "executable_import_ready": true, + "notes": [ + "world-scalar conditions gate a whole-game effect" + ] + } + ] + }, + "notes": [ + "world-scalar condition executable sample" + ] + } +}