diff --git a/README.md b/README.md index cf10d60..947ba0c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,10 @@ train roster and opaque economic-status lane needed for real descriptors `8` `Ec `Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights rather than a territory-owned policy bit. Whole-game ordinary-condition execution now exists too: special-condition thresholds, candidate-availability thresholds, and economic-status-code -thresholds now gate imported runtime records through the same service path, and checked-in +thresholds now gate imported runtime records through the same service path, and that world-side +condition batch now decodes from checked-in metadata instead of fixture-only ids: real +special-condition label ids, real economic-status ids, and the recovered `%1 Avail.` candidate +template plus candidate-name side strings all lower into the runtime condition model. Checked-in whole-game descriptor metadata now drives the first real world-side effect batch too: special-condition and candidate-availability setters import natively while world-flag rows remain parity-only until keyed mapping is grounded. Explicit unmapped world-condition and diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 76b45c0..14bc768 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -214,127 +214,192 @@ enum RealOrdinaryConditionMetric { CompanyTerritory(RuntimeTrackMetric), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RealWorldConditionKind { + SpecialCondition { label: &'static str }, + CandidateAvailability, + EconomicStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RealOrdinaryConditionKind { + Numeric(RealOrdinaryConditionMetric), + WorldState(RealWorldConditionKind), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct RealOrdinaryConditionMetadata { raw_condition_id: i32, label: &'static str, - metric: RealOrdinaryConditionMetric, + kind: RealOrdinaryConditionKind, } -const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 22] = [ +const REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID: i32 = 435; + +const REAL_ORDINARY_CONDITION_METADATA: [RealOrdinaryConditionMetadata; 24] = [ RealOrdinaryConditionMetadata { raw_condition_id: 1802, label: "Current Cash", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::CurrentCash), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::CurrentCash, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 951, label: "Total Debt", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TotalDebt), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TotalDebt, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2366, label: "Credit Rating", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::CreditRating), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::CreditRating, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2368, label: "Prime Rate", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::PrimeRate), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::PrimeRate, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2293, label: "Company Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesTotal), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesTotal, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2294, label: "Company Single Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesSingle), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesSingle, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2295, label: "Company Double Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesDouble), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesDouble, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2296, label: "Company Transition Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesTransition), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesTransition, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2297, label: "Company Electric Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesElectric), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesElectric, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2298, label: "Company Non-Electric Track Pieces", - metric: RealOrdinaryConditionMetric::Company(RuntimeCompanyMetric::TrackPiecesNonElectric), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company( + RuntimeCompanyMetric::TrackPiecesNonElectric, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2313, label: "Territory Track Pieces", - metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesTotal), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesTotal, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2314, label: "Territory Single Track Pieces", - metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesSingle), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesSingle, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2315, label: "Territory Double Track Pieces", - metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesDouble), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesDouble, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2316, label: "Territory Transition Track Pieces", - metric: RealOrdinaryConditionMetric::Territory( + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( RuntimeTerritoryMetric::TrackPiecesTransition, - ), + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2317, label: "Territory Electric Track Pieces", - metric: RealOrdinaryConditionMetric::Territory(RuntimeTerritoryMetric::TrackPiecesElectric), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( + RuntimeTerritoryMetric::TrackPiecesElectric, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2318, label: "Territory Non-Electric Track Pieces", - metric: RealOrdinaryConditionMetric::Territory( + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory( RuntimeTerritoryMetric::TrackPiecesNonElectric, - ), + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2323, label: "Company-Territory Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Total), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Total, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2324, label: "Company-Territory Single Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Single), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Single, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2325, label: "Company-Territory Double Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Double), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Double, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2326, label: "Company-Territory Transition Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Transition), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Transition, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2327, label: "Company-Territory Electric Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::Electric), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::Electric, + )), }, RealOrdinaryConditionMetadata { raw_condition_id: 2328, label: "Company-Territory Non-Electric Track Pieces", - metric: RealOrdinaryConditionMetric::CompanyTerritory(RuntimeTrackMetric::NonElectric), + kind: RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory( + RuntimeTrackMetric::NonElectric, + )), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, + label: "%1 Avail.", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability), + }, + RealOrdinaryConditionMetadata { + raw_condition_id: 2350, + label: "Economic Status", + kind: RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus), }, ]; @@ -633,6 +698,15 @@ const KNOWN_TAG_DEFINITIONS: [KnownTagDefinition; 4] = [ }, ]; +fn known_special_condition_definition_for_label_id( + label_id: u32, +) -> Option { + KNOWN_SPECIAL_CONDITION_DEFINITIONS + .iter() + .copied() + .find(|definition| !definition.hidden && definition.label_id == label_id) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SmpKnownTagHit { pub tag_id: u16, @@ -2286,17 +2360,22 @@ fn parse_real_condition_row_summary( let flag_bytes = row_bytes .get(5..PACKED_EVENT_REAL_CONDITION_ROW_LEN)? .to_vec(); + let candidate_name_display = candidate_name.clone(); + let candidate_name_ref = candidate_name_display.as_deref(); let ordinary_metadata = real_ordinary_condition_metadata(raw_condition_id); let comparator = ordinary_metadata .and_then(|_| decode_real_condition_comparator(subtype)) .map(condition_comparator_label); - let metric = ordinary_metadata.map(|metadata| metadata.label.to_string()); + let metric = + ordinary_metadata.map(|metadata| real_ordinary_condition_metric_label(metadata, candidate_name_ref)); let threshold = ordinary_metadata.and_then(|_| decode_real_condition_threshold(&flag_bytes)); let requires_candidate_name_binding = ordinary_metadata.is_some_and(|metadata| { matches!( - metadata.metric, - RealOrdinaryConditionMetric::Territory(_) - | RealOrdinaryConditionMetric::CompanyTerritory(_) + metadata.kind, + RealOrdinaryConditionKind::Numeric( + RealOrdinaryConditionMetric::Territory(_) + | RealOrdinaryConditionMetric::CompanyTerritory(_) + ) ) && candidate_name.is_some() }); let mut notes = Vec::new(); @@ -2312,6 +2391,14 @@ fn parse_real_condition_row_summary( .to_string(), ); } + if ordinary_metadata.is_some_and(|metadata| { + matches!( + metadata.kind, + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) + ) && candidate_name.is_none() + }) { + notes.push("candidate-availability condition row is missing its candidate-name side string".to_string()); + } Some(SmpLoadedPackedEventConditionRowSummary { row_index, raw_condition_id, @@ -2320,13 +2407,16 @@ fn parse_real_condition_row_summary( candidate_name, comparator, metric, - semantic_family: ordinary_metadata.map(|_| "numeric_threshold".to_string()), + semantic_family: ordinary_metadata + .map(|metadata| real_ordinary_condition_semantic_family(metadata).to_string()), semantic_preview: ordinary_metadata.and_then(|metadata| { threshold.map(|value| { let comparator_text = decode_real_condition_comparator(subtype) .map(condition_comparator_symbol) .unwrap_or("?"); - format!("Test {} {} {}", metadata.label, comparator_text, value) + let metric_label = + real_ordinary_condition_metric_label(metadata, candidate_name_ref); + format!("Test {} {} {}", metric_label, comparator_text, value) }) }), requires_candidate_name_binding, @@ -2384,6 +2474,47 @@ fn real_ordinary_condition_metadata( .iter() .copied() .find(|metadata| metadata.raw_condition_id == raw_condition_id) + .or_else(|| { + known_special_condition_definition_for_label_id(raw_condition_id as u32).map( + |definition| RealOrdinaryConditionMetadata { + raw_condition_id, + label: definition.label, + kind: RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::SpecialCondition { + label: definition.label, + }, + ), + }, + ) + }) +} + +fn real_ordinary_condition_metric_label( + metadata: RealOrdinaryConditionMetadata, + candidate_name: Option<&str>, +) -> String { + match metadata.kind { + RealOrdinaryConditionKind::Numeric(_) => metadata.label.to_string(), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { label }) => { + format!("Special Condition: {label}") + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::CandidateAvailability) => { + match candidate_name { + Some(name) => format!("Candidate Availability: {name}"), + None => "Candidate Availability".to_string(), + } + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { + "Economic Status".to_string() + } + } +} + +fn real_ordinary_condition_semantic_family(metadata: RealOrdinaryConditionMetadata) -> &'static str { + match metadata.kind { + RealOrdinaryConditionKind::Numeric(_) => "numeric_threshold", + RealOrdinaryConditionKind::WorldState(_) => "world_state_threshold", + } } fn decode_real_condition_comparator(subtype: u8) -> Option { @@ -2531,8 +2662,8 @@ fn decode_real_condition_row( let metadata = real_ordinary_condition_metadata(row.raw_condition_id)?; let comparator = decode_real_condition_comparator(row.subtype)?; let value = decode_real_condition_threshold(&row.flag_bytes)?; - match metadata.metric { - RealOrdinaryConditionMetric::Company(metric) => { + match metadata.kind { + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Company(metric)) => { Some(RuntimeCondition::CompanyNumericThreshold { target: RuntimeCompanyTarget::ConditionTrueCompany, metric, @@ -2540,15 +2671,18 @@ fn decode_real_condition_row( value, }) } - RealOrdinaryConditionMetric::Territory(metric) => negative_sentinel_scope + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::Territory(metric)) => { + negative_sentinel_scope .filter(|scope| scope.territory_scope_selector_is_0x63) .map(|_| RuntimeCondition::TerritoryNumericThreshold { target: RuntimeTerritoryTarget::AllTerritories, metric, comparator, value, - }), - RealOrdinaryConditionMetric::CompanyTerritory(metric) => negative_sentinel_scope + }) + } + RealOrdinaryConditionKind::Numeric(RealOrdinaryConditionMetric::CompanyTerritory(metric)) => { + negative_sentinel_scope .filter(|scope| scope.territory_scope_selector_is_0x63) .map(|_| RuntimeCondition::CompanyTerritoryNumericThreshold { target: RuntimeCompanyTarget::ConditionTrueCompany, @@ -2556,7 +2690,27 @@ fn decode_real_condition_row( metric, comparator, value, - }), + }) + } + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::SpecialCondition { + label, + }) => Some(RuntimeCondition::SpecialConditionThreshold { + label: label.to_string(), + comparator, + value, + }), + RealOrdinaryConditionKind::WorldState( + RealWorldConditionKind::CandidateAvailability, + ) => row.candidate_name.as_ref().map(|name| { + RuntimeCondition::CandidateAvailabilityThreshold { + name: name.clone(), + comparator, + value, + } + }), + RealOrdinaryConditionKind::WorldState(RealWorldConditionKind::EconomicStatus) => { + Some(RuntimeCondition::EconomicStatusCodeThreshold { comparator, value }) + } } } @@ -7925,6 +8079,17 @@ mod tests { bytes } + fn build_real_condition_row_with_threshold( + raw_condition_id: i32, + subtype: u8, + threshold: i32, + candidate_name: Option<&str>, + ) -> Vec { + let mut bytes = build_real_condition_row(raw_condition_id, subtype, 0, candidate_name); + bytes[5..9].copy_from_slice(&threshold.to_le_bytes()); + bytes + } + struct RealGroupedEffectRowSpec<'a> { descriptor_id: u32, opcode: u8, @@ -8460,6 +8625,180 @@ mod tests { assert!(summary.records[0].executable_import_ready); } + #[test] + fn decodes_real_special_condition_threshold_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold(3835, 0, 1, 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_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("Special Condition: Use Wartime Cargos") + ); + assert_eq!( + summary.records[0].standalone_condition_rows[0] + .semantic_family + .as_deref(), + Some("world_state_threshold") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::SpecialConditionThreshold { + label: "Use Wartime Cargos".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 1, + }] + ); + } + + #[test] + fn decodes_real_candidate_availability_threshold_from_checked_in_world_condition_metadata() { + let condition_row = + build_real_condition_row_with_threshold(REAL_CANDIDATE_AVAILABILITY_CONDITION_TEMPLATE_ID, 0, 2, Some("Mogul")); + 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("Candidate Availability: Mogul") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::CandidateAvailabilityThreshold { + name: "Mogul".to_string(), + comparator: RuntimeConditionComparator::Ge, + value: 2, + }] + ); + } + + #[test] + fn decodes_real_economic_status_threshold_from_checked_in_world_condition_metadata() { + let condition_row = build_real_condition_row_with_threshold(2350, 0, 4, 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_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("Economic Status") + ); + assert_eq!( + summary.records[0].decoded_conditions, + vec![RuntimeCondition::EconomicStatusCodeThreshold { + comparator: RuntimeConditionComparator::Ge, + value: 4, + }] + ); + } + #[test] fn keeps_real_world_flag_descriptor_parity_only_with_checked_in_metadata() { let grouped_row = build_real_grouped_effect_row(RealGroupedEffectRowSpec { diff --git a/docs/README.md b/docs/README.md index 54b358d..7dd65d1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,6 +101,9 @@ The highest-value next passes are now: candidate-availability thresholds, and economic-status-code thresholds now gate imported runtime records, and the packed-event frontier now reports explicit unmapped world-condition and world-descriptor buckets +- that whole-game condition batch is now metadata-driven too: special-condition label ids, + economic-status, and the generic `%1 Avail.` candidate-availability template plus candidate-name + side strings all decode through checked-in world-condition metadata instead of fixture-only ids - the first real whole-game grouped-descriptor batch is now metadata-driven too: checked-in descriptor metadata covers special-condition and candidate-availability setters, while the current world-flag family stays parity-only until keyed flag identity is grounded well enough diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index b0ea1af..b4adbef 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -52,6 +52,11 @@ Implemented today: through the same service path, and whole-game parity frontiers now report explicit unmapped world-condition and world-descriptor buckets rather than falling back to the generic ordinary or descriptor counts +- checked-in whole-game condition metadata now drives that same world-side condition batch too: + special-condition thresholds use the real special-condition label ids, economic-status thresholds + use the checked-in world label id, and candidate-availability thresholds now decode through the + recovered `%1 Avail.` template plus the packed candidate-name side string instead of fixture-only + placeholder ids - checked-in whole-game grouped-descriptor metadata now drives the first real world-side effect batch too: real special-condition and candidate-availability setter rows now decode and import through the ordinary runtime path, while world-flag rows remain parity-only until keyed flag @@ -59,10 +64,10 @@ Implemented today: That means the next implementation work is breadth, not bootstrap. The recommended next slice is broader real grouped-descriptor and ordinary condition-id coverage beyond the current access, -whole-game, train, player, and numeric-threshold batches, with the whole-game frontier now -centered on still-unmapped world-flag families and any later state families that need stronger -checked-in descriptor or key recovery. Richer runtime ownership should still be added only where a -later descriptor or condition family needs more than the current event-owned roster. +whole-game, train, player, and numeric-threshold batches, with the world-side frontier now +centered primarily on still-unmapped world-flag families and any later state families that need +stronger checked-in descriptor or key recovery. Richer runtime ownership should still be added only +where a later descriptor or condition family needs more than the current event-owned roster. ## Why This Boundary diff --git a/fixtures/runtime/packed-event-world-condition-gated-save-slice.json b/fixtures/runtime/packed-event-world-condition-gated-save-slice.json index 95879b9..4348e08 100644 --- a/fixtures/runtime/packed-event-world-condition-gated-save-slice.json +++ b/fixtures/runtime/packed-event-world-condition-gated-save-slice.json @@ -5,11 +5,11 @@ "description": "Tracked save-slice document with whole-game conditions gating a whole-game effect.", "original_save_filename": "captured-world-condition-gated.gms", "original_save_sha256": "world-condition-gated-sample-sha256", - "notes": [ - "tracked as JSON save-slice document rather than raw .smp", - "proves whole-game ordinary conditions gate imported runtime effects", - "whole-game grouped descriptor ids now line up with the checked-in metadata table" - ] + "notes": [ + "tracked as JSON save-slice document rather than raw .smp", + "proves whole-game ordinary conditions gate imported runtime effects", + "whole-game condition ids now line up with the checked-in metadata tables instead of fixture-only placeholders" + ] }, "save_slice": { "file_extension_hint": "gms", @@ -187,7 +187,7 @@ "standalone_condition_rows": [ { "row_index": 0, - "raw_condition_id": 3901, + "raw_condition_id": 3835, "subtype": 0, "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, @@ -197,27 +197,27 @@ "semantic_preview": "Test Use Wartime Cargos >= 1", "requires_candidate_name_binding": false, "notes": [ - "tracked whole-game condition sample" + "checked-in whole-game condition metadata sample" ] }, { "row_index": 1, - "raw_condition_id": 3902, + "raw_condition_id": 435, "subtype": 0, "flag_bytes": [2, 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, + "candidate_name": "Mogul", "comparator": "ge", "metric": "Candidate Availability: Mogul", "semantic_family": "world_state_threshold", "semantic_preview": "Test Mogul >= 2", "requires_candidate_name_binding": false, "notes": [ - "tracked whole-game condition sample" + "checked-in whole-game condition metadata sample with candidate-name side string" ] }, { "row_index": 2, - "raw_condition_id": 3903, + "raw_condition_id": 2350, "subtype": 0, "flag_bytes": [4, 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, @@ -227,7 +227,7 @@ "semantic_preview": "Test Economic Status >= 4", "requires_candidate_name_binding": false, "notes": [ - "tracked whole-game condition sample" + "checked-in whole-game condition metadata sample" ] } ], diff --git a/fixtures/runtime/packed-event-world-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-world-parity-save-slice-fixture.json index 646217c..1cb7d0b 100644 --- a/fixtures/runtime/packed-event-world-parity-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-world-parity-save-slice-fixture.json @@ -14,21 +14,18 @@ ], "expected_summary": { "packed_event_collection_present": true, - "packed_event_record_count": 2, - "packed_event_decoded_record_count": 2, + "packed_event_record_count": 1, + "packed_event_decoded_record_count": 1, "packed_event_imported_runtime_record_count": 0, "event_runtime_record_count": 0, "packed_event_blocked_unmapped_world_descriptor_count": 1, - "packed_event_blocked_unmapped_world_condition_count": 1 + "packed_event_blocked_unmapped_world_condition_count": 0 }, "expected_state_fragment": { "packed_event_collection": { "records": [ { "import_outcome": "blocked_unmapped_world_descriptor" - }, - { - "import_outcome": "blocked_unmapped_world_condition" } ] } diff --git a/fixtures/runtime/packed-event-world-parity-save-slice.json b/fixtures/runtime/packed-event-world-parity-save-slice.json index 0622adb..d824e66 100644 --- a/fixtures/runtime/packed-event-world-parity-save-slice.json +++ b/fixtures/runtime/packed-event-world-parity-save-slice.json @@ -2,12 +2,12 @@ "format_version": 1, "save_slice_id": "packed-event-world-parity-save-slice", "source": { - "description": "Tracked save-slice document preserving the current whole-game descriptor and condition frontier.", + "description": "Tracked save-slice document preserving the remaining whole-game descriptor frontier.", "original_save_filename": "captured-world-parity.gms", "original_save_sha256": "world-parity-sample-sha256", "notes": [ "tracked as JSON save-slice document rather than raw .smp", - "keeps one unmapped world descriptor and one unmapped world condition explicit", + "keeps the still-unmapped world descriptor family explicit after the first real whole-game condition batch moved to checked-in metadata", "whole-game world-flag descriptor identity is checked in, but keyed runtime mapping remains parity-only" ] }, @@ -31,10 +31,10 @@ "close_tag_offset": 33024, "packed_state_version": 1001, "packed_state_version_hex": "0x000003e9", - "live_id_bound": 52, - "live_record_count": 2, - "live_entry_ids": [49, 52], - "decoded_record_count": 2, + "live_id_bound": 49, + "live_record_count": 1, + "live_entry_ids": [49], + "decoded_record_count": 1, "imported_runtime_record_count": 0, "records": [ { @@ -94,56 +94,6 @@ "notes": [ "world-side descriptor remains parity-only" ] - }, - { - "record_index": 1, - "live_entry_id": 52, - "payload_offset": 32464, - "payload_len": 120, - "decode_status": "parity_only", - "payload_family": "real_packed_v1", - "trigger_kind": 7, - "one_shot": false, - "compact_control": { - "mode_byte_0x7ef": 7, - "primary_selector_0x7f0": 0, - "grouped_mode_0x7f4": 2, - "one_shot_header_0x7f5": 0, - "modifier_flag_0x7f9": 0, - "modifier_flag_0x7fa": 0, - "grouped_target_scope_ordinals_0x7fb": [0, 0, 0, 0], - "grouped_scope_checkboxes_0x7ff": [0, 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": 3904, - "subtype": 0, - "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": "ge", - "metric": "Special Condition: Disable Stock Buying and Selling", - "semantic_family": "world_state_threshold", - "semantic_preview": "Test Disable Stock Buying and Selling >= 1", - "requires_candidate_name_binding": false, - "notes": [ - "recovered world-side ordinary condition family without a checked-in executable mapping yet" - ] - } - ], - "negative_sentinel_scope": null, - "grouped_effect_row_counts": [0, 0, 0, 0], - "grouped_effect_rows": [], - "decoded_conditions": [], - "decoded_actions": [], - "executable_import_ready": false, - "notes": [ - "world-side ordinary condition remains parity-only" - ] } ] },