aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-assets
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-29 19:55:48 +0300
committerValentin Popov <valentin@popov.link>2026-10-29 19:55:48 +0300
commit6c079d9457fddf9927d3e2057463d95c502d4673 (patch)
tree8519a80dd36764d12c053bef9d69ac75dbe74c53 /crates/fparkan-assets
parentba7cb7f0f9f572bf587824cc5816ed933fe74f12 (diff)
downloadfparkan-6c079d9457fddf9927d3e2057463d95c502d4673.tar.xz
fparkan-6c079d9457fddf9927d3e2057463d95c502d4673.zip
feat(runtime): retain Control owner profiles during loading
Diffstat (limited to 'crates/fparkan-assets')
-rw-r--r--crates/fparkan-assets/src/lib.rs76
1 files changed, 76 insertions, 0 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 94018d1..2ee829f 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1081,6 +1081,8 @@ pub struct PreparedVisual {
pub struct PreparedComponentControl {
/// CTLD resource that supplied these bindings.
pub source: ResourceKey,
+ /// The 27 raw native owner-profile words at CTLD bytes 20..128.
+ pub native_profile_words: [u32; 27],
/// Number of nodes in the component model this control targets.
pub model_node_count: usize,
/// Enabled source rows in their original order, including duplicate nodes.
@@ -4191,6 +4193,19 @@ fn decode_component_control_payload(
return Err(invalid("complete binding table extends past the payload"));
}
+ let profile_bytes = payload
+ .get(HEADER_BYTES..HEADER_BYTES + FIXED_SECTION_BYTES)
+ .ok_or_else(|| invalid("fixed owner profile extends past the payload"))?;
+ let mut native_profile_words = [0_u32; FIXED_SECTION_BYTES / 4];
+ for (index, word) in native_profile_words.iter_mut().enumerate() {
+ let offset = index * 4;
+ *word = u32::from_le_bytes(
+ profile_bytes[offset..offset + 4]
+ .try_into()
+ .expect("profile word is exactly four bytes"),
+ );
+ }
+
let mut bindings = Vec::with_capacity(row_count);
let mut timeline_rows = Vec::with_capacity(row_count);
for row_index in 0..row_count {
@@ -4253,6 +4268,7 @@ fn decode_component_control_payload(
Ok(PreparedComponentControl {
source,
+ native_profile_words,
model_node_count,
bindings,
timeline_rows,
@@ -4841,6 +4857,66 @@ mod tests {
}
#[test]
+ fn component_control_preserves_raw_profile_between_header_and_sections() {
+ const PROFILE_START: usize = 20;
+ const PROFILE_WORDS: usize = 27;
+ const SECTION_BYTES: usize = 156 + 16;
+ const MATRIX_BYTES: usize = 4;
+ const ROW_START: usize = PROFILE_START + PROFILE_WORDS * 4 + SECTION_BYTES + MATRIX_BYTES;
+ const ROW_BYTES: usize = 36;
+
+ let source = ResourceKey {
+ archive: parse_path("controls.rlb").expect("archive"),
+ name: resource_name(b"profile.ctl"),
+ type_id: None,
+ };
+ let mut payload = vec![0; ROW_START + ROW_BYTES];
+ payload[0..4].copy_from_slice(&1_u32.to_le_bytes()); // one section
+ payload[4..8].copy_from_slice(&1_u32.to_le_bytes()); // one node per section
+ payload[8..12].copy_from_slice(&1_u32.to_le_bytes()); // one binding row
+ let profile = std::array::from_fn::<_, PROFILE_WORDS, _>(|index| match index {
+ 0 => 0xDEAD_BEEF,
+ 1 => 0x7FC0_1234,
+ index => 0xA500_0000 | u32::try_from(index).expect("index fits u32"),
+ });
+ for (index, word) in profile.iter().enumerate() {
+ let start = PROFILE_START + index * 4;
+ payload[start..start + 4].copy_from_slice(&word.to_le_bytes());
+ }
+ payload[ROW_START..ROW_START + 4].copy_from_slice(&1_u32.to_le_bytes());
+ payload[ROW_START + 4..ROW_START + 8].copy_from_slice(&1.25_f32.to_bits().to_le_bytes());
+ payload[ROW_START + 8..ROW_START + 12].copy_from_slice(&2.5_f32.to_bits().to_le_bytes());
+ payload[ROW_START + 12..ROW_START + 16].copy_from_slice(&0.75_f32.to_bits().to_le_bytes());
+ payload[ROW_START + 0x20..ROW_START + 0x24].copy_from_slice(&1_u32.to_le_bytes());
+
+ let control =
+ decode_component_control_payload(&payload, source, 2, 0).expect("profile CTLD");
+
+ assert_eq!(control.native_profile_words, profile);
+ assert_eq!(control.timeline_rows.len(), 1);
+ assert_eq!(control.timeline_rows[0].node_index, 1);
+ assert_eq!(
+ control.timeline_rows[0].frame_a.to_bits(),
+ 1.25_f32.to_bits()
+ );
+ assert_eq!(
+ control.timeline_rows[0].frame_b.to_bits(),
+ 2.5_f32.to_bits()
+ );
+ }
+
+ #[test]
+ fn component_control_rejects_truncated_native_profile() {
+ let source = ResourceKey {
+ archive: parse_path("controls.rlb").expect("archive"),
+ name: resource_name(b"truncated-profile.ctl"),
+ type_id: None,
+ };
+
+ assert!(decode_component_control_payload(&vec![0; 127], source, 0, 0).is_err());
+ }
+
+ #[test]
fn component_control_rejects_bad_bounds_but_keeps_sampler_inputs_raw() {
let source = ResourceKey {
archive: parse_path("controls.rlb").expect("archive"),