diff --git a/README.md b/README.md index dadd79f..adb36e0 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,12 @@ catalog context. The remaining recovered scalar world families execute too: carg `230..240` lower into `cargo_production_overrides`, and descriptor `453` lowers into `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 +locomotive cost thresholds, named cargo-production slot 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-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 07125a0..2db4dc3 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -1562,6 +1562,17 @@ fn lower_condition_targets_in_condition( comparator: *comparator, value: *value, }, + RuntimeCondition::CargoProductionSlotThreshold { + slot, + label, + comparator, + value, + } => RuntimeCondition::CargoProductionSlotThreshold { + slot: *slot, + label: label.clone(), + comparator: *comparator, + value: *value, + }, RuntimeCondition::CargoProductionTotalThreshold { comparator, value } => { RuntimeCondition::CargoProductionTotalThreshold { comparator: *comparator, @@ -1671,6 +1682,7 @@ fn condition_uses_condition_true_company(condition: &RuntimeCondition) -> bool { | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } | RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } @@ -2294,6 +2306,7 @@ fn runtime_condition_is_world_state(condition: &RuntimeCondition) -> bool { | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } | RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } @@ -2310,6 +2323,9 @@ fn ordinary_condition_row_is_world_state_family( || metric.contains("Candidate Availability") || metric.contains("Named Locomotive") || metric.contains("Cargo Production") + || metric.contains("Factory Production") + || metric.contains("Farm/Mine Production") + || metric.contains("Other Cargo Production") || metric.contains("Limited Track Building Amount") || metric.contains("Territory Access Cost") || metric.contains("Economic Status") @@ -2388,6 +2404,7 @@ fn runtime_condition_company_target_import_blocker( | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } | RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } @@ -3001,6 +3018,8 @@ mod tests { metric: None, semantic_family: None, semantic_preview: None, + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec!["negative sentinel-style condition row id".to_string()], }] @@ -3085,6 +3104,8 @@ mod tests { row_shape: "multivalue_scalar".to_string(), semantic_family: Some("multivalue_scalar".to_string()), semantic_preview: Some("Set Company Cash to 7 with aux [2, 3, 24, 36]".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: Some("Mikado".to_string()), notes: vec!["grouped effect row carries locomotive-name side string".to_string()], @@ -3115,6 +3136,8 @@ mod tests { "Set Deactivate Company to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3140,6 +3163,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Company Track Pieces Buildable to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3170,6 +3195,8 @@ mod tests { "Set Deactivate Player to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3201,6 +3228,8 @@ mod tests { "Set Territory - Allow All to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes, @@ -3226,6 +3255,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Economic Status to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3253,6 +3284,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Limited Track Building Amount to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3280,6 +3313,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Use Wartime Cargos to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3307,6 +3342,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Turbo Diesel Availability to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3335,6 +3372,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Unknown Loco Available to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: match descriptor_id { 241..=351 => Some(descriptor_id - 240), 457..=474 => Some(descriptor_id - 345), @@ -3375,6 +3414,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id, locomotive_name: None, notes: vec![], @@ -3431,6 +3472,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set {descriptor_label} to {value}")), + recovered_cargo_slot: Some(slot), + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3458,6 +3501,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some(format!("Set Territory Access Cost to {value}")), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3490,6 +3535,8 @@ mod tests { "Set {label} to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3520,6 +3567,8 @@ mod tests { "Set Confiscate All to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -3552,6 +3601,8 @@ mod tests { "Set Retire Train to {}", if enabled { "TRUE" } else { "FALSE" } )), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: locomotive_name.map(ToString::to_string), notes, @@ -3577,6 +3628,8 @@ mod tests { row_shape: "bool_toggle".to_string(), semantic_family: Some("bool_toggle".to_string()), semantic_preview: Some("Set Confiscate All to FALSE".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec![], @@ -5043,6 +5096,8 @@ mod tests { metric: Some("Territory Track Pieces".to_string()), semantic_family: Some("numeric_threshold".to_string()), semantic_preview: Some("Test Territory Track Pieces >= 10".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: Vec::new(), }, @@ -5223,6 +5278,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some("Set Unknown Loco Available to 42".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: Some(10), locomotive_name: None, notes: vec![], @@ -6324,6 +6381,8 @@ mod tests { semantic_preview: Some( "Set Company Cash to 250 with aux [2, 3, 24, 36]".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: Some("Mikado".to_string()), notes: vec![ @@ -7855,6 +7914,8 @@ mod tests { semantic_preview: Some( "Test Disable Stock Buying and Selling == TRUE".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![ "checked-in whole-game condition metadata sample".to_string(), @@ -7881,6 +7942,8 @@ mod tests { row_shape: "scalar_assignment".to_string(), semantic_family: Some("scalar_assignment".to_string()), semantic_preview: Some("Set Turbo Diesel Availability to 1".to_string()), + recovered_cargo_slot: None, + recovered_cargo_class: None, recovered_locomotive_id: None, locomotive_name: None, notes: vec!["checked-in whole-game grouped-effect sample".to_string()], @@ -8006,7 +8069,7 @@ mod tests { one_shot: Some(false), compact_control: Some(real_compact_control()), text_bands: packed_text_bands(), - standalone_condition_row_count: 5, + standalone_condition_row_count: 6, standalone_condition_rows: vec![ crate::SmpLoadedPackedEventConditionRowSummary { row_index: 0, @@ -8024,6 +8087,8 @@ mod tests { semantic_preview: Some( "Test Named Locomotive Availability: Big Boy == 42".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }, @@ -8044,11 +8109,37 @@ mod tests { "Test Named Locomotive Cost: Locomotive 1 == 250000" .to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }, crate::SmpLoadedPackedEventConditionRowSummary { row_index: 2, + raw_condition_id: 200, + subtype: 4, + flag_bytes: { + let mut bytes = vec![0; 25]; + bytes[0..4].copy_from_slice(&125_i32.to_le_bytes()); + bytes + }, + candidate_name: Some("Cargo Production Slot 1".to_string()), + comparator: Some("eq".to_string()), + metric: Some( + "Cargo Production: Cargo Production Slot 1".to_string(), + ), + semantic_family: Some("world_scalar_threshold".to_string()), + semantic_preview: Some( + "Test Cargo Production: Cargo Production Slot 1 == 125" + .to_string(), + ), + recovered_cargo_slot: Some(1), + recovered_cargo_class: None, + requires_candidate_name_binding: false, + notes: vec![], + }, + crate::SmpLoadedPackedEventConditionRowSummary { + row_index: 3, raw_condition_id: 2418, subtype: 4, flag_bytes: { @@ -8063,11 +8154,13 @@ mod tests { semantic_preview: Some( "Test Cargo Production Total == 125".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }, crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 3, + row_index: 4, raw_condition_id: 2547, subtype: 4, flag_bytes: { @@ -8082,11 +8175,13 @@ mod tests { semantic_preview: Some( "Test Limited Track Building Amount == 18".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }, crate::SmpLoadedPackedEventConditionRowSummary { - row_index: 4, + row_index: 5, raw_condition_id: 1516, subtype: 4, flag_bytes: { @@ -8101,6 +8196,8 @@ mod tests { semantic_preview: Some( "Test Territory Access Cost == 750000".to_string(), ), + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }, @@ -8123,6 +8220,12 @@ mod tests { comparator: RuntimeConditionComparator::Eq, value: 250000, }, + RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, RuntimeCondition::CargoProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 125, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index f4e89d8..849199e 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -254,6 +254,12 @@ pub enum RuntimeCondition { comparator: RuntimeConditionComparator, value: i64, }, + CargoProductionSlotThreshold { + slot: u32, + label: String, + comparator: RuntimeConditionComparator, + value: i64, + }, CargoProductionTotalThreshold { comparator: RuntimeConditionComparator, value: i64, @@ -1405,6 +1411,15 @@ fn validate_runtime_condition( Ok(()) } } + RuntimeCondition::CargoProductionSlotThreshold { slot, label, .. } => { + if !(1..=11).contains(slot) { + Err("slot must be in 1..=11".to_string()) + } else if label.trim().is_empty() { + Err("label must not be empty".to_string()) + } else { + Ok(()) + } + } RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } => Ok(()), diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 3ad3c1b..590e5cd 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -242,7 +242,11 @@ enum RealWorldConditionKind { CandidateAvailability, NamedLocomotiveAvailability, NamedLocomotiveCost, + CargoProductionSlot, CargoProductionTotal, + FactoryProductionTotal, + FarmMineProductionTotal, + OtherCargoProductionTotal, LimitedTrackBuildingAmount, TerritoryAccessCost, EconomicStatus, @@ -263,13 +267,17 @@ struct RealOrdinaryConditionMetadata { } const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; +const REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID: i32 = 200; 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_FACTORY_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2419; +const REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2420; +const REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID: i32 = 2421; 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; 29] = [ +const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 33] = [ RealOrdinaryConditionMetadata { raw_condition_id: 1802, label: "Current Cash", @@ -429,6 +437,11 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 29] = [ label: "%1 Avail.", kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability), }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, + label: "%1 Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot), + }, RealOrdinaryConditionMetadata { raw_condition_id: REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID, label: "Unknown Loco Available", @@ -446,6 +459,25 @@ const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 29] = [ label: "All Cargo Production", kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal), }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Factory Production", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID, + label: "All Farm/Mine Production", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FarmMineProductionTotal, + ), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_OTHER_CARGO_PRODUCTION_TOTAL_CONDITION_ID, + label: "Unknown Cargo Production", + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ), + }, RealOrdinaryConditionMetadata { raw_condition_id: REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID, label: "Limited Track Building Amount", @@ -1718,6 +1750,10 @@ pub struct SmpLoadedPackedEventConditionRowSummary { #[serde(default)] pub semantic_preview: Option, #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] pub requires_candidate_name_binding: bool, #[serde(default)] pub notes: Vec, @@ -1748,6 +1784,10 @@ pub struct SmpLoadedPackedEventGroupedEffectRowSummary { #[serde(default)] pub semantic_preview: Option, #[serde(default)] + pub recovered_cargo_slot: Option, + #[serde(default)] + pub recovered_cargo_class: Option, + #[serde(default)] pub recovered_locomotive_id: Option, #[serde(default)] pub locomotive_name: Option, @@ -2576,6 +2616,44 @@ fn parse_real_condition_row_summary( }) { notes.push("named locomotive condition row is missing its side-string binding".to_string()); } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) + ) && candidate_name.is_none() + }) { + notes.push( + "named cargo-production condition row is missing its side-string binding".to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FactoryProductionTotal + | RealWorldConditionKind::FarmMineProductionTotal + | RealWorldConditionKind::OtherCargoProductionTotal + ) + ) + }) { + notes.push( + "checked-in RT3.lng label is known, but this cargo aggregate condition family is not yet lowered" + .to_string(), + ); + } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) + ) && candidate_name_ref + .and_then(recovered_cargo_production_slot_from_condition_name) + .is_none() + }) { + notes.push( + "named cargo-production condition side string does not yet map to a checked-in cargo slot" + .to_string(), + ); + } Some(SmpLoadedPackedEventConditionRowSummary { row_index, raw_condition_id, @@ -2596,6 +2674,24 @@ fn parse_real_condition_row_summary( format!("Test {} {} {}", metric_label, comparator_text, value) }) }), + recovered_cargo_slot: ordinary_metadata.and_then(|metadata| match metadata.kind { + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + candidate_name_ref.and_then(recovered_cargo_production_slot_from_condition_name) + } + _ => None, + }), + recovered_cargo_class: ordinary_metadata.and_then(|metadata| match metadata.kind { + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FactoryProductionTotal, + ) => Some("factory".to_string()), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FarmMineProductionTotal, + ) => Some("farm_mine".to_string()), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ) => Some("other".to_string()), + _ => None, + }), requires_candidate_name_binding, notes, }) @@ -2707,9 +2803,24 @@ fn real_ordinary_condition_metric_label( None => "Named Locomotive Cost".to_string(), } } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + match candidate_name { + Some(name) => format!("Cargo Production: {name}"), + None => "Cargo Production".to_string(), + } + } RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { "Cargo Production Total".to_string() } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FactoryProductionTotal) => { + "Factory Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::FarmMineProductionTotal) => { + "Farm/Mine Production Total".to_string() + } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::OtherCargoProductionTotal, + ) => "Other Cargo Production Total".to_string(), RealOrdinaryConditionKind::WorldState( RealWorldConditionKind::LimitedTrackBuildingAmount, ) => "Limited Track Building Amount".to_string(), @@ -2736,7 +2847,11 @@ fn real_ordinary_condition_semantic_family( RealOrdinaryConditionKind::WorldState( RealWorldConditionKind::NamedLocomotiveAvailability | RealWorldConditionKind::NamedLocomotiveCost + | RealWorldConditionKind::CargoProductionSlot | RealWorldConditionKind::CargoProductionTotal + | RealWorldConditionKind::FactoryProductionTotal + | RealWorldConditionKind::FarmMineProductionTotal + | RealWorldConditionKind::OtherCargoProductionTotal | RealWorldConditionKind::LimitedTrackBuildingAmount | RealWorldConditionKind::TerritoryAccessCost, ) => "world_scalar_threshold", @@ -2850,6 +2965,11 @@ fn parse_real_grouped_effect_row_summary( "locomotive cost descriptor maps to live locomotive id {loco_id}" )); } + if let Some(cargo_slot) = recovered_cargo_production_slot(descriptor_id) { + notes.push(format!( + "cargo-production descriptor maps to world production slot {cargo_slot}" + )); + } Some(SmpLoadedPackedEventGroupedEffectRowSummary { group_index, @@ -2877,6 +2997,8 @@ fn parse_real_grouped_effect_row_summary( value_word_0x14, value_word_0x16, )), + recovered_cargo_slot: recovered_cargo_production_slot(descriptor_id), + recovered_cargo_class: None, recovered_locomotive_id: recovered_locomotive_availability_loco_id(descriptor_id) .or_else(|| recovered_locomotive_cost_loco_id(descriptor_id)), locomotive_name, @@ -2946,6 +3068,18 @@ fn decode_real_condition_row( comparator, value, }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionSlot) => { + row.candidate_name.as_ref().and_then(|name| { + recovered_cargo_production_slot_from_condition_name(name).map(|slot| { + RuntimeCondition::CargoProductionSlotThreshold { + slot, + label: name.clone(), + comparator, + value, + } + }) + }) + } RealOrdinaryConditionKind::WorldState( RealWorldConditionKind::NamedLocomotiveAvailability, ) => row.candidate_name.as_ref().map(|name| { @@ -2966,6 +3100,11 @@ fn decode_real_condition_row( RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CargoProductionTotal) => { Some(RuntimeCondition::CargoProductionTotalThreshold { comparator, value }) } + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::FactoryProductionTotal + | RealWorldConditionKind::FarmMineProductionTotal + | RealWorldConditionKind::OtherCargoProductionTotal, + ) => None, RealOrdinaryConditionKind::WorldState( RealWorldConditionKind::LimitedTrackBuildingAmount, ) => Some(RuntimeCondition::LimitedTrackBuildingAmountThreshold { comparator, value }), @@ -3028,6 +3167,11 @@ fn recovered_cargo_production_descriptor_metadata( }) } +fn recovered_cargo_production_slot(descriptor_id: u32) -> Option { + let slot = descriptor_id.checked_sub(229)?; + (1..=11).contains(&slot).then_some(slot) +} + fn recovered_locomotive_availability_descriptor_metadata( descriptor_id: u32, ) -> Option { @@ -3083,6 +3227,12 @@ fn recovered_cargo_production_label(descriptor_id: u32) -> Option<&'static str> .copied() } +fn recovered_cargo_production_slot_from_condition_name(name: &str) -> Option { + let suffix = name.strip_prefix("Cargo Production Slot ")?; + let slot = suffix.parse::().ok()?; + (1..=11).contains(&slot).then_some(slot) +} + fn recovered_locomotive_cost_loco_id(descriptor_id: u32) -> Option { if (352..=451).contains(&descriptor_id) { return Some(descriptor_id - 351); @@ -3761,6 +3911,7 @@ fn runtime_condition_supported_for_save_import(condition: &RuntimeCondition) -> | RuntimeCondition::CandidateAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveAvailabilityThreshold { .. } | RuntimeCondition::NamedLocomotiveCostThreshold { .. } + | RuntimeCondition::CargoProductionSlotThreshold { .. } | RuntimeCondition::CargoProductionTotalThreshold { .. } | RuntimeCondition::LimitedTrackBuildingAmountThreshold { .. } | RuntimeCondition::TerritoryAccessCostThreshold { .. } @@ -9668,6 +9819,72 @@ mod tests { ); } + #[test] + fn decodes_real_named_cargo_production_threshold_from_checked_in_metadata() { + let condition_row = build_real_condition_row_with_threshold( + REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID, + 4, + 125, + Some("Cargo Production Slot 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("Cargo Production: Cargo Production Slot 1") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0].recovered_cargo_slot, + Some(1) + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }] + ); + } + #[test] fn decodes_real_world_scalar_thresholds_from_checked_in_metadata() { let condition_rows = vec![ @@ -9821,6 +10038,11 @@ mod tests { #[test] fn looks_up_checked_in_world_scalar_condition_metadata() { + let named_cargo = + real_ordinary_condition_metadata(REAL_CARGO_PRODUCTION_CONDITION_TEMPLATE_ID) + .expect("named cargo condition metadata should exist"); + assert_eq!(named_cargo.label, "%1 Production"); + let availability = real_ordinary_condition_metadata(REAL_NAMED_LOCOMOTIVE_AVAILABILITY_CONDITION_ID) .expect("availability condition metadata should exist"); @@ -9834,6 +10056,15 @@ mod tests { .expect("cargo condition metadata should exist"); assert_eq!(cargo.label, "All Cargo Production"); + let factory = real_ordinary_condition_metadata(REAL_FACTORY_PRODUCTION_TOTAL_CONDITION_ID) + .expect("factory production condition metadata should exist"); + assert_eq!(factory.label, "All Factory Production"); + + let farm_mine = + real_ordinary_condition_metadata(REAL_FARM_MINE_PRODUCTION_TOTAL_CONDITION_ID) + .expect("farm/mine production condition metadata should exist"); + assert_eq!(farm_mine.label, "All Farm/Mine Production"); + let build_limit = real_ordinary_condition_metadata(REAL_LIMITED_TRACK_BUILDING_AMOUNT_CONDITION_ID) .expect("build-limit condition metadata should exist"); @@ -10517,6 +10748,8 @@ mod tests { metric: None, semantic_family: None, semantic_preview: None, + recovered_cargo_slot: None, + recovered_cargo_class: None, requires_candidate_name_binding: false, notes: vec![], }]; diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index c124bbe..d5d5892 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -752,6 +752,22 @@ fn evaluate_record_conditions( return Ok(None); } } + RuntimeCondition::CargoProductionSlotThreshold { + slot, + comparator, + value, + .. + } => { + let actual = state + .cargo_production_overrides + .get(slot) + .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 @@ -2234,6 +2250,12 @@ mod tests { comparator: RuntimeConditionComparator::Eq, value: 175000, }, + RuntimeCondition::CargoProductionSlotThreshold { + slot: 1, + label: "Cargo Production Slot 1".to_string(), + comparator: RuntimeConditionComparator::Eq, + value: 125, + }, RuntimeCondition::CargoProductionTotalThreshold { comparator: RuntimeConditionComparator::Eq, value: 200, diff --git a/docs/README.md b/docs/README.md index c1243c2..20c20fd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -139,9 +139,9 @@ The highest-value next passes are now: 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 + metadata lowers named locomotive availability, named locomotive cost, named cargo-production + slot thresholds, 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, diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 78e7dcf..481ffba 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -98,9 +98,9 @@ Implemented today: 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 + metadata lowers named locomotive availability, named locomotive cost, named cargo-production + slot thresholds, 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 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 391e34b..b65616b 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 @@ -81,6 +81,8 @@ "row_shape": "scalar_assignment", "semantic_family": "scalar_assignment", "semantic_preview": "Set Cargo Production Slot 1 to 125", + "recovered_cargo_slot": 1, + "recovered_cargo_class": null, "recovered_locomotive_id": null, "locomotive_name": null, "notes": [] 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 index e789a54..26d96d2 100644 --- a/fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json @@ -69,6 +69,13 @@ "comparator": "eq", "value": 250000 }, + { + "kind": "cargo_production_slot_threshold", + "slot": 1, + "label": "Cargo Production Slot 1", + "comparator": "eq", + "value": 125 + }, { "kind": "cargo_production_total_threshold", "comparator": "eq", diff --git a/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json b/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json index d8b0e69..65b28f5 100644 --- a/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json +++ b/fixtures/runtime/packed-event-world-scalar-condition-save-slice.json @@ -5,10 +5,10 @@ "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" - ] + "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, named cargo-production slot, aggregate cargo production, limited track building amount, and access rights cost" + ] }, "save_slice": { "file_extension_hint": "gms", @@ -130,6 +130,8 @@ "row_shape": "scalar_assignment", "semantic_family": "scalar_assignment", "semantic_preview": "Set Cargo Production Slot 1 to 125", + "recovered_cargo_slot": 1, + "recovered_cargo_class": null, "recovered_locomotive_id": null, "locomotive_name": null, "notes": [] @@ -232,7 +234,7 @@ "grouped_territory_selectors_0x80f": [-1, -1, -1, -1] }, "text_bands": [], - "standalone_condition_row_count": 5, + "standalone_condition_row_count": 6, "standalone_condition_rows": [ { "row_index": 0, @@ -266,6 +268,23 @@ }, { "row_index": 2, + "raw_condition_id": 200, + "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": "Cargo Production Slot 1", + "comparator": "eq", + "metric": "Cargo Production: Cargo Production Slot 1", + "semantic_family": "world_scalar_threshold", + "semantic_preview": "Test Cargo Production: Cargo Production Slot 1 == 125", + "recovered_cargo_slot": 1, + "recovered_cargo_class": null, + "requires_candidate_name_binding": false, + "notes": [ + "checked-in world-scalar condition metadata sample" + ] + }, + { + "row_index": 3, "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], @@ -280,7 +299,7 @@ ] }, { - "row_index": 3, + "row_index": 4, "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], @@ -295,7 +314,7 @@ ] }, { - "row_index": 4, + "row_index": 5, "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], @@ -349,6 +368,13 @@ "comparator": "eq", "value": 250000 }, + { + "kind": "cargo_production_slot_threshold", + "slot": 1, + "label": "Cargo Production Slot 1", + "comparator": "eq", + "value": 125 + }, { "kind": "cargo_production_total_threshold", "comparator": "eq", @@ -374,7 +400,7 @@ ], "executable_import_ready": true, "notes": [ - "world-scalar conditions gate a whole-game effect" + "world-scalar conditions gate a whole-game effect, including a checked-in named cargo-production slot threshold" ] } ] diff --git a/fixtures/runtime/packed-event-world-scalar-executable-save-slice.json b/fixtures/runtime/packed-event-world-scalar-executable-save-slice.json index 314d57a..f871272 100644 --- a/fixtures/runtime/packed-event-world-scalar-executable-save-slice.json +++ b/fixtures/runtime/packed-event-world-scalar-executable-save-slice.json @@ -81,6 +81,8 @@ "row_shape": "scalar_assignment", "semantic_family": "scalar_assignment", "semantic_preview": "Set Cargo Production Slot 1 to 125", + "recovered_cargo_slot": 1, + "recovered_cargo_class": null, "recovered_locomotive_id": null, "locomotive_name": null, "notes": []