feat(assets): decode CTLD tag 17 records
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
3 files changed
+314
-4
No files matched your search
@@ -3867,7 +3867,7 @@ fn control_i32_at(payload: &[u8], offset: usize) -> Option<i32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] =
|
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] =
|
||||||
&[1, 2, 3, 4, 5, 8, 9, 10, 12, 13, 15, 19, 21, 25, 26, 27];
|
&[1, 2, 3, 4, 5, 8, 9, 10, 12, 13, 15, 17, 19, 21, 25, 26, 27];
|
||||||
const CONTROL_TYPED_RECORD_FIXED_BYTES: usize = 0xB0;
|
const CONTROL_TYPED_RECORD_FIXED_BYTES: usize = 0xB0;
|
||||||
const CONTROL_TYPED_RECORD_COUNT_OFFSET: usize = 0xAC;
|
const CONTROL_TYPED_RECORD_COUNT_OFFSET: usize = 0xAC;
|
||||||
const CONTROL_TYPED_RECORD_LIMIT: usize = 4096;
|
const CONTROL_TYPED_RECORD_LIMIT: usize = 4096;
|
||||||
@@ -4377,7 +4377,8 @@ impl Hasher for StableHasher {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use fparkan_prototype::build_prototype_graph;
|
use fparkan_binary::{sha256, sha256_hex};
|
||||||
|
use fparkan_prototype::{build_prototype_graph, resolve_prototype_all};
|
||||||
use fparkan_resource::{resource_name, CachedResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository};
|
||||||
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs};
|
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -6533,6 +6534,132 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_tag17_shares_common_cursor_and_preserves_raw_record_tails() {
|
||||||
|
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, 3.0, 0.5, 0)]);
|
||||||
|
payload[12..16].copy_from_slice(&2_u32.to_le_bytes());
|
||||||
|
payload[16..20].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
|
||||||
|
let tag17_start = payload.len();
|
||||||
|
append_typed_record(&mut payload, 17, 2);
|
||||||
|
payload[tag17_start + 0x14..tag17_start + 0x18]
|
||||||
|
.copy_from_slice(&0xAABB_CCDDu32.to_le_bytes());
|
||||||
|
payload[tag17_start + 0xAC..tag17_start + 0xB0].copy_from_slice(&2_u32.to_le_bytes());
|
||||||
|
payload.splice(
|
||||||
|
tag17_start + 0xB0..tag17_start + 0xB0,
|
||||||
|
[0x1122_3344u32.to_le_bytes(), 0x5566_7788u32.to_le_bytes()].concat(),
|
||||||
|
);
|
||||||
|
payload.extend_from_slice(b"fx\0");
|
||||||
|
|
||||||
|
let known_start = payload.len();
|
||||||
|
append_typed_record(&mut payload, 4, 3);
|
||||||
|
payload[known_start + 0x28..known_start + 0x2C]
|
||||||
|
.copy_from_slice(&0xCAFE_D00Du32.to_le_bytes());
|
||||||
|
payload[known_start + 0xAC..known_start + 0xB0].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
payload.splice(
|
||||||
|
known_start + 0xB0..known_start + 0xB0,
|
||||||
|
0x0102_0304u32.to_le_bytes(),
|
||||||
|
);
|
||||||
|
payload.extend_from_slice(b"ctl\0");
|
||||||
|
|
||||||
|
let group_header = payload.len();
|
||||||
|
payload.resize(group_header + CONTROL_EFFECT_GROUP_HEADER_BYTES, 0);
|
||||||
|
append_effect_table(
|
||||||
|
&mut payload,
|
||||||
|
&[effect_row(
|
||||||
|
4,
|
||||||
|
[4, 5, 6],
|
||||||
|
[12, 13, 14],
|
||||||
|
15,
|
||||||
|
"effects.rlb",
|
||||||
|
"first",
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let control = decode_component_control_payload(&payload, source, 2, 0)
|
||||||
|
.expect("tag 17 and the following known record share the typed cursor");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
control
|
||||||
|
.typed_records
|
||||||
|
.iter()
|
||||||
|
.map(|record| record.tag)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[17, 4]
|
||||||
|
);
|
||||||
|
assert_eq!(control.typed_records[0].fixed_words[5], 0xAABB_CCDD);
|
||||||
|
assert_eq!(control.typed_records[0].values, [0x1122_3344, 0x5566_7788]);
|
||||||
|
assert_eq!(control.typed_records[0].string_bytes, b"fx");
|
||||||
|
assert_eq!(
|
||||||
|
control.typed_records[0].gate_provider,
|
||||||
|
PreparedControlGateProvider::NotApplicable
|
||||||
|
);
|
||||||
|
assert_eq!(control.typed_records[1].fixed_words[10], 0xCAFE_D00D);
|
||||||
|
assert_eq!(control.typed_records[1].values, [0x0102_0304]);
|
||||||
|
assert_eq!(control.typed_records[1].string_bytes, b"ctl");
|
||||||
|
assert_eq!(control.initial_effect_group, Some(0));
|
||||||
|
assert_eq!(
|
||||||
|
control.effect_section,
|
||||||
|
PreparedControlEffectSection::Parsed(vec![PreparedControlEffectRow {
|
||||||
|
table_index: 0,
|
||||||
|
row_index: 0,
|
||||||
|
raw_condition: [4, 5, 6],
|
||||||
|
point_offsets: [12, 13, 14],
|
||||||
|
selector: 15,
|
||||||
|
resource: PreparedControlResourcePair {
|
||||||
|
archive: fixed_name("effects.rlb"),
|
||||||
|
name: fixed_name("first"),
|
||||||
|
},
|
||||||
|
}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_truncated_tag17_frames_are_rejected() {
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").expect("archive"),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let mut valid = component_control_payload(&[]);
|
||||||
|
valid[12..16].copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
let record_start = valid.len();
|
||||||
|
append_typed_record(&mut valid, 17, 2);
|
||||||
|
valid[record_start + 0xAC..record_start + 0xB0].copy_from_slice(&2_u32.to_le_bytes());
|
||||||
|
valid.splice(
|
||||||
|
record_start + 0xB0..record_start + 0xB0,
|
||||||
|
[0x1234_5678u32.to_le_bytes(), 0x89AB_CDEFu32.to_le_bytes()].concat(),
|
||||||
|
);
|
||||||
|
valid.extend_from_slice(b"xy\0");
|
||||||
|
|
||||||
|
let assert_invalid = |payload: Vec<u8>, label: &str| {
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
decode_component_control_payload(&payload, source.clone(), 0, 0),
|
||||||
|
Err(AssetError::InvalidPrototype(_))
|
||||||
|
),
|
||||||
|
"truncated recognized tag 17 {label} must be InvalidPrototype"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (end, label) in [
|
||||||
|
(record_start + 0xAF, "prefix"),
|
||||||
|
(record_start + 0xB0 + 4, "values"),
|
||||||
|
(record_start + 0xB0 + 8, "length word"),
|
||||||
|
(record_start + 0xB0 + 8 + 4 + 2, "string bytes"),
|
||||||
|
] {
|
||||||
|
assert_invalid(valid[..end].to_vec(), label);
|
||||||
|
}
|
||||||
|
let mut bad_nul = valid;
|
||||||
|
*bad_nul.last_mut().expect("string has trailing NUL") = b'!';
|
||||||
|
assert_invalid(bad_nul, "trailing NUL");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_control_malformed_recognized_typed_tail_is_rejected() {
|
fn component_control_malformed_recognized_typed_tail_is_rejected() {
|
||||||
let source = ResourceKey {
|
let source = ResourceKey {
|
||||||
@@ -8325,6 +8452,165 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires licensed corpus"]
|
||||||
|
fn tag17_bareobject_controls_preserve_explicit_assets_and_real_effect_rows() {
|
||||||
|
let root = fixture_root("IS");
|
||||||
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
|
||||||
|
let repository = CachedResourceRepository::new(Arc::clone(&vfs));
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
"bm_m_01",
|
||||||
|
"bm_m_01.ctl",
|
||||||
|
"bm_m_a.cpt",
|
||||||
|
"bm_m_01.ndp",
|
||||||
|
"e45658e9f3a6b72519eb47bd03e3f41ebde12ab361361297ccb916d0aed5894d",
|
||||||
|
0x3F66_6666,
|
||||||
|
0x458C_A000,
|
||||||
|
["engine_mis_m", "smoke_m_ms_01", "gunf_missile_m"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"bm_l_01",
|
||||||
|
"bm_l_01.ctl",
|
||||||
|
"bm_l_a.cpt",
|
||||||
|
"bm_l_01.ndp",
|
||||||
|
"14fadfa27a4d4eab6ab9758f76a89031e085e880b42085219fa117562ab398cc",
|
||||||
|
0x3F59_999A,
|
||||||
|
0x457A_0000,
|
||||||
|
["engine_mis_l", "smoke_l_ms_01", "gunf_missile_l"],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (
|
||||||
|
root_name,
|
||||||
|
control_name,
|
||||||
|
point_name,
|
||||||
|
ndp_name,
|
||||||
|
payload_hash,
|
||||||
|
word11_bits,
|
||||||
|
word13_bits,
|
||||||
|
names,
|
||||||
|
) in cases
|
||||||
|
{
|
||||||
|
let prototypes = resolve_prototype_all(
|
||||||
|
&repository,
|
||||||
|
vfs.as_ref(),
|
||||||
|
&resource_name(root_name.as_bytes()),
|
||||||
|
)
|
||||||
|
.expect("resolve the original bare-object registry entry");
|
||||||
|
assert_eq!(prototypes.len(), 1, "{root_name}");
|
||||||
|
let prototype = &prototypes[0];
|
||||||
|
let control_source = prototype
|
||||||
|
.control_resource
|
||||||
|
.clone()
|
||||||
|
.expect("bare object has an explicit CTL reference");
|
||||||
|
let point_source = prototype
|
||||||
|
.point_resource
|
||||||
|
.clone()
|
||||||
|
.expect("bare object has an explicit CPT reference");
|
||||||
|
let ndp_source = prototype
|
||||||
|
.node_data_resource
|
||||||
|
.clone()
|
||||||
|
.expect("bare object has an explicit NDP reference");
|
||||||
|
assert_eq!(control_source.archive.as_str(), "weapon.rlb");
|
||||||
|
assert_eq!(control_source.name, resource_name(control_name.as_bytes()));
|
||||||
|
assert_eq!(point_source.archive.as_str(), "weapon.rlb");
|
||||||
|
assert_eq!(point_source.name, resource_name(point_name.as_bytes()));
|
||||||
|
assert_eq!(ndp_source.archive.as_str(), "weapon.rlb");
|
||||||
|
assert_eq!(ndp_source.name, resource_name(ndp_name.as_bytes()));
|
||||||
|
|
||||||
|
let archive_id = repository
|
||||||
|
.open_archive(&control_source.archive)
|
||||||
|
.expect("open the referenced CTL archive");
|
||||||
|
let entry = repository
|
||||||
|
.find(archive_id, &control_source.name)
|
||||||
|
.expect("find the referenced CTL")
|
||||||
|
.expect("referenced CTL exists");
|
||||||
|
let payload = repository.read(entry).expect("read the referenced CTL");
|
||||||
|
assert_eq!(
|
||||||
|
sha256_hex(&sha256(payload.as_slice())),
|
||||||
|
payload_hash,
|
||||||
|
"{root_name} CTL payload hash"
|
||||||
|
);
|
||||||
|
|
||||||
|
let assets = prepare_mission_assets_with_repository(
|
||||||
|
&repository,
|
||||||
|
&[0..prototypes.len()],
|
||||||
|
&prototypes,
|
||||||
|
)
|
||||||
|
.expect("prepare the resolved object through the production asset path");
|
||||||
|
assert_eq!(assets.object_component_controls.len(), 1, "{root_name}");
|
||||||
|
assert_eq!(assets.object_component_controls[0].len(), 1, "{root_name}");
|
||||||
|
let control = assets.object_component_controls[0][0]
|
||||||
|
.as_ref()
|
||||||
|
.expect("the resolved component has prepared control");
|
||||||
|
assert_eq!(control.source, control_source, "{root_name} CTL source");
|
||||||
|
let point_list = control
|
||||||
|
.point_list
|
||||||
|
.as_ref()
|
||||||
|
.expect("the parsed effect rows load the explicitly registered CPT");
|
||||||
|
assert_eq!(point_list.source, point_source, "{root_name} CPT source");
|
||||||
|
assert!(
|
||||||
|
point_list.points.len() >= 3,
|
||||||
|
"{root_name} effect point indices"
|
||||||
|
);
|
||||||
|
let node_data = control
|
||||||
|
.node_data
|
||||||
|
.as_ref()
|
||||||
|
.expect("the explicit NDP remains attached");
|
||||||
|
assert_eq!(node_data.source, ndp_source, "{root_name} NDP source");
|
||||||
|
assert_eq!(node_data.rows.len(), 1, "{root_name} NDP rows");
|
||||||
|
|
||||||
|
assert_eq!(control.typed_records.len(), 1, "{root_name}");
|
||||||
|
let record = &control.typed_records[0];
|
||||||
|
assert_eq!(record.tag, 17, "{root_name}");
|
||||||
|
let mut expected_fixed_words = [0_u32; 44];
|
||||||
|
expected_fixed_words[0] = 17;
|
||||||
|
expected_fixed_words[3..=6].fill(u32::MAX);
|
||||||
|
expected_fixed_words[9] = 0x3F80_0000;
|
||||||
|
expected_fixed_words[10] = 0x3F80_0000;
|
||||||
|
expected_fixed_words[11] = word11_bits;
|
||||||
|
expected_fixed_words[12] = 0x43FA_0000;
|
||||||
|
expected_fixed_words[13] = word13_bits;
|
||||||
|
assert_eq!(
|
||||||
|
record.fixed_words, expected_fixed_words,
|
||||||
|
"{root_name} raw words"
|
||||||
|
);
|
||||||
|
assert!(record.values.is_empty(), "{root_name} values");
|
||||||
|
assert!(record.string_bytes.is_empty(), "{root_name} string");
|
||||||
|
|
||||||
|
let expected_rows = names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(row_index, name)| {
|
||||||
|
let mut raw_name = fixed_name(name);
|
||||||
|
match row_index {
|
||||||
|
0 => raw_name[13..15].copy_from_slice(b"rd"),
|
||||||
|
1 => raw_name[14..16].copy_from_slice(b"rd"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
PreparedControlEffectRow {
|
||||||
|
table_index: 0,
|
||||||
|
row_index,
|
||||||
|
raw_condition: [0; 3],
|
||||||
|
point_offsets: [0, 1, 2],
|
||||||
|
selector: u32::try_from(row_index + 1).expect("small effect selector"),
|
||||||
|
resource: PreparedControlResourcePair {
|
||||||
|
archive: fixed_name("effects.rlb"),
|
||||||
|
name: raw_name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(control.initial_effect_group, Some(0), "{root_name}");
|
||||||
|
assert_eq!(
|
||||||
|
control.effect_section,
|
||||||
|
PreparedControlEffectSection::Parsed(expected_rows),
|
||||||
|
"{root_name} parsed type-4 rows"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn prepares_real_unit_asset_plan() {
|
fn prepares_real_unit_asset_plan() {
|
||||||
|
|||||||
@@ -425,7 +425,9 @@ Global Control getters `1/2` и setter `1` проверены в native x86 orac
|
|||||||
этого класса native oracle не запускал. Rust API обращается к строкам проекции
|
этого класса native oracle не запускал. Rust API обращается к строкам проекции
|
||||||
конкретного живого `ObjectHandle`; это запрос к уже загруженной таблице, а не
|
конкретного живого `ObjectHandle`; это запрос к уже загруженной таблице, а не
|
||||||
исполнение Control или Behavior tick. Setter `17`, остальные factory tags и
|
исполнение Control или Behavior tick. Setter `17`, остальные factory tags и
|
||||||
внешние эффекты их методов не включены. Для tag `9` selector `0` использует
|
внешние эффекты их методов не включены. Структурное чтение записи CTLD с
|
||||||
|
меткой 17 ещё не восстанавливает её обработчики и игровое поведение.
|
||||||
|
Для tag `9` selector `0` использует
|
||||||
отдельный scale `+0x11c`; остальные поддержанные классы используют коэффициент
|
отдельный scale `+0x11c`; остальные поддержанные классы используют коэффициент
|
||||||
напрямую. Пустой набор и нулевой знаменатель остаются native no-result, а
|
напрямую. Пустой набор и нулевой знаменатель остаются native no-result, а
|
||||||
неподдержанный factory tag и расчётный диапазон произведения/отношения за
|
неподдержанный factory tag и расчётный диапазон произведения/отношения за
|
||||||
|
|||||||
@@ -1525,7 +1525,7 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк
|
|||||||
`f_gener_ball` с offset 85.
|
`f_gener_ball` с offset 85.
|
||||||
|
|
||||||
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
|
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
|
||||||
Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель
|
Для tags `2`, `3`, `4`, `5`, `8`, `10`, `17`, `21` и `27` подтверждён общий читатель
|
||||||
`+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через
|
`+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через
|
||||||
обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в
|
обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в
|
||||||
позиции `+0x1C`. Читатель потребляет
|
позиции `+0x1C`. Читатель потребляет
|
||||||
@@ -1536,6 +1536,28 @@ Control.dll `+0x8B10` передаёт указатель текущей зап
|
|||||||
а также отказ при оборванном завершающем NUL. Это подтверждает структурный
|
а также отказ при оборванном завершающем NUL. Это подтверждает структурный
|
||||||
формат, но не назначает смысл всем коэффициентам устройств.
|
формат, но не назначает смысл всем коэффициентам устройств.
|
||||||
|
|
||||||
|
Путь записи с меткой `17` проверен на GOG `Control.dll` с SHA-256
|
||||||
|
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`:
|
||||||
|
ветвь `0x11` фабрики `+0x2D4B0` вызывает конструктор `+0x24760`, который
|
||||||
|
устанавливает указатель на виртуальную таблицу `0x1003C840`. Ячейка `+0x1C`
|
||||||
|
этой таблицы ссылается прямо на общий читатель `+0x21D50`. В ресурсах
|
||||||
|
`weapon.rlb:bm_m_01.ctl` и `weapon.rlb:bm_l_01.ctl` таблица привязок
|
||||||
|
заканчивается на смещении `0x120`, где начинается запись с меткой `17`.
|
||||||
|
Общий префикс размером `0xB0` байт включает саму метку; счётчик по `+0xAC` и
|
||||||
|
слово длины в этих двух ресурсах равны нулю, поэтому указатель чтения
|
||||||
|
переходит на `0x1D4`. Там начинаются `0x54` байта заголовка таблиц эффектов;
|
||||||
|
таблицы содержат соответственно `4`, `2`, `2` и `2` строки по 100 байт и
|
||||||
|
заканчиваются на `0x620`. Записи из реестра объектов `bm_m_01` и `bm_l_01`
|
||||||
|
при обычной подготовке ресурсов сохраняют явно указанные списки точек CPT
|
||||||
|
(`bm_m_a.cpt` и `bm_l_a.cpt`) и таблицы узлов NDP (`bm_m_01.ndp` и
|
||||||
|
`bm_l_01.ndp`); в первой таблице эффектов находятся три строки type 4.
|
||||||
|
Синтетический тест проверяет непустые массивы значений и строки у записи с
|
||||||
|
меткой 17 и следующей записи с меткой 4, затем переход к таблице эффектов.
|
||||||
|
Если считать 17 неизвестной, чтение остановится перед
|
||||||
|
ней; общий разбор позволяет пройти запись и прочитать следующие таблицы,
|
||||||
|
оставляя слова tag 17 числами без толкования. Обработчики этой метки и их
|
||||||
|
игровое поведение ещё не восстановлены.
|
||||||
|
|
||||||
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
|
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
|
||||||
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
|
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
|
||||||
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT
|
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT
|
||||||
|
|||||||
Reference in new issue
Block a user