plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plAnimTimeConvert — converts world time to local animation time.
//!
//! Handles speed, looping, ease-in/out curves, stop points, and wrap modes.
//!
//! C++ ref: plInterp/plAnimTimeConvert.h/.cpp, plATCEaseCurves.cpp

use std::io::Read;

use anyhow::Result;

use crate::resource::prp::PlasmaRead;

/// Animation time convert flags.
#[allow(dead_code)]
pub mod atc_flags {
    pub const NONE: u32 = 0x00;
    pub const STOPPED: u32 = 0x01;
    pub const LOOP: u32 = 0x02;
    pub const BACKWARDS: u32 = 0x04;
    pub const WRAP: u32 = 0x08;
    pub const NEEDS_RESET: u32 = 0x10;
    pub const EASING_IN: u32 = 0x20;
    pub const FORCED_MOVE: u32 = 0x40;
    pub const NO_CALLBACKS: u32 = 0x80;
    pub const FLAGS_MASK: u32 = 0xFF;
}

/// Ease curve types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EaseType {
    None = 0,
    ConstAccel = 1,
    Spline = 2,
}

/// Ease curve data.
#[derive(Debug, Clone)]
pub struct EaseCurve {
    pub min_length: f32,
    pub max_length: f32,
    pub norm_length: f32,
    pub start_speed: f32,
    pub speed: f32,
    pub begin_world_time: f64,
    /// Spline coefficients (only for spline ease curves).
    pub spline_coefs: Option<[f32; 4]>,
}

impl EaseCurve {
    pub fn read_creatable(reader: &mut impl Read) -> Result<Option<Self>> {
        let class_idx = reader.read_u16()?;
        if class_idx == 0x8000 {
            return Ok(None);
        }

        let min_length = reader.read_f32()?;
        let max_length = reader.read_f32()?;
        let norm_length = reader.read_f32()?;
        let start_speed = reader.read_f32()?;
        let speed = reader.read_f32()?;
        let begin_world_time = read_f64(reader)?;

        // Check if this is a spline ease curve
        let spline_coefs =
            if class_idx == crate::core::class_index::ClassIndex::PL_SPLINE_EASE_CURVE {
                Some([
                    reader.read_f32()?,
                    reader.read_f32()?,
                    reader.read_f32()?,
                    reader.read_f32()?,
                ])
            } else {
                None
            };

        Ok(Some(Self {
            min_length,
            max_length,
            norm_length,
            start_speed,
            speed,
            begin_world_time,
            spline_coefs,
        }))
    }
}

/// Parsed plAnimTimeConvert data.
#[derive(Debug, Clone)]
pub struct AnimTimeConvertData {
    pub flags: u32,
    pub begin: f32,
    pub end: f32,
    pub loop_end: f32,
    pub loop_begin: f32,
    pub speed: f32,
    pub ease_in: Option<EaseCurve>,
    pub ease_out: Option<EaseCurve>,
    pub speed_ease: Option<EaseCurve>,
    pub current_anim_time: f32,
    pub last_eval_world_time: f64,
    pub stop_points: Vec<f32>,
}

impl AnimTimeConvertData {
    /// Read a plAnimTimeConvert from a stream (as a creatable — class index already consumed).
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let flags = reader.read_u32()?;
        let begin = reader.read_f32()?;
        let end = reader.read_f32()?;
        let loop_end = reader.read_f32()?;
        let loop_begin = reader.read_f32()?;
        let speed = reader.read_f32()?;

        let ease_in = EaseCurve::read_creatable(reader)?;
        let ease_out = EaseCurve::read_creatable(reader)?;
        let speed_ease = EaseCurve::read_creatable(reader)?;

        let current_anim_time = reader.read_f32()?;
        let last_eval_world_time = read_f64(reader)?;

        // Callback messages (skip — they're creatables we can't fully parse yet)
        let num_callbacks = reader.read_u32()?;
        for _ in 0..num_callbacks {
            // Read and discard creatable
            let cb_class = reader.read_u16()?;
            if cb_class != 0x8000 {
                // plEventCallbackMsg — skip its data
                // This is a message creatable; for now we skip by reading its fields
                skip_event_callback_msg(reader)?;
            }
        }

        let num_stop_points = reader.read_u32()?;
        let mut stop_points = Vec::with_capacity(num_stop_points as usize);
        for _ in 0..num_stop_points {
            stop_points.push(reader.read_f32()?);
        }

        Ok(Self {
            flags,
            begin,
            end,
            loop_end,
            loop_begin,
            speed,
            ease_in,
            ease_out,
            speed_ease,
            current_anim_time,
            last_eval_world_time,
            stop_points,
        })
    }

    pub fn is_stopped(&self) -> bool {
        self.flags & atc_flags::STOPPED != 0
    }

    pub fn is_looping(&self) -> bool {
        self.flags & atc_flags::LOOP != 0
    }

    pub fn is_backwards(&self) -> bool {
        self.flags & atc_flags::BACKWARDS != 0
    }

    pub fn duration(&self) -> f32 {
        self.end - self.begin
    }

    /// Convert world time to local animation time.
    pub fn world_to_anim_time(&self, world_time: f64) -> f32 {
        if self.is_stopped() {
            return self.current_anim_time;
        }

        let elapsed = (world_time - self.last_eval_world_time) as f32 * self.speed;
        let mut t = self.current_anim_time + elapsed;

        if self.is_looping() {
            let loop_len = self.loop_end - self.loop_begin;
            if loop_len > 0.0 {
                while t > self.loop_end {
                    t -= loop_len;
                }
                while t < self.loop_begin {
                    t += loop_len;
                }
            }
        } else {
            t = t.clamp(self.begin, self.end);
        }

        t
    }
}

fn read_f64(reader: &mut impl Read) -> Result<f64> {
    let mut buf = [0u8; 8];
    reader.read_exact(&mut buf)?;
    Ok(f64::from_le_bytes(buf))
}

/// Skip a plEventCallbackMsg creatable (rough skip — reads known fields).
fn skip_event_callback_msg(reader: &mut impl Read) -> Result<()> {
    // plMessage base: sender key, receivers, timestamp, bcast_flags
    use crate::core::uoid::read_key_uoid;
    let _ = read_key_uoid(reader)?; // sender
    let num_recv = reader.read_u32()?;
    for _ in 0..num_recv {
        let _ = read_key_uoid(reader)?;
    }
    let _ = read_f64(reader)?; // timestamp
    let _ = reader.read_u32()?; // bcast_flags

    // plEventCallbackMsg fields
    let _ = reader.read_f32()?; // fEventTime
    let _ = reader.read_u16()?; // fEvent (CallbackEvent enum)
    let _ = reader.read_i16()?; // fIndex
    let _ = reader.read_u8()?; // fRepeats
    let _ = reader.read_i16()?; // fUser

    Ok(())
}

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

    #[test]
    fn test_world_to_anim_time_stopped() {
        let atc = AnimTimeConvertData {
            flags: atc_flags::STOPPED,
            begin: 0.0,
            end: 5.0,
            loop_begin: 0.0,
            loop_end: 5.0,
            speed: 1.0,
            ease_in: None,
            ease_out: None,
            speed_ease: None,
            current_anim_time: 2.5,
            last_eval_world_time: 0.0,
            stop_points: Vec::new(),
        };
        assert_eq!(atc.world_to_anim_time(100.0), 2.5);
    }

    #[test]
    fn test_world_to_anim_time_loop() {
        let atc = AnimTimeConvertData {
            flags: atc_flags::LOOP,
            begin: 0.0,
            end: 5.0,
            loop_begin: 1.0,
            loop_end: 4.0,
            speed: 1.0,
            ease_in: None,
            ease_out: None,
            speed_ease: None,
            current_anim_time: 3.5,
            last_eval_world_time: 0.0,
            stop_points: Vec::new(),
        };
        let t = atc.world_to_anim_time(1.0);
        assert!(t >= 1.0 && t <= 4.0, "t={} should be in loop range", t);
    }
}