plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plVertCoder — decode compressed vertex data from .prp files
//!
//! Plasma uses a custom compression scheme:
//! - Floats: quantized to uint16 with offset + quantum scaling
//! - Normals: encoded as 3 bytes (per-component byte mapping to [-1, 1])
//! - Colors: run-length encoded per channel (4 channels × 1 byte each)

use anyhow::Result;
use std::io::Read;
use super::prp::PlasmaRead;

const POS_QUANTUM: f32 = 1.0 / 1024.0;   // kPosQuantum
const WEIGHT_QUANTUM: f32 = 1.0 / 32768.0; // kWeightQuantum
const UVW_QUANTUM: f32 = 1.0 / 65536.0;    // kUVWQuantum (actually same precision)

// Quantum values per field type
const QUANTA: [f32; 10] = [
    POS_QUANTUM,    // kPosition
    WEIGHT_QUANTUM, // kWeight
    UVW_QUANTUM, UVW_QUANTUM, UVW_QUANTUM, UVW_QUANTUM, // kUVW 0-3
    UVW_QUANTUM, UVW_QUANTUM, UVW_QUANTUM, UVW_QUANTUM, // kUVW 4-7
];

const FIELD_POSITION: usize = 0;
const FIELD_WEIGHT: usize = 1;
const FIELD_UVW: usize = 2;

const SAME_MASK: u16 = 0x8000;

struct FloatState {
    offset: f32,
    all_same: bool,
    count: u16,
}

struct ByteState {
    count: u16,
    val: u8,
    same: bool,
}

pub struct VertCoder {
    floats: [[FloatState; 3]; 10],
    colors: [ByteState; 4],
}

impl VertCoder {
    pub fn new() -> Self {
        Self {
            floats: std::array::from_fn(|_| std::array::from_fn(|_| FloatState {
                offset: 0.0, all_same: false, count: 0,
            })),
            colors: std::array::from_fn(|_| ByteState {
                count: 0, val: 0, same: false,
            }),
        }
    }

    fn decode_float(&mut self, reader: &mut impl Read, field: usize, chan: usize,
                     dst: &mut [u8], offset: &mut usize) -> Result<()> {
        if self.floats[field][chan].count == 0 {
            // Read header: offset (float) + allSame (bool) + count (uint16)
            self.floats[field][chan].offset = reader.read_f32()?;

            let all_same = reader.read_u8()?;
            self.floats[field][chan].all_same = all_same != 0;

            self.floats[field][chan].count = reader.read_u16()?;
        }

        if self.floats[field][chan].all_same {
            // All values are the same — use offset directly
            let bytes = self.floats[field][chan].offset.to_le_bytes();
            dst[*offset..*offset + 4].copy_from_slice(&bytes);
        } else {
            // Read quantized uint16 and reconstruct float
            let ival = reader.read_u16()?;
            let fval = ival as f32 * QUANTA[field] + self.floats[field][chan].offset;
            let bytes = fval.to_le_bytes();
            dst[*offset..*offset + 4].copy_from_slice(&bytes);
        }

        *offset += 4;
        self.floats[field][chan].count = self.floats[field][chan].count.saturating_sub(1);
        Ok(())
    }

    fn decode_normal(&mut self, reader: &mut impl Read, dst: &mut [u8], offset: &mut usize) -> Result<()> {
        // Normal encoded as 3 bytes: each byte maps to float in [-1, 1]
        // C++ IDecodeNormal: (byte / 255.9f - 0.5f) * 2.0f
        for _ in 0..3 {
            let byte = reader.read_u8()?;
            let val = (byte as f32 / 255.9 - 0.5) * 2.0;
            dst[*offset..*offset + 4].copy_from_slice(&val.to_le_bytes());
            *offset += 4;
        }
        Ok(())
    }

    fn decode_byte(&mut self, reader: &mut impl Read, chan: usize, dst: &mut [u8], offset: &mut usize) -> Result<()> {
        if self.colors[chan].count == 0 {
            let cnt = reader.read_u16()?;
            if cnt & SAME_MASK != 0 {
                self.colors[chan].same = true;
                self.colors[chan].val = reader.read_u8()?;
                self.colors[chan].count = cnt & !SAME_MASK;
            } else {
                self.colors[chan].same = false;
                self.colors[chan].count = cnt;
            }
        }

        if !self.colors[chan].same {
            dst[*offset] = reader.read_u8()?;
        } else {
            dst[*offset] = self.colors[chan].val;
        }

        *offset += 1;
        self.colors[chan].count = self.colors[chan].count.saturating_sub(1);
        Ok(())
    }

    fn decode_color(&mut self, reader: &mut impl Read, dst: &mut [u8], offset: &mut usize) -> Result<()> {
        self.decode_byte(reader, 0, dst, offset)?;
        self.decode_byte(reader, 1, dst, offset)?;
        self.decode_byte(reader, 2, dst, offset)?;
        self.decode_byte(reader, 3, dst, offset)?;
        Ok(())
    }

    fn decode_vertex(&mut self, reader: &mut impl Read, dst: &mut [u8], offset: &mut usize,
                      format: u8) -> Result<()> {
        // Position (3 floats)
        self.decode_float(reader, FIELD_POSITION, 0, dst, offset)?;
        self.decode_float(reader, FIELD_POSITION, 1, dst, offset)?;
        self.decode_float(reader, FIELD_POSITION, 2, dst, offset)?;

        // Weights
        let num_weights = ((format & 0x30) >> 4) as usize;
        for j in 0..num_weights {
            self.decode_float(reader, FIELD_WEIGHT, j, dst, offset)?;
        }
        // Skin indices
        if format & 0x40 != 0 {
            let idx = reader.read_u32()?;
            dst[*offset..*offset + 4].copy_from_slice(&idx.to_le_bytes());
            *offset += 4;
        }

        // Normal (encoded as 2 bytes → float3)
        self.decode_normal(reader, dst, offset)?;

        // Color (4 bytes RLE)
        self.decode_color(reader, dst, offset)?;

        // Color2 (specular — zeroed out)
        dst[*offset..*offset + 4].copy_from_slice(&[0u8; 4]);
        *offset += 4;

        // UVWs
        let num_uvws = (format & 0x0F) as usize;
        for i in 0..num_uvws {
            self.decode_float(reader, FIELD_UVW + i, 0, dst, offset)?;
            self.decode_float(reader, FIELD_UVW + i, 1, dst, offset)?;
            self.decode_float(reader, FIELD_UVW + i, 2, dst, offset)?;
        }

        Ok(())
    }

    /// Decode compressed vertex data from stream.
    /// Returns partial data on failure instead of bailing entirely.
    pub fn read(&mut self, reader: &mut impl Read, format: u8, stride: usize,
                 num_verts: u16) -> Result<Vec<u8>> {
        let total_size = num_verts as usize * stride;
        let mut data = vec![0u8; total_size];

        for i in 0..num_verts as usize {
            let mut offset = i * stride;
            if let Err(e) = self.decode_vertex(reader, &mut data, &mut offset, format) {
                if i == 0 {
                    // No vertices decoded at all — propagate the error
                    return Err(e);
                }
                log::warn!("Vertex decode failed at vertex {}/{}: {}, returning partial data",
                           i, num_verts, e);
                // Truncate to successfully decoded vertices
                data.truncate(i * stride);
                return Ok(data);
            }
        }

        Ok(data)
    }
}