diff options
| author | Valentin Popov <valentin@popov.link> | 2026-11-01 07:41:00 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-11-01 07:41:00 +0300 |
| commit | 6c603976d4e368708935ab80e152b495a5c5e47a (patch) | |
| tree | b17df5ae85d8ec9c4899cf47dc71883d286eac79 | |
| parent | 651f5e10a7e5fbaa65a9acffd76d033925291e8e (diff) | |
| download | fparkan-6c603976d4e368708935ab80e152b495a5c5e47a.tar.xz fparkan-6c603976d4e368708935ab80e152b495a5c5e47a.zip | |
chore(render): report unexpanded ambient effects
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 42 | ||||
| -rw-r--r-- | crates/fparkan-fx/src/lib.rs | 16 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 3 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 9 |
4 files changed, 64 insertions, 6 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index b57d690..860922b 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -5498,6 +5498,8 @@ fn append_initial_control_effect_slots( let Some(point_list) = control.point_list.as_ref() else { return Ok(()); }; + let mut unsupported_commands = 0; + let mut state_dependent_commands = 0; for row in selected_initial_control_effect_rows(control.initial_effect_group, &control.effect_section) { @@ -5511,10 +5513,22 @@ fn append_initial_control_effect_slots( ) })?; let key = format!("{archive}/{name}"); + let mut record_skip = |reason: &str, + count: usize, + needs_state: bool, + command: Option<usize>| { + if needs_state { + state_dependent_commands += count; + } else { + unsupported_commands += count; + } + eprintln!("unexpanded Control FX reason:{reason} count:{count} needs_game_state:{needs_state} object:{object_index} component:{component_index} source:{key} table:{} row:{} command:{command:?}", row.table_index, row.row_index); + }; if !effect_documents.contains_key(&key) { let bytes = load_resource_entry_bytes_from_root(root, &archive, &name) .map_err(|error| format!("load Control FX {key}: {error}"))?; let Ok(document) = decode_fxid(bytes) else { + eprintln!("render_coverage control_fx_invalid_documents=1 (object:{object_index} component:{component_index} source:{key})"); continue; }; effect_documents.insert(key.clone(), document); @@ -5531,17 +5545,24 @@ fn append_initial_control_effect_slots( let duration_seconds = header.duration_seconds; if !duration_seconds.is_finite() || duration_seconds < 0.0 - || !matches!(header.time_mode, 0 | 1 | 2) - || (header.time_mode != 0 && duration_seconds <= 0.0) || !header.phase_jitter.is_finite() || !header.random_shift.iter().all(|value| value.is_finite()) || !header.scale.iter().all(|value| value.is_finite()) { - // Control can reference action effects whose header timing and - // scale conventions are outside this ambient subset. Leave them - // unexpanded until their lifetime and transform contract are - // understood rather than failing the whole mission preview. *unsupported_sounds += sound_commands; + record_skip("header_values", document.commands().len(), false, None); + continue; + } + if !matches!(header.time_mode, 0 | 1 | 2) + || (header.time_mode != 0 && duration_seconds <= 0.0) + { + *unsupported_sounds += sound_commands; + record_skip( + "time_mode", + document.commands().len(), + fparkan_fx::fx_time_mode_requires_owner_state(header.time_mode), + None, + ); continue; } let owner_node_index = @@ -5554,6 +5575,7 @@ fn append_initial_control_effect_slots( }); let Some(owner_node_index) = owner_node_index else { *unsupported_sounds += sound_commands; + record_skip("owner_node", document.commands().len(), false, None); continue; }; let owner_node_pose = control_effect_owner_node_pose( @@ -5563,6 +5585,7 @@ fn append_initial_control_effect_slots( ); let Some(owner_node_pose) = owner_node_pose else { *unsupported_sounds += sound_commands; + record_skip("owner_pose", document.commands().len(), false, None); continue; }; let root_transform = LegacyIron3dEulerTransform { @@ -5629,6 +5652,7 @@ fn append_initial_control_effect_slots( }); } else { *unsupported_sounds += 1; + record_skip("sound_body", 1, false, Some(command_index)); eprintln!("unsupported Control Op2 object={object_index} component={component_index} effect={key} command={command_index} (mode, backend parameter or malformed body)"); } } else { @@ -5637,6 +5661,7 @@ fn append_initial_control_effect_slots( continue; } let Some(kind) = ambient_effect_command_kind(command) else { + record_skip("opcode_or_body", 1, false, Some(command_index)); continue; }; if !ambient_effect_render_selector_allows_header( @@ -5646,11 +5671,13 @@ fn append_initial_control_effect_slots( continue; } let Some(reference) = command.resource_refs.first() else { + record_skip("material_reference", 1, false, Some(command_index)); continue; }; let material_archive = String::from_utf8_lossy(reference.archive_name()).into_owned(); let material_name = String::from_utf8_lossy(reference.resource_name()).into_owned(); if material_archive.is_empty() || material_name.is_empty() { + record_skip("empty_material_reference", 1, false, Some(command_index)); continue; } let cache_key = format!("{material_archive}/{material_name}"); @@ -5671,6 +5698,7 @@ fn append_initial_control_effect_slots( &prepared.phase_directional_rgb, &prepared.phase_additive_rgb, ) { + record_skip("material_tint", 1, false, Some(command_index)); continue; } let first_material = *prepared @@ -5688,6 +5716,7 @@ fn append_initial_control_effect_slots( [0.0; 3], AMBIENT_EFFECT_NATIVE_CATEGORY, ) else { + record_skip("mesh_template", 1, false, Some(command_index)); continue; }; Some(template) @@ -5822,6 +5851,7 @@ fn append_initial_control_effect_slots( }); } } + eprintln!("render_coverage control_fx_unsupported={unsupported_commands} control_fx_requires_game_state={state_dependent_commands}"); Ok(()) } diff --git a/crates/fparkan-fx/src/lib.rs b/crates/fparkan-fx/src/lib.rs index af7408a..8a567de 100644 --- a/crates/fparkan-fx/src/lib.rs +++ b/crates/fparkan-fx/src/lib.rs @@ -67,6 +67,12 @@ pub struct FxHeader { pub scale: [f32; 3], } +/// Effect.dll RVA 0x5C60 reads an owner/provider for modes 4 through 17, including mode 14. +#[must_use] +pub const fn fx_time_mode_requires_owner_state(mode: u32) -> bool { + matches!(mode, 4..=17) +} + /// FX opcode. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum FxOpcode { @@ -624,6 +630,16 @@ mod tests { use std::path::{Path, PathBuf}; #[test] + fn phase_sources_distinguish_elapsed_damage_and_progress_modes() { + for mode in [0, 1, 2, 3, 18, u32::MAX] { + assert!(!fx_time_mode_requires_owner_state(mode)); + } + for mode in [4, 5, 9, 13, 14, 15, 16, 17] { + assert!(fx_time_mode_requires_owner_state(mode)); + } + } + + #[test] fn decodes_synthetic_opcodes_and_refs() { let mut bytes = header(2); bytes.extend_from_slice(&command_with_ref_at( diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index ce3400e..a60fa68 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -90,6 +90,9 @@ Control Op1 пока использует цвет и радиус без слу Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные time modes/привязки считаются как `render_coverage control_op2_unsupported`. Управление фазой экземпляра командами Control остаётся до его подключения в runtime. +При расширении выбранных начальных строк все пропущенные команды считаются +в `control_fx_unsupported`, а time modes 4–17 — в `control_fx_requires_game_state`; +причина и владелец выводятся в лог. Неразобранные FXID считаются отдельно в `control_fx_invalid_documents`. Размеры команд, resource references, lifecycle, flags families и используемые time modes известны. Не закрыто значение каждого поля body opcodes 1--10, diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md index f3ecfb4..677c159 100644 --- a/docs/reference/environment-effects.md +++ b/docs/reference/environment-effects.md @@ -420,6 +420,15 @@ FX-команды используют общий preview clock и ограниÑ mode 0 оставляет фазу равной нулю, mode 1 вычисляет `elapsed / duration` без зажатия, mode 2 повторяет цикл. Header flags `0x200` и `0x20` преобразуют эту фазу; command path `0x8120` зажимает результат только после преобразований. +В [эталонной Effect.dll](original-binaries.md) RVA `0x5C60` режимы 4–17 +читают состояние владельца или провайдера; режим 14 тоже входит в эту группу. +Пропуски при расширении выбранных начальных Control-строк учитываются по +командам: `control_fx_requires_game_state` для этих режимов и +`control_fx_unsupported` для остальных непреобразованных команд. Диагностика +сохраняет причину, ресурс, объект, компонент, строку и номер команды. +`control_fx_invalid_documents` отдельно считает неразобранные FXID, где число +команд неизвестно. Счётчики суммируются по компонентам миссии; они не заменяют +учёт ещё не выбранных Control-групп и условных строк в фазе G. Effect renderer проверяет header bit `0x800` только при render selector 0; native AniMesh component path был захвачен с selector 1, поэтому preview использует selector 1 и не подавляет такие visual commands. |
