chore(render): report unexpanded ambient effects
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s
This commit is contained in:
4 files changed
+64
-6
No files matched your search
@@ -5498,6 +5498,8 @@ fn append_initial_control_effect_slots(
|
|||||||
let Some(point_list) = control.point_list.as_ref() else {
|
let Some(point_list) = control.point_list.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
let mut unsupported_commands = 0;
|
||||||
|
let mut state_dependent_commands = 0;
|
||||||
for row in
|
for row in
|
||||||
selected_initial_control_effect_rows(control.initial_effect_group, &control.effect_section)
|
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 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) {
|
if !effect_documents.contains_key(&key) {
|
||||||
let bytes = load_resource_entry_bytes_from_root(root, &archive, &name)
|
let bytes = load_resource_entry_bytes_from_root(root, &archive, &name)
|
||||||
.map_err(|error| format!("load Control FX {key}: {error}"))?;
|
.map_err(|error| format!("load Control FX {key}: {error}"))?;
|
||||||
let Ok(document) = decode_fxid(bytes) else {
|
let Ok(document) = decode_fxid(bytes) else {
|
||||||
|
eprintln!("render_coverage control_fx_invalid_documents=1 (object:{object_index} component:{component_index} source:{key})");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
effect_documents.insert(key.clone(), document);
|
effect_documents.insert(key.clone(), document);
|
||||||
@@ -5531,17 +5545,24 @@ fn append_initial_control_effect_slots(
|
|||||||
let duration_seconds = header.duration_seconds;
|
let duration_seconds = header.duration_seconds;
|
||||||
if !duration_seconds.is_finite()
|
if !duration_seconds.is_finite()
|
||||||
|| duration_seconds < 0.0
|
|| 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.phase_jitter.is_finite()
|
||||||
|| !header.random_shift.iter().all(|value| value.is_finite())
|
|| !header.random_shift.iter().all(|value| value.is_finite())
|
||||||
|| !header.scale.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;
|
*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;
|
continue;
|
||||||
}
|
}
|
||||||
let owner_node_index =
|
let owner_node_index =
|
||||||
@@ -5554,6 +5575,7 @@ fn append_initial_control_effect_slots(
|
|||||||
});
|
});
|
||||||
let Some(owner_node_index) = owner_node_index else {
|
let Some(owner_node_index) = owner_node_index else {
|
||||||
*unsupported_sounds += sound_commands;
|
*unsupported_sounds += sound_commands;
|
||||||
|
record_skip("owner_node", document.commands().len(), false, None);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let owner_node_pose = control_effect_owner_node_pose(
|
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 {
|
let Some(owner_node_pose) = owner_node_pose else {
|
||||||
*unsupported_sounds += sound_commands;
|
*unsupported_sounds += sound_commands;
|
||||||
|
record_skip("owner_pose", document.commands().len(), false, None);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let root_transform = LegacyIron3dEulerTransform {
|
let root_transform = LegacyIron3dEulerTransform {
|
||||||
@@ -5629,6 +5652,7 @@ fn append_initial_control_effect_slots(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
*unsupported_sounds += 1;
|
*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)");
|
eprintln!("unsupported Control Op2 object={object_index} component={component_index} effect={key} command={command_index} (mode, backend parameter or malformed body)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -5637,6 +5661,7 @@ fn append_initial_control_effect_slots(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(kind) = ambient_effect_command_kind(command) else {
|
let Some(kind) = ambient_effect_command_kind(command) else {
|
||||||
|
record_skip("opcode_or_body", 1, false, Some(command_index));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !ambient_effect_render_selector_allows_header(
|
if !ambient_effect_render_selector_allows_header(
|
||||||
@@ -5646,11 +5671,13 @@ fn append_initial_control_effect_slots(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(reference) = command.resource_refs.first() else {
|
let Some(reference) = command.resource_refs.first() else {
|
||||||
|
record_skip("material_reference", 1, false, Some(command_index));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let material_archive = String::from_utf8_lossy(reference.archive_name()).into_owned();
|
let material_archive = String::from_utf8_lossy(reference.archive_name()).into_owned();
|
||||||
let material_name = String::from_utf8_lossy(reference.resource_name()).into_owned();
|
let material_name = String::from_utf8_lossy(reference.resource_name()).into_owned();
|
||||||
if material_archive.is_empty() || material_name.is_empty() {
|
if material_archive.is_empty() || material_name.is_empty() {
|
||||||
|
record_skip("empty_material_reference", 1, false, Some(command_index));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let cache_key = format!("{material_archive}/{material_name}");
|
let cache_key = format!("{material_archive}/{material_name}");
|
||||||
@@ -5671,6 +5698,7 @@ fn append_initial_control_effect_slots(
|
|||||||
&prepared.phase_directional_rgb,
|
&prepared.phase_directional_rgb,
|
||||||
&prepared.phase_additive_rgb,
|
&prepared.phase_additive_rgb,
|
||||||
) {
|
) {
|
||||||
|
record_skip("material_tint", 1, false, Some(command_index));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let first_material = *prepared
|
let first_material = *prepared
|
||||||
@@ -5688,6 +5716,7 @@ fn append_initial_control_effect_slots(
|
|||||||
[0.0; 3],
|
[0.0; 3],
|
||||||
AMBIENT_EFFECT_NATIVE_CATEGORY,
|
AMBIENT_EFFECT_NATIVE_CATEGORY,
|
||||||
) else {
|
) else {
|
||||||
|
record_skip("mesh_template", 1, false, Some(command_index));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
Some(template)
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ pub struct FxHeader {
|
|||||||
pub scale: [f32; 3],
|
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.
|
/// FX opcode.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub enum FxOpcode {
|
pub enum FxOpcode {
|
||||||
@@ -623,6 +629,16 @@ mod tests {
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
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]
|
#[test]
|
||||||
fn decodes_synthetic_opcodes_and_refs() {
|
fn decodes_synthetic_opcodes_and_refs() {
|
||||||
let mut bytes = header(2);
|
let mut bytes = header(2);
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ Control Op1 пока использует цвет и радиус без слу
|
|||||||
Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
|
Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные
|
||||||
time modes/привязки считаются как `render_coverage control_op2_unsupported`.
|
time modes/привязки считаются как `render_coverage control_op2_unsupported`.
|
||||||
Управление фазой экземпляра командами Control остаётся до его подключения в runtime.
|
Управление фазой экземпляра командами Control остаётся до его подключения в runtime.
|
||||||
|
При расширении выбранных начальных строк все пропущенные команды считаются
|
||||||
|
в `control_fx_unsupported`, а time modes 4–17 — в `control_fx_requires_game_state`;
|
||||||
|
причина и владелец выводятся в лог. Неразобранные FXID считаются отдельно в `control_fx_invalid_documents`.
|
||||||
|
|
||||||
Размеры команд, resource references, lifecycle, flags families и используемые
|
Размеры команд, resource references, lifecycle, flags families и используемые
|
||||||
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
|
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
|
||||||
|
|||||||
@@ -420,6 +420,15 @@ FX-команды используют общий preview clock и ограни
|
|||||||
mode 0 оставляет фазу равной нулю, mode 1 вычисляет `elapsed / duration` без
|
mode 0 оставляет фазу равной нулю, mode 1 вычисляет `elapsed / duration` без
|
||||||
зажатия, mode 2 повторяет цикл. Header flags `0x200` и `0x20` преобразуют эту
|
зажатия, mode 2 повторяет цикл. Header flags `0x200` и `0x20` преобразуют эту
|
||||||
фазу; command path `0x8120` зажимает результат только после преобразований.
|
фазу; 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` только при
|
Effect renderer проверяет header bit `0x800` только при
|
||||||
render selector 0; native AniMesh component path был захвачен с selector 1,
|
render selector 0; native AniMesh component path был захвачен с selector 1,
|
||||||
поэтому preview использует selector 1 и не подавляет такие visual commands.
|
поэтому preview использует selector 1 и не подавляет такие visual commands.
|
||||||
|
|||||||
Reference in new issue
Block a user