diff --git a/crates/fparkan-fx/src/lib.rs b/crates/fparkan-fx/src/lib.rs index 437ae5c..2ef5b56 100644 --- a/crates/fparkan-fx/src/lib.rs +++ b/crates/fparkan-fx/src/lib.rs @@ -99,7 +99,8 @@ pub struct FxCommand { pub native_flag: bool, /// Command body after the word. pub raw_body: Arc<[u8]>, - /// Resource references discovered in known command layouts. + /// Resource references discovered in known command layouts, using + /// offsets relative to `raw_body` (the first byte after the command word). pub resource_refs: Vec, } @@ -549,11 +550,11 @@ fn command_size(opcode: FxOpcode) -> usize { } fn resource_refs(index: u32, opcode: FxOpcode, body: &[u8]) -> Result, FxError> { - if !has_resource_ref(opcode) { + let Some(offset) = resource_ref_offset(opcode) else { return Ok(Vec::new()); - } + }; let raw = body - .get(..64) + .get(offset..offset + 64) .ok_or(FxError::InvalidResourceRef { index, opcode })?; let mut archive_raw = [0; 32]; let mut name_raw = [0; 32]; @@ -565,18 +566,19 @@ fn resource_refs(index: u32, opcode: FxOpcode, body: &[u8]) -> Result bool { - matches!( - opcode, - FxOpcode::Op2 - | FxOpcode::Op3 - | FxOpcode::Op4 - | FxOpcode::Op5 - | FxOpcode::Op7 - | FxOpcode::Op8 - | FxOpcode::Op9 - | FxOpcode::Op10 - ) +/// Known fixed resource fields, relative to the command body. Native loaders +/// receive the command-record pointer, which starts with the 4-byte command +/// word, so their record-relative offsets are four bytes larger: Op2 `0x54`, +/// Op3/4/9 `0x88`, Op5 `0x30`, Op7/10 `0x90`, and Op8 `0xB8`. +fn resource_ref_offset(opcode: FxOpcode) -> Option { + match opcode { + FxOpcode::Op2 => Some(0x50), + FxOpcode::Op3 | FxOpcode::Op4 | FxOpcode::Op9 => Some(0x84), + FxOpcode::Op5 => Some(0x2C), + FxOpcode::Op7 | FxOpcode::Op10 => Some(0x8C), + FxOpcode::Op8 => Some(0xB4), + FxOpcode::Op1 | FxOpcode::Op6 => None, + } } fn bounded_cstr(raw: &[u8]) -> &[u8] { @@ -606,7 +608,13 @@ mod tests { #[test] fn decodes_synthetic_opcodes_and_refs() { let mut bytes = header(2); - bytes.extend_from_slice(&command_with_ref(0x0102, 148, b"sounds.lib", b"boom.wav")); + bytes.extend_from_slice(&command_with_ref_at( + 0x0102, + 148, + 0x50, + b"sounds.lib", + b"boom.wav", + )); bytes.extend_from_slice(&command(0x0106, 4)); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); @@ -706,27 +714,81 @@ mod tests { } #[test] - fn fixed_resource_refs_preserve_tails() { - let mut bytes = header(1); - let mut command = command_with_ref(0x0102, 148, b"sounds.lib", b"boom.wav"); - command[4 + 20] = 0xAB; - command[36 + 20] = 0xCD; - bytes.extend_from_slice(&command); + fn mixed_opcode_resource_refs_use_body_offsets_and_preserve_raw_tails() { + let layouts = [ + (2, 0x50, b"sounds.lib".as_slice(), b"boom.wav".as_slice()), + (3, 0x84, b"material.lib".as_slice(), b"jet_r".as_slice()), + (4, 0x84, b"material.lib".as_slice(), b"glow_y".as_slice()), + (5, 0x2C, b"material.lib".as_slice(), b"shook2_b".as_slice()), + ( + 7, + 0x8C, + b"material.lib".as_slice(), + b"fire_smoke".as_slice(), + ), + ( + 8, + 0xB4, + b"material.lib".as_slice(), + b"fire_smoke".as_slice(), + ), + ( + 9, + 0x84, + b"material.lib".as_slice(), + b"laser_y_hit".as_slice(), + ), + ( + 10, + 0x8C, + b"material.lib".as_slice(), + b"NE_Gibs_Aln".as_slice(), + ), + ]; + let mut bytes = header(u32::try_from(layouts.len() + 1).expect("command count")); + for (opcode, offset, archive, name) in layouts { + let mut record = command(0x0100 | opcode, opcode_size(opcode)); + let body = &mut record[4..]; + for (index, word) in body[..64].chunks_exact_mut(4).enumerate() { + word.copy_from_slice(&0x3F80_0000_u32.wrapping_add(index as u32).to_le_bytes()); + } + body[offset..offset + 64].fill(0); + copy_cstr(&mut body[offset..offset + 32], archive); + copy_cstr(&mut body[offset + 32..offset + 64], name); + body[offset + 20] = 0xAB; + body[offset + 52] = 0xCD; + bytes.extend_from_slice(&record); + } + let mut op1 = command(0x0101, opcode_size(1)); + for (index, word) in op1[4..68].chunks_exact_mut(4).enumerate() { + word.copy_from_slice(&0x3F80_0000_u32.wrapping_add(index as u32).to_le_bytes()); + } + bytes.extend_from_slice(&op1); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); - let reference = &document.commands()[0].resource_refs[0]; - assert_eq!(reference.archive_name(), b"sounds.lib"); - assert_eq!(reference.resource_name(), b"boom.wav"); - assert_eq!(reference.archive_raw[20], 0xAB); - assert_eq!(reference.name_raw[20], 0xCD); + for (command, (_, _, archive, name)) in document.commands().iter().zip(layouts) { + assert_eq!(command.resource_refs.len(), 1); + let reference = &command.resource_refs[0]; + assert_eq!(reference.archive_name(), archive); + assert_eq!(reference.resource_name(), name); + assert_eq!(reference.archive_raw[20], 0xAB); + assert_eq!(reference.name_raw[20], 0xCD); + } + assert!(document + .commands() + .last() + .expect("Op1") + .resource_refs + .is_empty()); } #[test] fn missing_dependency_error_contains_effect_command_archive_and_name() { let mut bytes = header(1); - bytes.extend_from_slice(&command_with_ref( + bytes.extend_from_slice(&command_with_ref_at( 0x0102, 148, + 0x50, b"sounds.lib", b"missing.wav", )); @@ -988,10 +1050,17 @@ mod tests { out } - fn command_with_ref(word: u32, size: usize, archive: &[u8], name: &[u8]) -> Vec { + fn command_with_ref_at( + word: u32, + size: usize, + body_offset: usize, + archive: &[u8], + name: &[u8], + ) -> Vec { let mut out = command(word, size); - copy_cstr(&mut out[4..36], archive); - copy_cstr(&mut out[36..68], name); + let start = 4 + body_offset; + copy_cstr(&mut out[start..start + 32], archive); + copy_cstr(&mut out[start + 32..start + 64], name); out } diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index b104f7a..01038b7 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -735,10 +735,27 @@ struct FxResourceRef64 { }; ``` -Имена сравниваются case-insensitive по ASCII, а tail после первого nul byte -сохраняется. Resolve выполняется при создании command object или лениво при -первом запуске, но ошибка должна включать имя эффекта, номер команды, archive -и resource name. +Offsets относятся к body команды, сразу после `command_word`. Native loaders +получают pointer на всю запись, поэтому record offset на 4 байта больше: + +| Opcode | Body offset | Record offset | +| --- | ---: | ---: | +| 2 | `0x50` | `0x54` | +| 3 | `0x84` | `0x88` | +| 4 | `0x84` | `0x88` | +| 5 | `0x2C` | `0x30` | +| 7 | `0x8C` | `0x90` | +| 8 | `0xB4` | `0xB8` | +| 9 | `0x84` | `0x88` | +| 10 | `0x8C` | `0x90` | + +Parser сохраняет все 64 байта пары, включая suffix после первого nul byte и +пустые поля. Декодированные имена заканчиваются на первом nul byte. +Извлечение dependency не подтверждает визуальную parity команд. + +Имена сравниваются case-insensitive по ASCII. Resolve выполняется при создании +command object или лениво при первом запуске, но ошибка должна включать имя +эффекта, номер команды, archive и resource name. Базовый normalized age: