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
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
//! plClusterGroup parser — extracts instanced vegetation geometry from .prp data
//!
//! C++ ref: plClusterGroup.cpp, plCluster.cpp, plSpanTemplate.cpp, plSpanInstance.cpp
//! plClusterGroup contains a shared vertex template (plSpanTemplate) that is stamped
//! at many world positions via plSpanInstance transforms. At runtime, C++ unpacks these
//! into plDrawableSpans with kLiteVtxNonPreshaded | kPropRunTimeLight.

use anyhow::{Result, bail};
use std::io::{Read, Seek};
use crate::resource::prp::PlasmaRead;
use crate::core::uoid::{Uoid, read_key_uoid};

/// Vertex produced by cluster unpacking — position, normal, color, UVs, flags.
/// Mirrors the engine renderer's Vertex layout so callers can transmute or convert.
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct ClusterVertex {
    pub position: [f32; 4],
    pub normal: [f32; 4],
    pub color: [f32; 4],
    pub uv: [f32; 2],
    pub uv2: [f32; 2],
    pub uv3: [f32; 2],
    pub flags: f32,
    pub _pad: f32,
    pub uv4: [f32; 2],
    pub uv5: [f32; 2],
}

/// Parsed cluster group — template geometry + per-instance transforms
#[derive(Debug)]
pub struct ClusterGroup {
    pub template: SpanTemplate,
    pub material_uoid: Option<Uoid>,
    pub clusters: Vec<Cluster>,
}

/// Shared vertex/index template for all instances in a cluster group
#[derive(Debug)]
pub struct SpanTemplate {
    pub format: u16,
    pub num_verts: u16,
    pub num_tris: u16,
    pub stride: usize,
    pub vert_data: Vec<u8>,
    pub index_data: Vec<u16>,
}

/// A cluster contains multiple span instances sharing the same template
#[derive(Debug)]
pub struct Cluster {
    pub encoding: SpanEncoding,
    pub instances: Vec<SpanInstance>,
}

/// Encoding descriptor for per-instance position deltas and colors
#[derive(Debug, Clone, Copy)]
pub struct SpanEncoding {
    pub code: u8,
    pub pos_scale: f32,
}

/// A single instance: 3x4 transform matrix + optional position deltas + optional colors
#[derive(Debug)]
pub struct SpanInstance {
    pub l2w: [[f32; 4]; 3],
    pub pos_delta: Option<Vec<u8>>,
    pub col: Option<Vec<u8>>,
}

// SpanEncoding position format flags
const POS_NONE: u8 = 0x0;
const POS_888: u8 = 0x1;
const POS_161616: u8 = 0x2;
const POS_101010: u8 = 0x4;
const POS_008: u8 = 0x8;
const POS_MASK: u8 = POS_888 | POS_161616 | POS_101010 | POS_008;

// SpanEncoding color format flags
const COL_NONE: u16 = 0x0;
const COL_A8: u16 = 0x10;
const COL_I8: u16 = 0x20;
const COL_AI88: u16 = 0x40;
const COL_RGB888: u16 = 0x80;
const COL_ARGB8888: u16 = 0x100;
const COL_MASK: u16 = COL_A8 | COL_I8 | COL_AI88 | COL_RGB888 | COL_ARGB8888;

// SpanTemplate format flags
const TMPL_POS: u16 = 0x1;
const TMPL_NORM: u16 = 0x2;
const TMPL_COLOR: u16 = 0x4;
const TMPL_WGTIDX: u16 = 0x8;
const TMPL_UVW_MASK: u16 = 0xF0;
const TMPL_WEIGHT_MASK: u16 = 0x300;
const TMPL_COLOR2: u16 = 0x400;

impl SpanEncoding {
    fn pos_stride(&self) -> usize {
        match self.code & POS_MASK {
            POS_888 => 3,
            POS_161616 => 6,
            POS_101010 => 4,
            POS_008 => 1,
            _ => 0,
        }
    }

    fn col_stride(&self) -> usize {
        let code = self.code as u16;
        match code & COL_MASK {
            COL_A8 | COL_I8 => 1,
            COL_AI88 => 2,
            COL_RGB888 => 3,
            COL_ARGB8888 => 4,
            _ => 0,
        }
    }
}

impl SpanTemplate {
    pub fn num_uvws(&self) -> usize { ((self.format & TMPL_UVW_MASK) >> 4) as usize }
    fn num_weights(&self) -> usize { ((self.format & TMPL_WEIGHT_MASK) >> 8) as usize }
    fn has_pos(&self) -> bool { self.format & TMPL_POS != 0 }
    fn has_norm(&self) -> bool { self.format & TMPL_NORM != 0 }
    fn has_color(&self) -> bool { self.format & TMPL_COLOR != 0 }
    fn has_color2(&self) -> bool { self.format & TMPL_COLOR2 != 0 }
    fn has_wgt_idx(&self) -> bool { self.format & TMPL_WGTIDX != 0 }

    fn calc_stride(format: u16) -> usize {
        let mut s = 0usize;
        if format & TMPL_POS != 0 { s += 12; } // hsPoint3
        let num_weights = ((format & TMPL_WEIGHT_MASK) >> 8) as usize;
        s += num_weights * 4; // float per weight
        if format & TMPL_WGTIDX != 0 { s += 4; } // uint32
        if format & TMPL_NORM != 0 { s += 12; } // hsVector3
        if format & TMPL_COLOR != 0 { s += 4; } // uint32
        if format & TMPL_COLOR2 != 0 { s += 4; } // uint32
        let num_uvws = ((format & TMPL_UVW_MASK) >> 4) as usize;
        s += num_uvws * 12; // hsPoint3 per UVW
        s
    }

    // Offsets into per-vertex data
    fn pos_offset(&self) -> usize { 0 }
    fn norm_offset(&self) -> usize {
        let mut o = if self.has_pos() { 12 } else { 0 };
        o += self.num_weights() * 4;
        if self.has_wgt_idx() { o += 4; }
        o
    }
    fn color_offset(&self) -> usize {
        self.norm_offset() + if self.has_norm() { 12 } else { 0 }
    }
    fn color2_offset(&self) -> usize {
        self.color_offset() + if self.has_color() { 4 } else { 0 }
    }
    fn uvw_offset(&self) -> usize {
        self.color2_offset() + if self.has_color2() { 4 } else { 0 }
    }
}

impl ClusterGroup {
    /// Parse a plClusterGroup from object data.
    /// C++ ref: plClusterGroup::Read (plClusterGroup.cpp:103-136)
    pub fn read(reader: &mut (impl Read + Seek)) -> Result<Self> {
        // hsKeyedObject::Read — read self-key
        let _self_key = read_key_uoid(reader)?;

        // Read plSpanTemplate
        let template = SpanTemplate::read(reader)?;

        // Read material key ref (mgr->ReadKeyNotifyMe)
        let material_uoid = read_key_uoid(reader)?;

        // Read clusters
        let num_clusters = reader.read_u32()? as usize;
        let mut clusters = Vec::with_capacity(num_clusters);
        for _ in 0..num_clusters {
            clusters.push(Cluster::read(reader, template.num_verts)?);
        }

        // Skip vis regions (key refs)
        let num_regions = reader.read_u32()? as usize;
        for _ in 0..num_regions {
            let _ = read_key_uoid(reader)?;
        }

        // Skip light refs (key refs)
        let num_lights = reader.read_u32()? as usize;
        for _ in 0..num_lights {
            let _ = read_key_uoid(reader)?;
        }

        // LOD distances
        let _min_dist = reader.read_f32()?;
        let _max_dist = reader.read_f32()?;

        // Render level
        let _render_level = reader.read_u32()?;

        // Scene node key
        let _ = read_key_uoid(reader)?;

        let total_insts: usize = clusters.iter().map(|c| c.instances.len()).sum();
        log::debug!("ClusterGroup: {} clusters, {} total instances, template: {} verts, {} tris, format=0x{:X}",
            clusters.len(), total_insts, template.num_verts, template.num_tris, template.format);

        Ok(Self {
            template,
            material_uoid,
            clusters,
        })
    }

    /// Unpack all instances into individual meshes (vertices + indices).
    /// Each cluster becomes one mesh with all its instances merged.
    /// C++ ref: plCluster::UnPack (plCluster.cpp:112-207)
    pub fn unpack_meshes(&self) -> Vec<(Vec<ClusterVertex>, Vec<u16>)> {
        let templ = &self.template;
        let verts_per_inst = templ.num_verts as usize;
        let indices_per_inst = templ.num_tris as usize * 3;

        let mut result = Vec::new();

        for cluster in &self.clusters {
            let num_insts = cluster.instances.len();
            if num_insts == 0 { continue; }

            let mut all_verts = Vec::with_capacity(verts_per_inst * num_insts);
            let mut all_indices = Vec::with_capacity(indices_per_inst * num_insts);
            let mut idx_offset = 0u16;

            for inst in &cluster.instances {
                // Copy template indices with offset
                for &idx in &templ.index_data {
                    all_indices.push(idx + idx_offset);
                }
                idx_offset += verts_per_inst as u16;

                // Build local-to-world matrix (3x4 → 4x4 row-major flat)
                let l2w = &inst.l2w;
                let m: [f32; 16] = [
                    l2w[0][0], l2w[0][1], l2w[0][2], l2w[0][3],
                    l2w[1][0], l2w[1][1], l2w[1][2], l2w[1][3],
                    l2w[2][0], l2w[2][1], l2w[2][2], l2w[2][3],
                    0.0, 0.0, 0.0, 1.0,
                ];

                // Compute inverse-transpose for normal transform
                // For a 3x3 rotation+scale, transpose of inverse ≈ cofactor matrix
                let w2l_t = transpose_inverse_3x3(&m);

                // Create position delta iterator
                let enc = &cluster.encoding;
                let pos_stride = enc.pos_stride();
                let col_stride = enc.col_stride();

                for vi in 0..verts_per_inst {
                    let base = vi * templ.stride;

                    // Read position from template
                    let pos_off = templ.pos_offset();
                    let (mut px, mut py, mut pz) = if templ.has_pos() && base + pos_off + 12 <= templ.vert_data.len() {
                        let o = base + pos_off;
                        (
                            f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
                            f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
                            f32::from_le_bytes(templ.vert_data[o+8..o+12].try_into().unwrap()),
                        )
                    } else {
                        (0.0, 0.0, 0.0)
                    };

                    // Apply position delta if present
                    if let Some(ref pos_data) = inst.pos_delta {
                        let delta_off = vi * pos_stride;
                        let (dx, dy, dz) = decode_pos_delta(enc, pos_data, delta_off);
                        px += dx;
                        py += dy;
                        pz += dz;
                    }

                    // Transform position by L2W
                    let wx = px * m[0] + py * m[1] + pz * m[2] + m[3];
                    let wy = px * m[4] + py * m[5] + pz * m[6] + m[7];
                    let wz = px * m[8] + py * m[9] + pz * m[10] + m[11];

                    // Read normal from template
                    let norm_off = templ.norm_offset();
                    let (nx, ny, nz) = if templ.has_norm() && base + norm_off + 12 <= templ.vert_data.len() {
                        let o = base + norm_off;
                        (
                            f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
                            f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
                            f32::from_le_bytes(templ.vert_data[o+8..o+12].try_into().unwrap()),
                        )
                    } else {
                        (0.0, 1.0, 0.0)
                    };

                    // Transform normal by inverse-transpose
                    let wnx = nx * w2l_t[0] + ny * w2l_t[1] + nz * w2l_t[2];
                    let wny = nx * w2l_t[3] + ny * w2l_t[4] + nz * w2l_t[5];
                    let wnz = nx * w2l_t[6] + ny * w2l_t[7] + nz * w2l_t[8];
                    let nlen = (wnx*wnx + wny*wny + wnz*wnz).sqrt().max(1e-10);

                    // Read color from template, apply instance color override
                    let (r, g, b, a) = if templ.has_color() {
                        let col_off = templ.color_offset();
                        let o = base + col_off;
                        if o + 4 <= templ.vert_data.len() {
                            let mut c = u32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap());
                            // Apply instance color override
                            if let Some(ref col_data) = inst.col {
                                c = decode_color(enc, col_data, vi * col_stride, c);
                            }
                            // D3DCOLOR format: 0xAARRGGBB (LE bytes: B,G,R,A)
                            let ca = ((c >> 24) & 0xFF) as f32 / 255.0;
                            let cr = ((c >> 16) & 0xFF) as f32 / 255.0;
                            let cg = ((c >> 8) & 0xFF) as f32 / 255.0;
                            let cb = (c & 0xFF) as f32 / 255.0;
                            (cr, cg, cb, ca)
                        } else {
                            (1.0, 1.0, 1.0, 1.0)
                        }
                    } else {
                        (1.0, 1.0, 1.0, 1.0)
                    };

                    // Read UV0 from template
                    let uvw_off = templ.uvw_offset();
                    let (u, v) = if templ.num_uvws() > 0 {
                        let o = base + uvw_off;
                        if o + 8 <= templ.vert_data.len() {
                            (
                                f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
                                f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
                            )
                        } else {
                            (0.0, 0.0)
                        }
                    } else {
                        (0.0, 0.0)
                    };

                    // Read UV1 from template
                    let (u2, v2) = if templ.num_uvws() > 1 {
                        let o = base + uvw_off + 12; // skip UV0 (hsPoint3 = 12 bytes)
                        if o + 8 <= templ.vert_data.len() {
                            (
                                f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
                                f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
                            )
                        } else {
                            (0.0, 0.0)
                        }
                    } else {
                        (0.0, 0.0)
                    };

                    all_verts.push(ClusterVertex {
                        position: [wx, wy, wz, 0.0],
                        normal: [wnx / nlen, wny / nlen, wnz / nlen, 0.0],
                        color: [r, g, b, a],
                        uv: [u, v],
                        uv2: [u2, v2],
                        uv3: [0.0, 0.0],
                        // kLiteVtxNonPreshaded flag = bit 1 (2.0)
                        flags: 2.0,
                        _pad: 0.0,
                        uv4: [0.0, 0.0],
                        uv5: [0.0, 0.0],
                    });
                }
            }

            if !all_verts.is_empty() && !all_indices.is_empty() {
                result.push((all_verts, all_indices));
            }
        }

        result
    }
}

impl SpanTemplate {
    fn read(reader: &mut (impl Read + Seek)) -> Result<Self> {
        let num_verts = reader.read_u16()?;
        let format = reader.read_u16()?;
        let num_tris = reader.read_u16()?;

        let stride = Self::calc_stride(format);
        let vert_size = num_verts as usize * stride;
        let idx_size = num_tris as usize * 3;

        let mut vert_data = vec![0u8; vert_size];
        reader.read_exact(&mut vert_data)?;

        let mut index_data = Vec::with_capacity(idx_size);
        for _ in 0..idx_size {
            index_data.push(reader.read_u16()?);
        }

        Ok(Self { format, num_verts, num_tris, stride, vert_data, index_data })
    }
}

impl Cluster {
    fn read(reader: &mut (impl Read + Seek), num_template_verts: u16) -> Result<Self> {
        // plSpanEncoding
        let code = reader.read_u8()?;
        let pos_scale = reader.read_f32()?;
        let encoding = SpanEncoding { code, pos_scale };

        let num_verts = num_template_verts as usize;
        let num_insts = reader.read_u32()? as usize;
        let mut instances = Vec::with_capacity(num_insts);

        let pos_stride = encoding.pos_stride();
        let col_stride = encoding.col_stride();

        for _ in 0..num_insts {
            // 3x4 transform matrix (12 floats)
            let mut l2w = [[0.0f32; 4]; 3];
            for row in &mut l2w {
                for col in row.iter_mut() {
                    *col = reader.read_f32()?;
                }
            }

            // Position deltas (optional, based on encoding)
            let pos_delta = if pos_stride > 0 {
                let size = num_verts * pos_stride;
                let mut data = vec![0u8; size];
                reader.read_exact(&mut data)?;
                Some(data)
            } else {
                None
            };

            // Color data (optional, based on encoding)
            let col = if col_stride > 0 {
                let size = num_verts * col_stride;
                let mut data = vec![0u8; size];
                reader.read_exact(&mut data)?;
                Some(data)
            } else {
                None
            };

            instances.push(SpanInstance { l2w, pos_delta, col });
        }

        Ok(Self { encoding, instances })
    }
}

/// Decode position delta from encoded data.
/// C++ ref: plSpanInstanceIter::DelPos (plSpanInstance.h:246-266)
fn decode_pos_delta(enc: &SpanEncoding, data: &[u8], offset: usize) -> (f32, f32, f32) {
    match enc.code & POS_MASK {
        POS_888 => {
            if offset + 3 <= data.len() {
                let dx = data[offset] as i8 as f32 * enc.pos_scale;
                let dy = data[offset + 1] as i8 as f32 * enc.pos_scale;
                let dz = data[offset + 2] as i8 as f32 * enc.pos_scale;
                (dx, dy, dz)
            } else { (0.0, 0.0, 0.0) }
        }
        POS_161616 => {
            if offset + 6 <= data.len() {
                let dx = i16::from_le_bytes(data[offset..offset+2].try_into().unwrap()) as f32 * enc.pos_scale;
                let dy = i16::from_le_bytes(data[offset+2..offset+4].try_into().unwrap()) as f32 * enc.pos_scale;
                let dz = i16::from_le_bytes(data[offset+4..offset+6].try_into().unwrap()) as f32 * enc.pos_scale;
                (dx, dy, dz)
            } else { (0.0, 0.0, 0.0) }
        }
        POS_101010 => {
            if offset + 4 <= data.len() {
                let packed = u32::from_le_bytes(data[offset..offset+4].try_into().unwrap());
                let dx = (packed & 0x3F) as f32 * enc.pos_scale;
                let dy = ((packed >> 10) & 0x3F) as f32 * enc.pos_scale;
                let dz = ((packed >> 20) & 0x3F) as f32 * enc.pos_scale;
                (dx, dy, dz)
            } else { (0.0, 0.0, 0.0) }
        }
        POS_008 => {
            if offset < data.len() {
                let dz = data[offset] as i8 as f32 * enc.pos_scale;
                (0.0, 0.0, dz)
            } else { (0.0, 0.0, 0.0) }
        }
        _ => (0.0, 0.0, 0.0),
    }
}

/// Decode color from encoded data, merging with template color.
/// C++ ref: plSpanInstanceIter::Color (plSpanInstance.h:271-299)
fn decode_color(enc: &SpanEncoding, data: &[u8], offset: usize, template_color: u32) -> u32 {
    let code = enc.code as u16;
    match code & COL_MASK {
        COL_A8 => {
            if offset < data.len() {
                (template_color & 0x00FFFFFF) | ((data[offset] as u32) << 24)
            } else { template_color }
        }
        COL_I8 => {
            if offset < data.len() {
                let v = data[offset] as u32;
                (template_color & 0xFF000000) | (v << 16) | (v << 8) | v
            } else { template_color }
        }
        COL_AI88 => {
            if offset + 2 <= data.len() {
                let val = u16::from_le_bytes(data[offset..offset+2].try_into().unwrap());
                let col = (val & 0xFF) as u32;
                let alpha = ((val & 0xFF00) >> 8) as u32;
                (alpha << 24) | (col << 16) | (col << 8) | col
            } else { template_color }
        }
        COL_RGB888 => {
            if offset + 3 <= data.len() {
                (template_color & 0xFF000000)
                    | ((data[offset] as u32) << 16)
                    | ((data[offset + 1] as u32) << 8)
                    | (data[offset + 2] as u32)
            } else { template_color }
        }
        COL_ARGB8888 => {
            if offset + 4 <= data.len() {
                u32::from_le_bytes(data[offset..offset+4].try_into().unwrap())
            } else { template_color }
        }
        _ => template_color,
    }
}

/// Compute 3x3 inverse-transpose for normal transformation.
/// Returns [row0_x, row0_y, row0_z, row1_x, row1_y, row1_z, row2_x, row2_y, row2_z]
fn transpose_inverse_3x3(m: &[f32; 16]) -> [f32; 9] {
    // Extract 3x3 rotation/scale from row-major 4x4
    let a = m[0]; let b = m[1]; let c = m[2];
    let d = m[4]; let e = m[5]; let f = m[6];
    let g = m[8]; let h = m[9]; let i = m[10];

    let det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g);
    if det.abs() < 1e-10 {
        return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
    }
    let inv_det = 1.0 / det;

    // Cofactor matrix (which is the transpose of the inverse × det)
    // Then transpose that to get inverse-transpose
    // inverse-transpose = cofactor / det ... but we want cofactor^T / det
    // Actually: (M^-1)^T = cofactor(M) / det
    // So inverse-transpose row i, col j = cofactor(i,j) / det
    [
        (e*i - f*h) * inv_det, (f*g - d*i) * inv_det, (d*h - e*g) * inv_det,
        (c*h - b*i) * inv_det, (a*i - c*g) * inv_det, (b*g - a*h) * inv_det,
        (b*f - c*e) * inv_det, (c*d - a*f) * inv_det, (a*e - b*d) * inv_det,
    ]
}