diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-31 16:00:48 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-31 16:00:48 +0300 |
| commit | 6c7740f13396c124b9ec1df2ae7b05daee2f4905 (patch) | |
| tree | 61c169293d67281a6258c212ce9d734e7d2e702a /crates/fparkan-assets | |
| parent | 5adefa67146885f1a307e80117b698b965d70ceb (diff) | |
| download | fparkan-6c7740f13396c124b9ec1df2ae7b05daee2f4905.tar.xz fparkan-6c7740f13396c124b9ec1df2ae7b05daee2f4905.zip | |
feat(runtime): project factory-2 selector 6 metadata
Diffstat (limited to 'crates/fparkan-assets')
| -rw-r--r-- | crates/fparkan-assets/src/lib.rs | 221 |
1 files changed, 207 insertions, 14 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 9cc6b5b..a1ae4ec 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -2112,6 +2112,33 @@ impl<R: ResourceRepository> AssetManager<R> { ) -> Result<MissionAssetPlan, AssetError> { build_mission_asset_plan_with_repository(&self.repository, prototypes) } + + /// Reads raw opcodes from the validated initial Control effect group. + /// + /// A complete no-effect section or an out-of-range raw initial-group + /// selector returns `Some(Vec::new())`; unsupported shapes return `None`. + /// + /// # Errors + /// + /// Returns [`AssetError`] when the Control resource is malformed. + pub fn initial_control_effect_opcodes( + &self, + source: &ResourceKey, + model_node_count: usize, + prototype_index: usize, + ) -> Result<Option<Vec<u32>>, AssetError> { + let Some(payload) = read_optional_key(&self.repository, source, Some("component control"))? + else { + return Ok(None); + }; + let (_, opcodes) = decode_component_control_payload_with_opcodes( + &payload, + source.clone(), + model_node_count, + prototype_index, + )?; + Ok(opcodes) + } } /// Produces a count-only plan from a prototype graph. @@ -3881,6 +3908,7 @@ const CONTROL_EFFECT_ROW_BYTES: usize = 100; struct DecodedComponentEffectSection { section: PreparedControlEffectSection, initial_effect_group: Option<usize>, + initial_group_opcodes: Option<Vec<u32>>, typed_records: Vec<PreparedControlTypedRecord>, } @@ -3905,6 +3933,7 @@ where } }, initial_effect_group: None, + initial_group_opcodes: (cursor == payload.len()).then(Vec::new), typed_records: Vec::new(), }); } @@ -3926,6 +3955,7 @@ where typed_record_tag: Some(tag), }, initial_effect_group: None, + initial_group_opcodes: None, typed_records, }); } @@ -4015,8 +4045,9 @@ where } if effect_group_count == 0 { + let complete = cursor == payload.len(); return Ok(DecodedComponentEffectSection { - section: if cursor == payload.len() { + section: if complete { PreparedControlEffectSection::Parsed(Vec::new()) } else { PreparedControlEffectSection::Unsupported { @@ -4025,6 +4056,7 @@ where } }, initial_effect_group: None, + initial_group_opcodes: complete.then(Vec::new), typed_records, }); } @@ -4043,6 +4075,7 @@ where .filter(|group| *group < effect_group_count); cursor = group_header_end; + let mut initial_group_opcodes = Some(Vec::new()); let mut effect_rows = Vec::new(); for table_index in 0..effect_group_count { let row_count = usize::try_from( @@ -4069,7 +4102,14 @@ where for row_index in 0..row_count { let row_start = rows_start + row_index * CONTROL_EFFECT_ROW_BYTES; let row = &payload[row_start..row_start + CONTROL_EFFECT_ROW_BYTES]; - if control_u32_at(row, 0x0C) != Some(4) { + let opcode = control_u32_at(row, 0x0C).expect("fixed effect row has opcode"); + if initial_effect_group == Some(table_index) { + initial_group_opcodes + .as_mut() + .expect("valid initial group has an opcode list") + .push(opcode); + } + if opcode != 4 { continue; } let raw_condition = [ @@ -4108,12 +4148,14 @@ where typed_record_tag: None, }, initial_effect_group: None, + initial_group_opcodes: None, typed_records, }); } Ok(DecodedComponentEffectSection { section: PreparedControlEffectSection::Parsed(effect_rows), initial_effect_group, + initial_group_opcodes, typed_records, }) } @@ -4130,6 +4172,21 @@ fn decode_component_control_payload( model_node_count: usize, prototype_index: usize, ) -> Result<PreparedComponentControl, AssetError> { + decode_component_control_payload_with_opcodes( + payload, + source, + model_node_count, + prototype_index, + ) + .map(|(control, _)| control) +} + +fn decode_component_control_payload_with_opcodes( + payload: &[u8], + source: ResourceKey, + model_node_count: usize, + prototype_index: usize, +) -> Result<(PreparedComponentControl, Option<Vec<u32>>), AssetError> { const HEADER_BYTES: usize = 5 * 4; const FIXED_SECTION_BYTES: usize = 108; const SECTION_FIXED_BYTES: usize = 156; @@ -4267,18 +4324,22 @@ fn decode_component_control_payload( &invalid, )?; - Ok(PreparedComponentControl { - source, - native_profile_words, - model_node_count, - bindings, - timeline_rows, - typed_records: decoded_effect.typed_records, - node_data: None, - effect_section: decoded_effect.section, - initial_effect_group: decoded_effect.initial_effect_group, - point_list: None, - }) + let initial_group_opcodes = decoded_effect.initial_group_opcodes; + Ok(( + PreparedComponentControl { + source, + native_profile_words, + model_node_count, + bindings, + timeline_rows, + typed_records: decoded_effect.typed_records, + node_data: None, + effect_section: decoded_effect.section, + initial_effect_group: decoded_effect.initial_effect_group, + point_list: None, + }, + initial_group_opcodes, + )) } fn sibling_name(key: &ResourceKey, extension: &str) -> Result<ResourceName, AssetError> { @@ -6804,6 +6865,138 @@ mod tests { } #[test] + fn initial_control_effect_opcodes_keep_raw_selected_rows_in_order() { + let source = ResourceKey { + archive: parse_path("controls.rlb").expect("archive"), + name: resource_name(b"parts.ctl"), + type_id: None, + }; + let mut payload = component_control_payload(&[]); + payload[16..20].copy_from_slice(&1_u32.to_le_bytes()); + let group_header = payload.len(); + payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0); + let opcodes = [1, 4, 4, 12]; + append_effect_table( + &mut payload, + &opcodes.map(|opcode| effect_row(opcode, [0; 3], [0; 3], 0, "", "")), + ); + + let (control, decoded_opcodes) = + decode_component_control_payload_with_opcodes(&payload, source.clone(), 2, 0) + .expect("complete selected group"); + assert_eq!(decoded_opcodes, Some(opcodes.to_vec())); + let PreparedControlEffectSection::Parsed(rows) = control.effect_section else { + panic!("opcode-4 projection should remain parsed"); + }; + assert_eq!( + rows.iter().map(|row| row.row_index).collect::<Vec<_>>(), + [1, 2] + ); + + let repository = repository_with_archives(&[("controls.rlb", &[(b"parts.ctl", &payload)])]); + let manager = AssetManager::new(repository); + assert_eq!( + manager + .initial_control_effect_opcodes(&source, 2, 0) + .expect("public raw-opcode scanner"), + Some(opcodes.to_vec()) + ); + } + + #[test] + fn initial_control_effect_opcodes_distinguish_complete_empty_from_unknown_shapes() { + let source = |name: &[u8]| ResourceKey { + archive: parse_path("controls.rlb").expect("archive"), + name: resource_name(name), + type_id: None, + }; + let empty = component_control_payload(&[]); + + let mut selected_noop = component_control_payload(&[]); + selected_noop[16..20].copy_from_slice(&1_u32.to_le_bytes()); + let noop_header = selected_noop.len(); + selected_noop.resize(noop_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0); + append_effect_table(&mut selected_noop, &[]); + + let mut unsupported = component_control_payload(&[]); + unsupported[12..16].copy_from_slice(&1_u32.to_le_bytes()); + append_typed_record(&mut unsupported, 99, 0); + + let mut trailing_data = component_control_payload(&[]); + trailing_data.push(0xAA); + let repository = repository_with_archives(&[( + "controls.rlb", + &[ + (b"empty.ctl", &empty), + (b"noop.ctl", &selected_noop), + (b"unsupported.ctl", &unsupported), + (b"trailing.ctl", &trailing_data), + ], + )]); + let manager = AssetManager::new(repository); + + assert_eq!( + manager + .initial_control_effect_opcodes(&source(b"empty.ctl"), 2, 0) + .expect("complete empty CTLD"), + Some(Vec::new()) + ); + assert_eq!( + manager + .initial_control_effect_opcodes(&source(b"noop.ctl"), 2, 0) + .expect("complete no-op selected group"), + Some(Vec::new()) + ); + for name in [b"unsupported.ctl".as_slice(), b"trailing.ctl".as_slice()] { + assert_eq!( + manager + .initial_control_effect_opcodes(&source(name), 2, 0) + .expect("unsupported but bounded shape"), + None, + "{} remains unavailable", + String::from_utf8_lossy(name) + ); + } + } + + #[test] + fn initial_control_effect_opcodes_treat_out_of_range_initial_indices_as_known_empty() { + let make_payload = |raw_initial_group: u32| { + let mut payload = component_control_payload(&[]); + payload[16..20].copy_from_slice(&1_u32.to_le_bytes()); + let group_header = payload.len(); + payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0); + payload[group_header..group_header + 4] + .copy_from_slice(&raw_initial_group.to_le_bytes()); + append_effect_table(&mut payload, &[effect_row(1, [0; 3], [0; 3], 0, "", "")]); + payload + }; + let sentinel = make_payload(u32::MAX); + let exact_count = make_payload(1); + let repository = repository_with_archives(&[( + "controls.rlb", + &[ + (b"sentinel.ctl", &sentinel), + (b"exact-count.ctl", &exact_count), + ], + )]); + let manager = AssetManager::new(repository); + let sources = + [b"sentinel.ctl".as_slice(), b"exact-count.ctl".as_slice()].map(|name| ResourceKey { + archive: parse_path("controls.rlb").expect("archive"), + name: resource_name(name), + type_id: None, + }); + let results = sources.map(|source| { + manager + .initial_control_effect_opcodes(&source, 2, 0) + .expect("complete CTLD frames") + }); + + assert_eq!(results, [Some(Vec::new()), Some(Vec::new())]); + } + + #[test] fn tag21_gate_provider_comes_from_validated_explicit_ndp() { let source = ResourceKey { archive: parse_path("parts.rlb").expect("CTL archive"), |
