plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
//! plAGAnim — animation clips holding named channels with applicators.
//!
//! C++ ref: plAnimation/plAGAnim.h/.cpp, plMatrixChannel.h/.cpp,
//!          plScalarChannel.h/.cpp, plPointChannel.h/.cpp, plQuatChannel.h/.cpp
//!
//! ## Binary Format for plAGAnim applicator list
//!
//! Each animation contains a list of applicator+channel pairs:
//! ```text
//! [u32 num_applicators]
//! For each pair:
//!   [Applicator creatable]   — u16 class_idx + applicator data
//!   [Channel creatable]      — u16 class_idx + channel data
//! ```
//!
//! ## Applicator format (plAGApplicator::Read, plAGApplicator.cpp:141-148)
//!
//! All applicator subclasses use the base Read with no additional fields:
//! ```text
//! u8       fEnabled
//! safe_str fChannelName        — target bone/node name
//! ```
//!
//! Applicator class indices (all share same binary format):
//!   0x030E  plMatrixChannelApplicator
//!   0x030F  plPointChannelApplicator
//!   0x0314  plQuatChannelApplicator
//!   0x0319  plScalarChannelApplicator
//!   0x0310  plLightDiffuseApplicator
//!   0x0311  plLightAmbientApplicator
//!   0x0312  plLightSpecularApplicator
//!   0x0313  plOmniApplicator
//!   0x031A  plSpotInnerApplicator
//!   0x031B  plSpotOuterApplicator
//!   0x03A8  plOmniCutoffApplicator
//!   0x022A  plOmniSqApplicator
//!
//! ## Channel formats (each starts with base plAGChannel::Read → safe_string fName)
//!
//! plMatrixControllerChannel (0x02DE):
//!   safe_str fName
//!   creatable fController      — plTMController/plCompoundController/plLeafController
//!   AffineParts fAP            — 15 floats (T:3f + Q:4f + U:4f + K:3f + F:1f)
//!
//! plMatrixConstant (0x0331):
//!   safe_str fName
//!   AffineParts fAP            — 15 floats (T:3f + Q:4f + U:4f + K:3f + F:1f)
//!
//! plScalarControllerChannel (0x0318):
//!   safe_str fName
//!   creatable fController      — plLeafController
//!
//! plScalarConstant (0x0330):
//!   safe_str fName
//!   f32 fResult
//!
//! plPointControllerChannel (0x030B):
//!   safe_str fName
//!   creatable fController      — plCompoundController/plLeafController
//!
//! plPointConstant (0x02E1):
//!   safe_str fName
//!   Point3 fResult             — 3 floats (x, y, z)
//!
//! plQuatConstant (0x02E4):
//!   safe_str fName
//!   Quat fResult               — 4 floats (x, y, z, w)
//!
//! ## hsAffineParts format (plTransform/hsAffineParts.cpp:417-425)
//!
//! ```text
//! hsVector3 fT   — Translation (3× f32 LE)
//! hsQuat    fQ   — Essential rotation (4× f32 LE: x, y, z, w)
//! hsQuat    fU   — Stretch rotation (4× f32 LE: x, y, z, w)
//! hsVector3 fK   — Stretch factors / scale (3× f32 LE)
//! float     fF   — Sign of determinant (1× f32 LE)
//! ```
//! Total: 15 floats = 60 bytes

use std::io::Read;

use anyhow::{Result, bail};

use crate::core::class_index::ClassIndex;
use crate::core::synched_object::SynchedObjectData;
use crate::core::uoid::{Uoid, read_key_uoid};
use crate::resource::prp::PlasmaRead;

use super::controller::Controller;

/// Body usage for animations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BodyUsage {
    Unknown = 0,
    Upper = 1,
    Full = 2,
    Lower = 3,
}

impl BodyUsage {
    pub fn from_u8(v: u8) -> Self {
        match v {
            1 => Self::Upper,
            2 => Self::Full,
            3 => Self::Lower,
            _ => Self::Unknown,
        }
    }
}

/// Decomposed affine transform (hsAffineParts).
/// C++ ref: plTransform/hsAffineParts.h
#[derive(Debug, Clone)]
pub struct AffineParts {
    pub translation: [f32; 3],       // fT — translation
    pub rotation: [f32; 4],          // fQ — essential rotation (quaternion x,y,z,w)
    pub stretch_rotation: [f32; 4],  // fU — stretch rotation (quaternion)
    pub scale: [f32; 3],             // fK — stretch factors
    pub det_sign: f32,               // fF — sign of determinant
}

impl AffineParts {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        Ok(Self {
            translation: [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?],
            rotation: [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?, reader.read_f32()?],
            stretch_rotation: [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?, reader.read_f32()?],
            scale: [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?],
            det_sign: reader.read_f32()?,
        })
    }
}

/// Channel data extracted from an animation applicator.
///
/// Each channel targets a named bone/node and contains either a controller
/// with keyframe data or a constant value.
#[derive(Debug, Clone)]
pub enum ChannelData {
    /// Matrix controller channel — drives a bone's transform via keyframes.
    /// Contains a plController + initial hsAffineParts rest pose.
    /// C++ ref: plMatrixControllerChannel (plMatrixChannel.cpp:596-604)
    MatrixController {
        controller: Option<Controller>,
        initial_pose: AffineParts,
    },
    /// Matrix constant — static bone transform with no animation.
    /// C++ ref: plMatrixConstant (plMatrixChannel.cpp:217-222)
    MatrixConstant {
        pose: AffineParts,
    },
    /// Scalar controller channel — drives a single float via keyframes.
    /// C++ ref: plScalarControllerChannel (plScalarChannel.cpp:379-384)
    ScalarController(Option<Controller>),
    /// Scalar constant — static float value.
    /// C++ ref: plScalarConstant (plScalarChannel.cpp:148-152)
    ScalarConstant(f32),
    /// Point controller channel — drives a 3D point via keyframes.
    /// C++ ref: plPointControllerChannel (plPointChannel.cpp:381-386)
    PointController(Option<Controller>),
    /// Point constant — static 3D point.
    /// C++ ref: plPointConstant (plPointChannel.cpp:132-136)
    PointConstant([f32; 3]),
    /// Quaternion constant — static rotation.
    /// C++ ref: plQuatConstant (plQuatChannel.cpp:147-151)
    QuatConstant([f32; 4]),
}

/// A channel-applicator pair in an animation.
///
/// The applicator routes a channel's output to a target (bone transform,
/// scalar property, etc.). The channel_name identifies the target bone.
///
/// C++ ref: plAGApplicator::Read (plAGApplicator.cpp:141-148)
#[derive(Debug, Clone)]
pub struct AnimApplicator {
    /// Applicator class index.
    pub applicator_class: u16,
    /// Channel name from the applicator (target bone/node name).
    pub channel_name: String,
    pub enabled: bool,
    /// Channel class index.
    pub channel_class: u16,
    /// Channel name from the channel's own data (fName from plAGChannel::Read).
    pub channel_name_from_channel: String,
    /// Parsed channel data including controller with keyframes.
    pub channel_data: ChannelData,
}

impl AnimApplicator {
    /// Read an applicator+channel pair from a stream.
    ///
    /// plAGAnim::Read serializes applicators and channels as interleaved
    /// creatable pairs: [applicator_creatable][channel_creatable] for each.
    /// C++ ref: plAGAnim.cpp:203-224
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        // === Read applicator creatable ===
        // All applicator subclasses use the same base plAGApplicator::Read:
        //   fEnabled (u8 bool), fChannelName (safe_string)
        // C++ ref: plAGApplicator.cpp:141-148
        let app_class = reader.read_u16()?;
        let (channel_name, enabled) = if app_class != 0x8000 {
            let enabled = reader.read_u8()? != 0;
            let name = reader.read_safe_string()?;
            (name, enabled)
        } else {
            (String::new(), false)
        };

        // === Read channel creatable ===
        // Dispatch on class index to read subclass-specific data.
        // All channels start with base plAGChannel::Read → safe_string fName.
        let chan_class = reader.read_u16()?;
        if chan_class == 0x8000 {
            return Ok(Self {
                applicator_class: app_class,
                channel_name,
                enabled,
                channel_class: 0x8000,
                channel_name_from_channel: String::new(),
                channel_data: ChannelData::ScalarConstant(0.0),
            });
        }

        // Base plAGChannel::Read — all channel types read fName first
        let channel_name_from_channel = reader.read_safe_string()?;

        let channel_data = match chan_class {
            // plMatrixControllerChannel (0x02DE):
            //   base name (already read) + controller creatable + AffineParts
            ClassIndex::PL_MATRIX_CONTROLLER_CHANNEL => {
                let controller = Controller::read_creatable(reader)?;
                let initial_pose = AffineParts::read(reader)?;
                ChannelData::MatrixController { controller, initial_pose }
            }

            // plMatrixConstant (0x0331):
            //   base name (already read) + AffineParts
            ClassIndex::PL_MATRIX_CONSTANT => {
                let pose = AffineParts::read(reader)?;
                ChannelData::MatrixConstant { pose }
            }

            // plScalarControllerChannel (0x0318):
            //   base name (already read) + controller creatable
            ClassIndex::PL_SCALAR_CONTROLLER_CHANNEL => {
                let controller = Controller::read_creatable(reader)?;
                ChannelData::ScalarController(controller)
            }

            // plScalarConstant (0x0330):
            //   base name (already read) + f32 value
            ClassIndex::PL_SCALAR_CONSTANT => {
                let value = reader.read_f32()?;
                ChannelData::ScalarConstant(value)
            }

            // plPointControllerChannel (0x030B):
            //   base name (already read) + controller creatable
            ClassIndex::PL_POINT_CONTROLLER_CHANNEL => {
                let controller = Controller::read_creatable(reader)?;
                ChannelData::PointController(controller)
            }

            // plPointConstant (0x02E1):
            //   base name (already read) + Point3 (3 floats)
            ClassIndex::PL_POINT_CONSTANT => {
                let value = [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?];
                ChannelData::PointConstant(value)
            }

            // plQuatConstant (0x02E4):
            //   base name (already read) + Quat (4 floats: x, y, z, w)
            ClassIndex::PL_QUAT_CONSTANT => {
                let value = [reader.read_f32()?, reader.read_f32()?, reader.read_f32()?, reader.read_f32()?];
                ChannelData::QuatConstant(value)
            }

            _ => {
                bail!("Unknown channel class 0x{:04X} ({}) for applicator '{}'",
                    chan_class,
                    ClassIndex::class_name(chan_class),
                    channel_name);
            }
        };

        Ok(Self {
            applicator_class: app_class,
            channel_name,
            enabled,
            channel_class: chan_class,
            channel_name_from_channel,
            channel_data,
        })
    }
}

/// Named time marker in an animation.
#[derive(Debug, Clone)]
pub struct AnimMarker {
    pub name: String,
    pub time: f32,
}

/// Named loop in an animation.
#[derive(Debug, Clone)]
pub struct AnimLoop {
    pub name: String,
    pub begin: f32,
    pub end: f32,
}

/// Parsed plAGAnim data (base animation clip).
#[derive(Debug, Clone)]
pub struct AGAnimData {
    pub self_key: Option<Uoid>,
    pub synched: SynchedObjectData,
    pub name: String,
    pub start: f32,
    pub end: f32,
    pub applicators: Vec<AnimApplicator>,
}

impl AGAnimData {
    /// Read a plAGAnim from a stream (after creatable class index).
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let self_key = read_key_uoid(reader)?;
        let synched = SynchedObjectData::read(reader)?;

        let name = reader.read_safe_string()?;
        let start = reader.read_f32()?;
        let end = reader.read_f32()?;

        let num_apps = reader.read_u32()?;
        let mut applicators = Vec::with_capacity(num_apps as usize);
        for _ in 0..num_apps {
            applicators.push(AnimApplicator::read(reader)?);
        }

        Ok(Self {
            self_key,
            synched,
            name,
            start,
            end,
            applicators,
        })
    }
}

/// Parsed plATCAnim data (timed animation with looping, markers, etc.).
#[derive(Debug, Clone)]
pub struct ATCAnimData {
    pub base: AGAnimData,
    pub initial: f32,
    pub auto_start: bool,
    pub loop_start: f32,
    pub loop_end: f32,
    pub do_loop: bool,
    pub ease_in_type: u8,
    pub ease_in_min: f32,
    pub ease_in_max: f32,
    pub ease_in_length: f32,
    pub ease_out_type: u8,
    pub ease_out_min: f32,
    pub ease_out_max: f32,
    pub ease_out_length: f32,
    pub markers: Vec<AnimMarker>,
    pub loops: Vec<AnimLoop>,
    pub stop_points: Vec<f32>,
}

impl ATCAnimData {
    /// Read a plATCAnim from a stream (after creatable class index).
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = AGAnimData::read(reader)?;

        let initial = reader.read_f32()?;
        let auto_start = reader.read_u8()? != 0;
        let loop_start = reader.read_f32()?;
        let loop_end = reader.read_f32()?;
        let do_loop = reader.read_u8()? != 0;

        let ease_in_type = reader.read_u8()?;
        let ease_in_min = reader.read_f32()?;
        let ease_in_max = reader.read_f32()?;
        let ease_in_length = reader.read_f32()?;
        let ease_out_type = reader.read_u8()?;
        let ease_out_min = reader.read_f32()?;
        let ease_out_max = reader.read_f32()?;
        let ease_out_length = reader.read_f32()?;

        let num_markers = reader.read_u32()?;
        let mut markers = Vec::with_capacity(num_markers as usize);
        for _ in 0..num_markers {
            markers.push(AnimMarker {
                name: reader.read_safe_string()?,
                time: reader.read_f32()?,
            });
        }

        let num_loops = reader.read_u32()?;
        let mut loops = Vec::with_capacity(num_loops as usize);
        for _ in 0..num_loops {
            loops.push(AnimLoop {
                name: reader.read_safe_string()?,
                begin: reader.read_f32()?,
                end: reader.read_f32()?,
            });
        }

        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 {
            base,
            initial,
            auto_start,
            loop_start,
            loop_end,
            do_loop,
            ease_in_type,
            ease_in_min,
            ease_in_max,
            ease_in_length,
            ease_out_type,
            ease_out_min,
            ease_out_max,
            ease_out_length,
            markers,
            loops,
            stop_points,
        })
    }
}

/// Parsed plEmoteAnim data.
#[derive(Debug, Clone)]
pub struct EmoteAnimData {
    pub base: ATCAnimData,
    pub fade_in: f32,
    pub fade_out: f32,
    pub body_usage: BodyUsage,
}

impl EmoteAnimData {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = ATCAnimData::read(reader)?;
        let fade_in = reader.read_f32()?;
        let fade_out = reader.read_f32()?;
        let body_usage = BodyUsage::from_u8(reader.read_u8()?);

        Ok(Self {
            base,
            fade_in,
            fade_out,
            body_usage,
        })
    }
}

/// Parsed plAgeGlobalAnim data.
#[derive(Debug, Clone)]
pub struct AgeGlobalAnimData {
    pub base: AGAnimData,
    pub global_var_name: String,
}

impl AgeGlobalAnimData {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = AGAnimData::read(reader)?;
        let global_var_name = reader.read_safe_string()?;

        Ok(Self {
            base,
            global_var_name,
        })
    }
}