Skip to main content

plasma_prp/animation/
controller.rs

1//! plController hierarchy — leaf controllers, compound controllers, TM controllers.
2//!
3//! C++ ref: plInterp/plController.h/.cpp
4
5use std::io::Read;
6
7use anyhow::{Result, bail};
8
9use crate::core::class_index::ClassIndex;
10use crate::resource::prp::PlasmaRead;
11
12use super::keys::{KeyFrame, KeyType, read_keys};
13
14/// A controller — drives a value over time using keyframes.
15#[derive(Debug, Clone)]
16pub enum Controller {
17    /// Leaf controller with typed keyframes.
18    Leaf(LeafController),
19    /// Compound controller with X/Y/Z sub-controllers.
20    Compound(CompoundController),
21    /// Transform controller: position + rotation + scale.
22    TM(TMController),
23}
24
25impl Controller {
26    /// Read a controller from a stream (as a creatable: u16 class_index + data).
27    /// Returns None for null creatables (class_index = 0x8000).
28    pub fn read_creatable(reader: &mut impl Read) -> Result<Option<Self>> {
29        let class_idx = reader.read_u16()?;
30        if class_idx == 0x8000 {
31            return Ok(None); // Null creatable
32        }
33
34        log::trace!("Controller: reading class 0x{:04X} ({})",
35            class_idx, crate::core::class_index::ClassIndex::class_name(class_idx));
36
37        match class_idx {
38            ClassIndex::PL_LEAF_CONTROLLER => {
39                Ok(Some(Controller::Leaf(LeafController::read(reader)?)))
40            }
41            ClassIndex::PL_COMPOUND_CONTROLLER => {
42                Ok(Some(Controller::Compound(CompoundController::read(reader)?)))
43            }
44            ClassIndex::PL_TMCONTROLLER => {
45                Ok(Some(Controller::TM(TMController::read(reader)?)))
46            }
47            // Legacy controller types — read as compound
48            ClassIndex::PL_COMPOUND_ROT_CONTROLLER | ClassIndex::PL_COMPOUND_POS_CONTROLLER => {
49                Ok(Some(Controller::Compound(CompoundController::read(reader)?)))
50            }
51            _ => {
52                bail!(
53                    "Unknown controller class: 0x{:04X} ({})",
54                    class_idx,
55                    crate::core::class_index::ClassIndex::class_name(class_idx)
56                );
57            }
58        }
59    }
60}
61
62/// Leaf controller — stores an array of typed interpolation keys.
63#[derive(Debug, Clone)]
64pub struct LeafController {
65    pub key_type: KeyType,
66    pub keys: Vec<KeyFrame>,
67}
68
69impl LeafController {
70    pub fn read(reader: &mut impl Read) -> Result<Self> {
71        let type_byte = reader.read_u8()?;
72        let key_type = KeyType::from_u8(type_byte)?;
73        let num_keys = reader.read_u32()?;
74        let keys = read_keys(reader, key_type, num_keys)?;
75
76        Ok(Self { key_type, keys })
77    }
78
79    pub fn num_keys(&self) -> usize {
80        self.keys.len()
81    }
82
83    pub fn is_empty(&self) -> bool {
84        self.keys.is_empty()
85    }
86}
87
88/// Compound controller — combines X/Y/Z sub-controllers.
89#[derive(Debug, Clone)]
90pub struct CompoundController {
91    pub x_controller: Option<Box<Controller>>,
92    pub y_controller: Option<Box<Controller>>,
93    pub z_controller: Option<Box<Controller>>,
94}
95
96impl CompoundController {
97    pub fn read(reader: &mut impl Read) -> Result<Self> {
98        let x = Controller::read_creatable(reader)?.map(Box::new);
99        let y = Controller::read_creatable(reader)?.map(Box::new);
100        let z = Controller::read_creatable(reader)?.map(Box::new);
101
102        Ok(Self {
103            x_controller: x,
104            y_controller: y,
105            z_controller: z,
106        })
107    }
108}
109
110/// Transform controller — position + rotation + scale controllers.
111#[derive(Debug, Clone)]
112pub struct TMController {
113    pub pos_controller: Option<Box<Controller>>,
114    pub rot_controller: Option<Box<Controller>>,
115    pub scale_controller: Option<Box<Controller>>,
116}
117
118impl TMController {
119    pub fn read(reader: &mut impl Read) -> Result<Self> {
120        let pos = Controller::read_creatable(reader)?.map(Box::new);
121        let rot = Controller::read_creatable(reader)?.map(Box::new);
122        let scale = Controller::read_creatable(reader)?.map(Box::new);
123
124        Ok(Self {
125            pos_controller: pos,
126            rot_controller: rot,
127            scale_controller: scale,
128        })
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::io::Cursor;
136
137    #[test]
138    fn test_read_null_creatable() {
139        let data = [0x00, 0x80]; // 0x8000 LE = null
140        let mut cursor = Cursor::new(&data);
141        let ctrl = Controller::read_creatable(&mut cursor).unwrap();
142        assert!(ctrl.is_none());
143    }
144
145    #[test]
146    fn test_read_leaf_scalar() {
147        let mut data = Vec::new();
148        // Class index for plLeafController
149        data.extend_from_slice(&ClassIndex::PL_LEAF_CONTROLLER.to_le_bytes());
150        // Key type: scalar (3)
151        data.push(3);
152        // Num keys: 2
153        data.extend_from_slice(&2u32.to_le_bytes());
154        // Key 1: frame=0, value=0.0
155        data.extend_from_slice(&0u16.to_le_bytes());
156        data.extend_from_slice(&0.0f32.to_le_bytes());
157        // Key 2: frame=30, value=1.0
158        data.extend_from_slice(&30u16.to_le_bytes());
159        data.extend_from_slice(&1.0f32.to_le_bytes());
160
161        let mut cursor = Cursor::new(&data);
162        let ctrl = Controller::read_creatable(&mut cursor).unwrap().unwrap();
163        match ctrl {
164            Controller::Leaf(leaf) => {
165                assert_eq!(leaf.num_keys(), 2);
166                assert_eq!(leaf.key_type, KeyType::Scalar);
167            }
168            _ => panic!("Expected leaf controller"),
169        }
170    }
171
172    #[test]
173    fn test_read_compound() {
174        let mut data = Vec::new();
175        // Class index for plCompoundController
176        data.extend_from_slice(&ClassIndex::PL_COMPOUND_CONTROLLER.to_le_bytes());
177        // X controller: null
178        data.extend_from_slice(&0x8000u16.to_le_bytes());
179        // Y controller: leaf scalar with 1 key
180        data.extend_from_slice(&ClassIndex::PL_LEAF_CONTROLLER.to_le_bytes());
181        data.push(3); // scalar
182        data.extend_from_slice(&1u32.to_le_bytes());
183        data.extend_from_slice(&0u16.to_le_bytes());
184        data.extend_from_slice(&0.5f32.to_le_bytes());
185        // Z controller: null
186        data.extend_from_slice(&0x8000u16.to_le_bytes());
187
188        let mut cursor = Cursor::new(&data);
189        let ctrl = Controller::read_creatable(&mut cursor).unwrap().unwrap();
190        match ctrl {
191            Controller::Compound(compound) => {
192                assert!(compound.x_controller.is_none());
193                assert!(compound.y_controller.is_some());
194                assert!(compound.z_controller.is_none());
195            }
196            _ => panic!("Expected compound controller"),
197        }
198    }
199}