plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plLoadMask — quality/capability LOD filtering.
//!
//! Two quality bytes (one per capability level 0..1), packed into a single byte
//! for serialization. "Always" = both 0xFF (no filtering).
//!
//! C++ ref: CoreLib/plLoadMask.h, plLoadMask.cpp

use std::io::{Read, Write};

use anyhow::Result;

use crate::resource::prp::PlasmaRead;

/// plLoadMask — determines whether an object should be loaded based on
/// quality and capability settings.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct LoadMask {
    /// Quality mask for capability level 0 (low-end hardware).
    pub quality_lo: u8,
    /// Quality mask for capability level 1 (high-end hardware).
    pub quality_hi: u8,
}

impl LoadMask {
    pub const ALWAYS: LoadMask = LoadMask {
        quality_lo: 0xFF,
        quality_hi: 0xFF,
    };

    pub const NEVER: LoadMask = LoadMask {
        quality_lo: 0,
        quality_hi: 0,
    };

    /// Create from packed byte (same encoding as PRP file).
    pub fn from_byte(byte: u8) -> Self {
        Self {
            quality_lo: ((byte & 0xF0) >> 4) | 0xF0,
            quality_hi: (byte & 0x0F) | 0xF0,
        }
    }

    pub fn new(quality_lo: u8, quality_hi: u8) -> Self {
        Self {
            quality_lo,
            quality_hi,
        }
    }

    /// Construct from the packed single-byte serialization format.
    /// C++ ref: plLoadMask::Read (CoreLib/plLoadMask.cpp:81-92)
    pub fn from_packed(byte: u8) -> Self {
        Self {
            quality_lo: ((byte & 0xF0) >> 4) | 0xF0,
            quality_hi: (byte & 0x0F) | 0xF0,
        }
    }

    /// Returns true if this mask actually filters (is not "always load").
    pub fn is_used(&self) -> bool {
        self.quality_lo != 0xFF || self.quality_hi != 0xFF
    }

    /// Returns true if this mask would never load anything.
    pub fn never_loads(&self) -> bool {
        self.quality_lo == 0 && self.quality_hi == 0
    }

    /// Read from stream: 1 packed byte.
    /// Upper nibble = quality[0] lower 4 bits, lower nibble = quality[1] lower 4 bits.
    /// On read, OR in 0xF0 to reconstruct full byte.
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let byte = reader.read_u8()?;
        let quality_lo = ((byte & 0xF0) >> 4) | 0xF0;
        let quality_hi = (byte & 0x0F) | 0xF0;
        Ok(Self {
            quality_lo,
            quality_hi,
        })
    }

    /// Write to stream: 1 packed byte.
    pub fn write(&self, writer: &mut impl Write) -> Result<()> {
        let byte = (self.quality_lo << 4) | (self.quality_hi & 0x0F);
        writer.write_all(&[byte])?;
        Ok(())
    }
}

impl Default for LoadMask {
    fn default() -> Self {
        Self::ALWAYS
    }
}

impl std::fmt::Debug for LoadMask {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if *self == Self::ALWAYS {
            write!(f, "LoadMask(Always)")
        } else {
            write!(
                f,
                "LoadMask(lo:{:#04x} hi:{:#04x})",
                self.quality_lo, self.quality_hi
            )
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_always() {
        let m = LoadMask::ALWAYS;
        assert!(!m.is_used());
        assert!(!m.never_loads());
    }

    #[test]
    fn test_never() {
        let m = LoadMask::NEVER;
        assert!(m.is_used());
        assert!(m.never_loads());
    }

    #[test]
    fn test_round_trip() {
        let m = LoadMask::new(0xF3, 0xF5);
        let mut buf = Vec::new();
        m.write(&mut buf).unwrap();
        assert_eq!(buf.len(), 1);

        let mut cursor = Cursor::new(&buf);
        let m2 = LoadMask::read(&mut cursor).unwrap();
        assert_eq!(m, m2);
    }

    #[test]
    fn test_always_round_trip() {
        let m = LoadMask::ALWAYS;
        let mut buf = Vec::new();
        m.write(&mut buf).unwrap();

        let mut cursor = Cursor::new(&buf);
        let m2 = LoadMask::read(&mut cursor).unwrap();
        assert_eq!(m, m2);
    }
}