diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-18 12:49:45 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-18 12:49:45 +0300 |
| commit | 88132dc9e1eed8df0b4db97c7ae068c14fc6e938 (patch) | |
| tree | 1e772522a1d600d9e53fa237a5d1d07508e313d9 | |
| parent | 47cb2a693fa49a5931e35164ca629fa1a026be51 (diff) | |
| download | fparkan-88132dc9e1eed8df0b4db97c7ae068c14fc6e938.tar.xz fparkan-88132dc9e1eed8df0b4db97c7ae068c14fc6e938.zip | |
Parse CTLD initial effect group selector
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 1 | ||||
| -rw-r--r-- | crates/fparkan-assets/src/lib.rs | 73 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 13 |
3 files changed, 85 insertions, 2 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 0827cf2..7068882 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -6363,6 +6363,7 @@ mod tests { }], node_data: None, effect_section: fparkan_assets::PreparedControlEffectSection::Absent, + initial_effect_group: None, point_list: None, } } diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 1fd405c..d34effb 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -949,6 +949,9 @@ pub struct PreparedComponentControl { pub node_data: Option<PreparedControlNodeDataTable>, /// Parsed native effect rows in this component's control resource. pub effect_section: PreparedControlEffectSection, + /// Validated group selected by native Control's first effect-group + /// header word; `None` is the native no-group sentinel or an invalid index. + pub initial_effect_group: Option<usize>, /// Component-local points selected for parsed effect rows, when present. pub point_list: Option<PreparedControlPointList>, } @@ -3647,6 +3650,7 @@ const CONTROL_EFFECT_ROW_BYTES: usize = 100; struct DecodedComponentEffectSection { section: PreparedControlEffectSection, + initial_effect_group: Option<usize>, typed_records: Vec<PreparedControlTypedRecord>, } @@ -3670,6 +3674,7 @@ where typed_record_tag: None, } }, + initial_effect_group: None, typed_records: Vec::new(), }); } @@ -3690,6 +3695,7 @@ where typed_record_index: Some(record_index), typed_record_tag: Some(tag), }, + initial_effect_group: None, typed_records, }); } @@ -3788,16 +3794,24 @@ where typed_record_tag: None, } }, + initial_effect_group: None, typed_records, }); } - cursor = cursor + let group_header_end = cursor .checked_add(CONTROL_EFFECT_GROUP_HEADER_BYTES) .ok_or_else(|| invalid("effect group header end overflows"))?; - if cursor > payload.len() { + if group_header_end > payload.len() { return Err(invalid("effect group header extends past the payload")); } + let initial_effect_group = usize::try_from( + control_u32_at(payload, cursor) + .expect("validated effect group header contains its initial selector"), + ) + .ok() + .filter(|group| *group < effect_group_count); + cursor = group_header_end; let mut effect_rows = Vec::new(); for table_index in 0..effect_group_count { @@ -3863,11 +3877,13 @@ where typed_record_index: None, typed_record_tag: None, }, + initial_effect_group: None, typed_records, }); } Ok(DecodedComponentEffectSection { section: PreparedControlEffectSection::Parsed(effect_rows), + initial_effect_group, typed_records, }) } @@ -4016,6 +4032,7 @@ fn decode_component_control_payload( 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, }) } @@ -4508,6 +4525,58 @@ mod tests { } #[test] + fn component_control_uses_validated_initial_effect_group_header_word() { + 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(&[(1, 0.0, 1.0, 0.25, 0)]); + payload[12..16].copy_from_slice(&1_u32.to_le_bytes()); + payload[16..20].copy_from_slice(&9_u32.to_le_bytes()); + append_typed_record(&mut payload, 21, 0); + let group_header = payload.len(); + payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0); + payload[group_header..group_header + 4].copy_from_slice(&8_u32.to_le_bytes()); + for group in 0..9 { + if group == 8 { + append_effect_table( + &mut payload, + &[effect_row( + 4, + [0; 3], + [0; 3], + 17, + "effects.rlb", + "f_smalllight_y", + )], + ); + } else { + append_effect_table(&mut payload, &[]); + } + } + + let control = decode_component_control_payload(&payload, source.clone(), 2, 3) + .expect("nine declared effect groups parse"); + assert_eq!(control.initial_effect_group, Some(8)); + let PreparedControlEffectSection::Parsed(rows) = control.effect_section else { + panic!("effect groups should parse"); + }; + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].table_index, 8); + + payload[group_header..group_header + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + let no_default = decode_component_control_payload(&payload, source.clone(), 2, 3) + .expect("native no-group sentinel is accepted"); + assert_eq!(no_default.initial_effect_group, None); + + payload[group_header..group_header + 4].copy_from_slice(&9_u32.to_le_bytes()); + let out_of_range = decode_component_control_payload(&payload, source, 2, 3) + .expect("out-of-range selector does not invalidate tables"); + assert_eq!(out_of_range.initial_effect_group, None); + } + + #[test] fn tag21_gate_provider_comes_from_validated_explicit_ndp() { let source = ResourceKey { archive: parse_path("parts.rlb").expect("CTL archive"), diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index ebecddd..d1af7ba 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -454,6 +454,19 @@ C0 равен 0, поэтому найденные offsets 104–107, 119–121 условий, загрузка FX materials, activation, выполнение FX opcodes и видимый рендер эффектов пока не реализованы. +После типизированных записей CTLD хранит 21 ссылку на группу эффектов. В +`fr_l_plant.ctl` первая ссылка равна `8`, остальные 20 содержат `u32::MAX`; за +ними расположены 13 таблиц групп. При разборе по адресу `+0x8B10` к каждой +ссылке, кроме `u32::MAX`, прибавляется число групп, зарегистрированных до +таблиц этого CTL; получившиеся индексы сохраняются в control mapping. При +событии `0x80000020` код по адресу `+0x7830` сразу выходит, если поле дескриптора +`+0xC0` равно нулю. Разбор по адресу `+0x8B10` вызывает диспетчер групп `+0x2800` +с индексом группы по умолчанию только при ненулевом `+0xC0`. Назначение поля +дескриптора и условия его заполнения на стороне producer пока неизвестны. +`initial_effect_group` хранит проверенную ссылку на группу по умолчанию. Это +поле описывает данные CTL и не выполняет activation, загрузку FX materials, +FX opcodes или рендер эффектов. + CPU-only обход `MISSIONS/Autodemo.00/data.tma` подготовил четыре CTL с 80 строками type 4 и четыре явных CPT-списка (всего 322 записи). Среди пар были `fortif.rlb/fr_l_plant.ctl` и `fortif.rlb/fr_l_plant.cpt` (155 точек), а также |
