aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-18 10:42:05 +0300
committerValentin Popov <valentin@popov.link>2026-10-18 10:42:05 +0300
commitec62f4f2e2c8d64107d17e54908c1c28a52fa1b0 (patch)
tree305b049bc8a0b8466910d604fa6e6de05137ee0a
parent6a0cf0c37465e8ffaa08c75a5912f599309c5050 (diff)
downloadfparkan-ec62f4f2e2c8d64107d17e54908c1c28a52fa1b0.tar.xz
fparkan-ec62f4f2e2c8d64107d17e54908c1c28a52fa1b0.zip
fix(assets): parse native CTL tags 2 and 5
-rw-r--r--crates/fparkan-assets/src/lib.rs75
-rw-r--r--docs/tomes/06-behavior.md9
2 files changed, 62 insertions, 22 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 4d7be69..7ef546f 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -3599,7 +3599,7 @@ fn control_i32_at(payload: &[u8], offset: usize) -> Option<i32> {
))
}
-const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] = &[1, 8, 9, 12, 13, 15, 19, 21, 25, 26];
+const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] = &[1, 2, 5, 8, 9, 12, 13, 15, 19, 21, 25, 26];
const CONTROL_TYPED_RECORD_FIXED_BYTES: usize = 0xB0;
const CONTROL_TYPED_RECORD_COUNT_OFFSET: usize = 0xAC;
const CONTROL_TYPED_RECORD_LIMIT: usize = 4096;
@@ -4297,32 +4297,30 @@ mod tests {
}
#[test]
- fn component_control_unproven_effect_tags_preserve_node_bindings() {
+ fn component_control_unknown_effect_tag_preserves_node_bindings() {
let source = ResourceKey {
archive: parse_path("controls.rlb").expect("archive"),
name: resource_name(b"parts.ctl"),
type_id: None,
};
- for tag in [2_u32, 5, 0xDEAD_BEEF] {
- 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(&1_u32.to_le_bytes());
- payload.extend_from_slice(&tag.to_le_bytes());
-
- let control = decode_component_control_payload(&payload, source.clone(), 2, 3)
- .expect("unproven tag is preserved as unsupported");
-
- assert_eq!(control.bindings.len(), 1);
- assert_eq!(control.bindings[0].node_index, 1);
- assert_eq!(control.bindings[0].frame_b, 1.0);
- assert_eq!(
- control.effect_section,
- PreparedControlEffectSection::Unsupported {
- typed_record_index: Some(0),
- typed_record_tag: Some(tag),
- }
- );
- }
+ 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(&1_u32.to_le_bytes());
+ payload.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
+
+ let control = decode_component_control_payload(&payload, source.clone(), 2, 3)
+ .expect("unknown tag is retained");
+
+ assert_eq!(control.bindings.len(), 1);
+ assert_eq!(control.bindings[0].node_index, 1);
+ assert_eq!(control.bindings[0].frame_b, 1.0);
+ assert_eq!(
+ control.effect_section,
+ PreparedControlEffectSection::Unsupported {
+ typed_record_index: Some(0),
+ typed_record_tag: Some(0xDEAD_BEEF),
+ }
+ );
}
#[test]
@@ -4351,6 +4349,39 @@ mod tests {
}
#[test]
+ fn component_control_tags2_and5_share_the_typed_cursor_parser() {
+ 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(&3_u32.to_le_bytes());
+ append_typed_record(&mut payload, 2, 0);
+ append_typed_record(&mut payload, 5, 2);
+ payload.extend_from_slice(b"fx\0");
+ append_typed_record(&mut payload, 21, 0);
+
+ let control = decode_component_control_payload(&payload, source, 2, 0)
+ .expect("tags 2 and 5 share the native typed cursor parser");
+
+ assert_eq!(
+ control
+ .typed_records
+ .iter()
+ .map(|record| record.tag)
+ .collect::<Vec<_>>(),
+ [2, 5, 21],
+ "tag 21 must begin at the cursor returned after tag 5's string"
+ );
+ assert_eq!(control.typed_records[1].string_bytes, b"fx");
+ assert_eq!(
+ control.effect_section,
+ PreparedControlEffectSection::Parsed(Vec::new())
+ );
+ }
+
+ #[test]
fn component_control_malformed_recognized_typed_tail_is_rejected() {
let source = ResourceKey {
archive: parse_path("controls.rlb").expect("archive"),
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index e663389..ebecddd 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -424,6 +424,15 @@ type 4, в том числе четыре `f_smalllight_y` с offsets 104–107
offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, включая
`f_gener_ball` с offset 85.
+Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
+Теги 2 и 5 направляются в общий читатель `+0x21D50`: таблица виртуальных методов
+тега 2 (`+0x3CBD8`) вызывает его через обёртку `+0x29650`, а таблица тега 5
+(`+0x3CDF8`) указывает прямо на него в позиции `+0x1C`. Читатель потребляет
+заголовок размером `0xB0` байт со счётчиком `u32` по `+0xAC`, затем `count`
+значений `u32`, слово длины и, при ненулевой длине, строку с завершающим NUL;
+указатель возврата указывает на следующую запись. Поэтому для тегов 2 и 5 подтверждён общий
+структурный формат; смысл их полей пока не назначен.
+
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT