plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plLayer — concrete layer implementation with state, colors, textures.
//!
//! C++ ref: plSurface/plLayer.h/.cpp, plLayerInterface.h/.cpp

use std::io::Read;

use anyhow::Result;

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

use super::state::MatState;

/// RGBA color.
#[derive(Debug, Clone, Copy, Default)]
pub struct Color {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl Color {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        Ok(Self {
            r: reader.read_f32()?,
            g: reader.read_f32()?,
            b: reader.read_f32()?,
            a: reader.read_f32()?,
        })
    }

    pub fn white() -> Self {
        Self {
            r: 1.0,
            g: 1.0,
            b: 1.0,
            a: 1.0,
        }
    }
}

/// Read an hsMatrix44 from a stream.
fn read_matrix44(reader: &mut impl Read) -> Result<[f32; 16]> {
    let flag = reader.read_u8()?;
    if flag == 0 {
        return Ok([
            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
        ]);
    }
    let mut m = [0f32; 16];
    for val in &mut m {
        *val = reader.read_f32()?;
    }
    Ok(m)
}

/// UVW source constants.
#[allow(dead_code)]
pub mod uvw_src {
    pub const PASS_THRU: u32 = 0x00000000;
    pub const IDX_MASK: u32 = 0x0000FFFF;
    pub const NORMAL: u32 = 0x00010000;
    pub const POSITION: u32 = 0x00020000;
    pub const REFLECT: u32 = 0x00030000;
}

/// Parsed plLayer data.
#[derive(Debug, Clone)]
pub struct LayerData {
    // plLayerInterface base
    pub self_key: Option<Uoid>,
    pub synched: SynchedObjectData,
    pub underlay: Option<Uoid>,

    // plLayer fields
    pub state: MatState,
    pub transform: [f32; 16],
    pub preshade_color: Color,
    pub runtime_color: Color,
    pub ambient_color: Color,
    pub specular_color: Color,
    pub uvw_src: u32,
    pub opacity: f32,
    pub lod_bias: f32,
    pub specular_power: f32,
    pub texture: Option<Uoid>,
    pub vertex_shader: Option<Uoid>,
    pub pixel_shader: Option<Uoid>,
    pub bump_env_xfm: [f32; 16],
}

impl LayerData {
    /// Read a plLayer from a stream (after creatable class index).
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        // plLayerInterface::Read → plSynchedObject::Read → hsKeyedObject::Read
        let self_key = read_key_uoid(reader)?;
        let synched = SynchedObjectData::read(reader)?;
        // underlay key
        let underlay = read_key_uoid(reader)?;

        // plLayer::Read
        let state = MatState::read(reader)?;

        let transform = read_matrix44(reader)?;
        let preshade_color = Color::read(reader)?;
        let runtime_color = Color::read(reader)?;
        let ambient_color = Color::read(reader)?;
        let specular_color = Color::read(reader)?;

        let uvw_src = reader.read_u32()?;
        let opacity = reader.read_f32()?;
        let lod_bias = reader.read_f32()?;
        let specular_power = reader.read_f32()?;

        let texture = read_key_uoid(reader)?;
        let vertex_shader = read_key_uoid(reader)?;
        let pixel_shader = read_key_uoid(reader)?;

        let bump_env_xfm = read_matrix44(reader)?;

        Ok(Self {
            self_key,
            synched,
            underlay,
            state,
            transform,
            preshade_color,
            runtime_color,
            ambient_color,
            specular_color,
            uvw_src,
            opacity,
            lod_bias,
            specular_power,
            texture,
            vertex_shader,
            pixel_shader,
            bump_env_xfm,
        })
    }

    /// Get the texture name, if any.
    pub fn texture_name(&self) -> Option<&str> {
        self.texture.as_ref().map(|u| u.object_name.as_str())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::class_index::ClassIndex;
    use crate::resource::prp::PrpPage;
    use std::io::Cursor;
    use std::path::Path;

    #[test]
    fn test_parse_cleft_layers() {
        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
        if !path.exists() {
            eprintln!("Skipping test: {:?} not found", path);
            return;
        }

        let page = PrpPage::from_file(path).unwrap();
        let layer_keys: Vec<_> = page.keys_of_type(ClassIndex::PL_LAYER);

        let mut parsed = 0;
        let mut with_texture = 0;
        for key in &layer_keys {
            if let Some(data) = page.object_data(key) {
                let mut cursor = Cursor::new(data);
                let _ = cursor.read_i16().unwrap();

                match LayerData::read(&mut cursor) {
                    Ok(layer) => {
                        parsed += 1;
                        if layer.texture.is_some() {
                            with_texture += 1;
                        }
                        assert_eq!(
                            layer.self_key.as_ref().unwrap().object_name,
                            key.object_name
                        );
                        // Opacity should be in [0, 1] range
                        assert!(
                            layer.opacity >= 0.0 && layer.opacity <= 1.0,
                            "Invalid opacity {} for {}",
                            layer.opacity,
                            key.object_name
                        );
                    }
                    Err(e) => {
                        panic!("Failed to parse layer '{}': {}", key.object_name, e);
                    }
                }
            }
        }

        eprintln!(
            "Parsed {} layers ({} with textures) from Cleft",
            parsed, with_texture
        );
        assert!(parsed > 0);
        assert!(with_texture > 0);
    }
}