Skip to main content

plasma_prp/animation/
time_convert.rs

1//! plAnimTimeConvert — converts world time to local animation time.
2//!
3//! Handles speed, looping, ease-in/out curves, stop points, and wrap modes.
4//!
5//! C++ ref: plInterp/plAnimTimeConvert.h/.cpp, plATCEaseCurves.cpp
6
7use std::io::Read;
8
9use anyhow::Result;
10
11use crate::resource::prp::PlasmaRead;
12
13/// Animation time convert flags.
14#[allow(dead_code)]
15pub mod atc_flags {
16    pub const NONE: u32 = 0x00;
17    pub const STOPPED: u32 = 0x01;
18    pub const LOOP: u32 = 0x02;
19    pub const BACKWARDS: u32 = 0x04;
20    pub const WRAP: u32 = 0x08;
21    pub const NEEDS_RESET: u32 = 0x10;
22    pub const EASING_IN: u32 = 0x20;
23    pub const FORCED_MOVE: u32 = 0x40;
24    pub const NO_CALLBACKS: u32 = 0x80;
25    pub const FLAGS_MASK: u32 = 0xFF;
26}
27
28/// Ease curve types.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EaseType {
31    None = 0,
32    ConstAccel = 1,
33    Spline = 2,
34}
35
36/// Ease curve data.
37#[derive(Debug, Clone)]
38pub struct EaseCurve {
39    pub min_length: f32,
40    pub max_length: f32,
41    pub norm_length: f32,
42    pub start_speed: f32,
43    pub speed: f32,
44    pub begin_world_time: f64,
45    /// Spline coefficients (only for spline ease curves).
46    pub spline_coefs: Option<[f32; 4]>,
47}
48
49impl EaseCurve {
50    pub fn read_creatable(reader: &mut impl Read) -> Result<Option<Self>> {
51        let class_idx = reader.read_u16()?;
52        if class_idx == 0x8000 {
53            return Ok(None);
54        }
55
56        let min_length = reader.read_f32()?;
57        let max_length = reader.read_f32()?;
58        let norm_length = reader.read_f32()?;
59        let start_speed = reader.read_f32()?;
60        let speed = reader.read_f32()?;
61        let begin_world_time = read_f64(reader)?;
62
63        // Check if this is a spline ease curve
64        let spline_coefs =
65            if class_idx == crate::core::class_index::ClassIndex::PL_SPLINE_EASE_CURVE {
66                Some([
67                    reader.read_f32()?,
68                    reader.read_f32()?,
69                    reader.read_f32()?,
70                    reader.read_f32()?,
71                ])
72            } else {
73                None
74            };
75
76        Ok(Some(Self {
77            min_length,
78            max_length,
79            norm_length,
80            start_speed,
81            speed,
82            begin_world_time,
83            spline_coefs,
84        }))
85    }
86}
87
88/// Parsed plAnimTimeConvert data.
89#[derive(Debug, Clone)]
90pub struct AnimTimeConvertData {
91    pub flags: u32,
92    pub begin: f32,
93    pub end: f32,
94    pub loop_end: f32,
95    pub loop_begin: f32,
96    pub speed: f32,
97    pub ease_in: Option<EaseCurve>,
98    pub ease_out: Option<EaseCurve>,
99    pub speed_ease: Option<EaseCurve>,
100    pub current_anim_time: f32,
101    pub last_eval_world_time: f64,
102    pub stop_points: Vec<f32>,
103}
104
105impl AnimTimeConvertData {
106    /// Read a plAnimTimeConvert from a stream (as a creatable — class index already consumed).
107    pub fn read(reader: &mut impl Read) -> Result<Self> {
108        let flags = reader.read_u32()?;
109        let begin = reader.read_f32()?;
110        let end = reader.read_f32()?;
111        let loop_end = reader.read_f32()?;
112        let loop_begin = reader.read_f32()?;
113        let speed = reader.read_f32()?;
114
115        let ease_in = EaseCurve::read_creatable(reader)?;
116        let ease_out = EaseCurve::read_creatable(reader)?;
117        let speed_ease = EaseCurve::read_creatable(reader)?;
118
119        let current_anim_time = reader.read_f32()?;
120        let last_eval_world_time = read_f64(reader)?;
121
122        // Callback messages (skip — they're creatables we can't fully parse yet)
123        let num_callbacks = reader.read_u32()?;
124        for _ in 0..num_callbacks {
125            // Read and discard creatable
126            let cb_class = reader.read_u16()?;
127            if cb_class != 0x8000 {
128                // plEventCallbackMsg — skip its data
129                // This is a message creatable; for now we skip by reading its fields
130                skip_event_callback_msg(reader)?;
131            }
132        }
133
134        let num_stop_points = reader.read_u32()?;
135        let mut stop_points = Vec::with_capacity(num_stop_points as usize);
136        for _ in 0..num_stop_points {
137            stop_points.push(reader.read_f32()?);
138        }
139
140        Ok(Self {
141            flags,
142            begin,
143            end,
144            loop_end,
145            loop_begin,
146            speed,
147            ease_in,
148            ease_out,
149            speed_ease,
150            current_anim_time,
151            last_eval_world_time,
152            stop_points,
153        })
154    }
155
156    pub fn is_stopped(&self) -> bool {
157        self.flags & atc_flags::STOPPED != 0
158    }
159
160    pub fn is_looping(&self) -> bool {
161        self.flags & atc_flags::LOOP != 0
162    }
163
164    pub fn is_backwards(&self) -> bool {
165        self.flags & atc_flags::BACKWARDS != 0
166    }
167
168    pub fn duration(&self) -> f32 {
169        self.end - self.begin
170    }
171
172    /// Convert world time to local animation time.
173    pub fn world_to_anim_time(&self, world_time: f64) -> f32 {
174        if self.is_stopped() {
175            return self.current_anim_time;
176        }
177
178        let elapsed = (world_time - self.last_eval_world_time) as f32 * self.speed;
179        let mut t = self.current_anim_time + elapsed;
180
181        if self.is_looping() {
182            let loop_len = self.loop_end - self.loop_begin;
183            if loop_len > 0.0 {
184                while t > self.loop_end {
185                    t -= loop_len;
186                }
187                while t < self.loop_begin {
188                    t += loop_len;
189                }
190            }
191        } else {
192            t = t.clamp(self.begin, self.end);
193        }
194
195        t
196    }
197}
198
199fn read_f64(reader: &mut impl Read) -> Result<f64> {
200    let mut buf = [0u8; 8];
201    reader.read_exact(&mut buf)?;
202    Ok(f64::from_le_bytes(buf))
203}
204
205/// Skip a plEventCallbackMsg creatable (rough skip — reads known fields).
206fn skip_event_callback_msg(reader: &mut impl Read) -> Result<()> {
207    // plMessage base: sender key, receivers, timestamp, bcast_flags
208    use crate::core::uoid::read_key_uoid;
209    let _ = read_key_uoid(reader)?; // sender
210    let num_recv = reader.read_u32()?;
211    for _ in 0..num_recv {
212        let _ = read_key_uoid(reader)?;
213    }
214    let _ = read_f64(reader)?; // timestamp
215    let _ = reader.read_u32()?; // bcast_flags
216
217    // plEventCallbackMsg fields
218    let _ = reader.read_f32()?; // fEventTime
219    let _ = reader.read_u16()?; // fEvent (CallbackEvent enum)
220    let _ = reader.read_i16()?; // fIndex
221    let _ = reader.read_u8()?; // fRepeats
222    let _ = reader.read_i16()?; // fUser
223
224    Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_world_to_anim_time_stopped() {
233        let atc = AnimTimeConvertData {
234            flags: atc_flags::STOPPED,
235            begin: 0.0,
236            end: 5.0,
237            loop_begin: 0.0,
238            loop_end: 5.0,
239            speed: 1.0,
240            ease_in: None,
241            ease_out: None,
242            speed_ease: None,
243            current_anim_time: 2.5,
244            last_eval_world_time: 0.0,
245            stop_points: Vec::new(),
246        };
247        assert_eq!(atc.world_to_anim_time(100.0), 2.5);
248    }
249
250    #[test]
251    fn test_world_to_anim_time_loop() {
252        let atc = AnimTimeConvertData {
253            flags: atc_flags::LOOP,
254            begin: 0.0,
255            end: 5.0,
256            loop_begin: 1.0,
257            loop_end: 4.0,
258            speed: 1.0,
259            ease_in: None,
260            ease_out: None,
261            speed_ease: None,
262            current_anim_time: 3.5,
263            last_eval_world_time: 0.0,
264            stop_points: Vec::new(),
265        };
266        let t = atc.world_to_anim_time(1.0);
267        assert!(t >= 1.0 && t <= 4.0, "t={} should be in loop range", t);
268    }
269}