plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plController hierarchy — leaf controllers, compound controllers, TM controllers.
//!
//! C++ ref: plInterp/plController.h/.cpp

use std::io::Read;

use anyhow::{Result, bail};

use crate::core::class_index::ClassIndex;
use crate::resource::prp::PlasmaRead;

use super::keys::{KeyFrame, KeyType, read_keys};

/// A controller — drives a value over time using keyframes.
#[derive(Debug, Clone)]
pub enum Controller {
    /// Leaf controller with typed keyframes.
    Leaf(LeafController),
    /// Compound controller with X/Y/Z sub-controllers.
    Compound(CompoundController),
    /// Transform controller: position + rotation + scale.
    TM(TMController),
}

impl Controller {
    /// Read a controller from a stream (as a creatable: u16 class_index + data).
    /// Returns None for null creatables (class_index = 0x8000).
    pub fn read_creatable(reader: &mut impl Read) -> Result<Option<Self>> {
        let class_idx = reader.read_u16()?;
        if class_idx == 0x8000 {
            return Ok(None); // Null creatable
        }

        log::trace!("Controller: reading class 0x{:04X} ({})",
            class_idx, crate::core::class_index::ClassIndex::class_name(class_idx));

        match class_idx {
            ClassIndex::PL_LEAF_CONTROLLER => {
                Ok(Some(Controller::Leaf(LeafController::read(reader)?)))
            }
            ClassIndex::PL_COMPOUND_CONTROLLER => {
                Ok(Some(Controller::Compound(CompoundController::read(reader)?)))
            }
            ClassIndex::PL_TMCONTROLLER => {
                Ok(Some(Controller::TM(TMController::read(reader)?)))
            }
            // Legacy controller types — read as compound
            ClassIndex::PL_COMPOUND_ROT_CONTROLLER | ClassIndex::PL_COMPOUND_POS_CONTROLLER => {
                Ok(Some(Controller::Compound(CompoundController::read(reader)?)))
            }
            _ => {
                bail!(
                    "Unknown controller class: 0x{:04X} ({})",
                    class_idx,
                    crate::core::class_index::ClassIndex::class_name(class_idx)
                );
            }
        }
    }
}

/// Leaf controller — stores an array of typed interpolation keys.
#[derive(Debug, Clone)]
pub struct LeafController {
    pub key_type: KeyType,
    pub keys: Vec<KeyFrame>,
}

impl LeafController {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let type_byte = reader.read_u8()?;
        let key_type = KeyType::from_u8(type_byte)?;
        let num_keys = reader.read_u32()?;
        let keys = read_keys(reader, key_type, num_keys)?;

        Ok(Self { key_type, keys })
    }

    pub fn num_keys(&self) -> usize {
        self.keys.len()
    }

    pub fn is_empty(&self) -> bool {
        self.keys.is_empty()
    }
}

/// Compound controller — combines X/Y/Z sub-controllers.
#[derive(Debug, Clone)]
pub struct CompoundController {
    pub x_controller: Option<Box<Controller>>,
    pub y_controller: Option<Box<Controller>>,
    pub z_controller: Option<Box<Controller>>,
}

impl CompoundController {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let x = Controller::read_creatable(reader)?.map(Box::new);
        let y = Controller::read_creatable(reader)?.map(Box::new);
        let z = Controller::read_creatable(reader)?.map(Box::new);

        Ok(Self {
            x_controller: x,
            y_controller: y,
            z_controller: z,
        })
    }
}

/// Transform controller — position + rotation + scale controllers.
#[derive(Debug, Clone)]
pub struct TMController {
    pub pos_controller: Option<Box<Controller>>,
    pub rot_controller: Option<Box<Controller>>,
    pub scale_controller: Option<Box<Controller>>,
}

impl TMController {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let pos = Controller::read_creatable(reader)?.map(Box::new);
        let rot = Controller::read_creatable(reader)?.map(Box::new);
        let scale = Controller::read_creatable(reader)?.map(Box::new);

        Ok(Self {
            pos_controller: pos,
            rot_controller: rot,
            scale_controller: scale,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_read_null_creatable() {
        let data = [0x00, 0x80]; // 0x8000 LE = null
        let mut cursor = Cursor::new(&data);
        let ctrl = Controller::read_creatable(&mut cursor).unwrap();
        assert!(ctrl.is_none());
    }

    #[test]
    fn test_read_leaf_scalar() {
        let mut data = Vec::new();
        // Class index for plLeafController
        data.extend_from_slice(&ClassIndex::PL_LEAF_CONTROLLER.to_le_bytes());
        // Key type: scalar (3)
        data.push(3);
        // Num keys: 2
        data.extend_from_slice(&2u32.to_le_bytes());
        // Key 1: frame=0, value=0.0
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend_from_slice(&0.0f32.to_le_bytes());
        // Key 2: frame=30, value=1.0
        data.extend_from_slice(&30u16.to_le_bytes());
        data.extend_from_slice(&1.0f32.to_le_bytes());

        let mut cursor = Cursor::new(&data);
        let ctrl = Controller::read_creatable(&mut cursor).unwrap().unwrap();
        match ctrl {
            Controller::Leaf(leaf) => {
                assert_eq!(leaf.num_keys(), 2);
                assert_eq!(leaf.key_type, KeyType::Scalar);
            }
            _ => panic!("Expected leaf controller"),
        }
    }

    #[test]
    fn test_read_compound() {
        let mut data = Vec::new();
        // Class index for plCompoundController
        data.extend_from_slice(&ClassIndex::PL_COMPOUND_CONTROLLER.to_le_bytes());
        // X controller: null
        data.extend_from_slice(&0x8000u16.to_le_bytes());
        // Y controller: leaf scalar with 1 key
        data.extend_from_slice(&ClassIndex::PL_LEAF_CONTROLLER.to_le_bytes());
        data.push(3); // scalar
        data.extend_from_slice(&1u32.to_le_bytes());
        data.extend_from_slice(&0u16.to_le_bytes());
        data.extend_from_slice(&0.5f32.to_le_bytes());
        // Z controller: null
        data.extend_from_slice(&0x8000u16.to_le_bytes());

        let mut cursor = Cursor::new(&data);
        let ctrl = Controller::read_creatable(&mut cursor).unwrap().unwrap();
        match ctrl {
            Controller::Compound(compound) => {
                assert!(compound.x_controller.is_none());
                assert!(compound.y_controller.is_some());
                assert!(compound.z_controller.is_none());
            }
            _ => panic!("Expected compound controller"),
        }
    }
}