plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! Lighting system — plLightInfo, directional/omni/spot lights.
//!
//! C++ ref: plGLight/plLightInfo.h/.cpp

use std::io::Read;

use anyhow::Result;

use crate::core::class_index::ClassIndex;
use crate::core::scene_object::ObjInterfaceData;
use crate::core::uoid::{Uoid, read_key_uoid};
use crate::material::layer::Color;
use crate::resource::prp::PlasmaRead;

/// Light type discriminant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LightType {
    Directional,
    Omni,
    Spot,
    LimitedDir,
}

/// Parsed light properties common to all light types.
#[derive(Debug, Clone)]
pub struct LightProperties {
    pub ambient: Color,
    pub diffuse: Color,
    pub specular: Color,
    pub soft_volume: Option<Uoid>,
}

/// Parsed plLightInfo data.
#[derive(Debug, Clone)]
pub struct LightInfoData {
    pub base: ObjInterfaceData,
    pub light_type: LightType,
    pub props: LightProperties,
    /// Light-to-world transform. For directional lights, the Z-axis (m[8..11])
    /// gives the world-space light direction.
    pub light_to_world: [f32; 16],

    // Directional has no extra fields
    // Omni/Spot have:
    pub attenuation: [f32; 3], // constant, linear, quadratic
    pub cutoff_distance: f32,

    // Spot only:
    pub inner_cone: f32,
    pub outer_cone: f32,
    pub falloff: f32,
}

impl LightInfoData {
    /// Read a plLightInfo (or derived) from a stream.
    /// The light_type indicates which subclass format to parse.
    pub fn read(reader: &mut impl Read, light_type: LightType) -> Result<Self> {
        let base = ObjInterfaceData::read(reader)?;

        // plLightInfo::Read
        let ambient = Color::read(reader)?;
        let diffuse = Color::read(reader)?;
        let specular = Color::read(reader)?;

        // Transform matrices
        let _ = read_matrix44(reader)?; // light-to-local
        let _ = read_matrix44(reader)?; // local-to-light
        let light_to_world = read_matrix44(reader)?;
        let _ = read_matrix44(reader)?; // world-to-light

        // Reference keys
        // C++ ref: plLightInfo::Read (plLightInfo.cpp)
        let _projection = read_key_uoid(reader)?;  // fProjection
        let soft_volume = read_key_uoid(reader)?;   // fSoftVolume
        let _scene_node = read_key_uoid(reader)?;   // fSceneNode

        // Visibility regions
        let num_vis = reader.read_u32()?;
        for _ in 0..num_vis {
            let _ = read_key_uoid(reader)?;
        }

        let mut attenuation = [1.0, 0.0, 0.0];
        let mut cutoff_distance = 0.0;
        let mut inner_cone = 0.0;
        let mut outer_cone = 0.0;
        let mut falloff = 1.0;

        match light_type {
            LightType::Omni | LightType::Spot => {
                // plOmniLightInfo::Read (plLightInfo.cpp:616-621)
                attenuation[0] = reader.read_f32()?;
                attenuation[1] = reader.read_f32()?;
                attenuation[2] = reader.read_f32()?;
                cutoff_distance = reader.read_f32()?;

                if light_type == LightType::Spot {
                    // plSpotLightInfo::Read (plLightInfo.cpp:973-979)
                    // Order: falloff, inner, outer (NOT inner, outer, falloff)
                    falloff = reader.read_f32()?;
                    inner_cone = reader.read_f32()?;
                    outer_cone = reader.read_f32()?;
                }
            }
            LightType::LimitedDir => {
                // plLimitedDirLightInfo inherits from plDirectionalLightInfo, NOT plOmniLightInfo.
                // Reads width/height/depth (3 floats), not attenuation (4 floats).
                // C++ ref: plLightInfo.cpp:658-665
                let _width = reader.read_f32()?;
                let _height = reader.read_f32()?;
                let _depth = reader.read_f32()?;
            }
            LightType::Directional => {}
        }

        Ok(Self {
            base,
            light_type,
            props: LightProperties {
                ambient,
                diffuse,
                specular,
                soft_volume,
            },
            light_to_world,
            attenuation,
            cutoff_distance,
            inner_cone,
            outer_cone,
            falloff,
        })
    }
}

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)
}

#[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_lights() {
        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 mut parsed = 0;

        // Try directional lights
        for key in page.keys_of_type(ClassIndex::PL_DIRECTIONAL_LIGHT_INFO) {
            if let Some(data) = page.object_data(key) {
                let mut cursor = Cursor::new(data);
                let _ = cursor.read_i16().unwrap();

                match LightInfoData::read(&mut cursor, LightType::Directional) {
                    Ok(light) => {
                        parsed += 1;
                        eprintln!(
                            "  DirectionalLight '{}': diffuse=({:.2},{:.2},{:.2})",
                            key.object_name,
                            light.props.diffuse.r,
                            light.props.diffuse.g,
                            light.props.diffuse.b,
                        );
                    }
                    Err(e) => {
                        eprintln!("Failed to parse directional light '{}': {}", key.object_name, e);
                    }
                }
            }
        }

        // Try omni lights
        for key in page.keys_of_type(ClassIndex::PL_OMNI_LIGHT_INFO) {
            if let Some(data) = page.object_data(key) {
                let mut cursor = Cursor::new(data);
                let _ = cursor.read_i16().unwrap();

                match LightInfoData::read(&mut cursor, LightType::Omni) {
                    Ok(_) => parsed += 1,
                    Err(e) => {
                        eprintln!("Failed to parse omni light '{}': {}", key.object_name, e);
                    }
                }
            }
        }

        eprintln!("Parsed {} lights from Cleft", parsed);
        // Cleft_District_Cleft.prp has 6 directional + 11 omni lights
        assert!(parsed >= 11, "Cleft should have at least 11 lights, got {}", parsed);
    }
}