plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plLocation — identifies a (age, chapter, page) triple via a sequence number.
//!
//! C++ ref: pnKeyedObject/plUoid.h, plUoid.cpp

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

use anyhow::Result;

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

/// Flags for plLocation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocFlags(u16);

impl LocFlags {
    pub const LOCAL_ONLY: u16 = 0x1;
    pub const VOLATILE: u16 = 0x2;
    pub const RESERVED: u16 = 0x4;
    pub const BUILT_IN: u16 = 0x8;
    pub const ITINERANT: u16 = 0x10;
}

/// Sequence number ranges.
const GLOBAL_FIXED_LOC_IDX: u32 = 0;
const LOCAL_LOC_START_IDX: u32 = 3;
const LOCAL_LOC_END_IDX: u32 = 32;
const NORMAL_LOC_START_IDX: u32 = LOCAL_LOC_END_IDX + 1;
const RESERVED_LOC_START: u32 = 0xFF000000;
const GLOBAL_SERVER_LOC_IDX: u32 = RESERVED_LOC_START;
const RESERVED_LOC_AVAILABLE_START: u32 = GLOBAL_SERVER_LOC_IDX + 1;
const INVALID_LOC_IDX: u32 = 0xFFFFFFFF;

/// A location in the Plasma world — identifies a page within an age.
///
/// Stored as a sequence number (u32) + flags (u16).
/// Equality ignores the kItinerant flag per C++ behavior.
#[derive(Clone, Copy)]
pub struct Location {
    pub sequence_number: u32,
    pub flags: u16,
}

impl Location {
    /// Create an invalid (uninitialized) location.
    pub fn invalid() -> Self {
        Self {
            sequence_number: INVALID_LOC_IDX,
            flags: 0,
        }
    }

    /// Create a location with the given sequence number and flags.
    pub fn new(sequence_number: u32, flags: u16) -> Self {
        Self {
            sequence_number,
            flags,
        }
    }

    /// Create a reserved location.
    pub fn make_reserved(number: u32) -> Self {
        Self {
            sequence_number: RESERVED_LOC_AVAILABLE_START + number,
            flags: LocFlags::RESERVED,
        }
    }

    /// Create a normal location from a page sequence offset.
    pub fn make_normal(number: u32) -> Self {
        Self {
            sequence_number: NORMAL_LOC_START_IDX + number,
            flags: 0,
        }
    }

    pub fn is_valid(&self) -> bool {
        self.sequence_number != INVALID_LOC_IDX
    }

    pub fn is_reserved(&self) -> bool {
        self.flags & LocFlags::RESERVED != 0
    }

    pub fn is_itinerant(&self) -> bool {
        self.flags & LocFlags::ITINERANT != 0
    }

    pub fn is_virtual(&self) -> bool {
        self.sequence_number == GLOBAL_FIXED_LOC_IDX
    }

    pub fn is_local(&self) -> bool {
        self.sequence_number >= LOCAL_LOC_START_IDX
            && self.sequence_number <= LOCAL_LOC_END_IDX
    }

    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let sequence_number = reader.read_u32()?;
        let flags = reader.read_u16()?;
        Ok(Self {
            sequence_number,
            flags,
        })
    }

    pub fn write(&self, writer: &mut impl Write) -> Result<()> {
        writer.write_all(&self.sequence_number.to_le_bytes())?;
        writer.write_all(&self.flags.to_le_bytes())?;
        Ok(())
    }

    // Well-known locations
    pub const GLOBAL_FIXED: Location = Location {
        sequence_number: GLOBAL_FIXED_LOC_IDX,
        flags: 0,
    };
    pub const LOCAL_START: Location = Location {
        sequence_number: LOCAL_LOC_START_IDX,
        flags: 0,
    };
    pub const LOCAL_END: Location = Location {
        sequence_number: LOCAL_LOC_END_IDX,
        flags: 0,
    };
    pub const NORMAL_START: Location = Location {
        sequence_number: NORMAL_LOC_START_IDX,
        flags: 0,
    };
    pub const GLOBAL_SERVER: Location = Location {
        sequence_number: GLOBAL_SERVER_LOC_IDX,
        flags: LocFlags::RESERVED,
    };
    pub const INVALID: Location = Location {
        sequence_number: INVALID_LOC_IDX,
        flags: 0,
    };
}

impl PartialEq for Location {
    fn eq(&self, other: &Self) -> bool {
        // Ignore the kItinerant flag when comparing, matching C++ behavior
        self.sequence_number == other.sequence_number
            && (self.flags & !LocFlags::ITINERANT) == (other.flags & !LocFlags::ITINERANT)
    }
}

impl Eq for Location {}

impl std::hash::Hash for Location {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.sequence_number.hash(state);
        (self.flags & !LocFlags::ITINERANT).hash(state);
    }
}

impl PartialOrd for Location {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Location {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.sequence_number.cmp(&other.sequence_number)
    }
}

impl Default for Location {
    fn default() -> Self {
        Self::invalid()
    }
}

impl fmt::Debug for Location {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Location(S:{:#x} F:{:#x})", self.sequence_number, self.flags)
    }
}

impl fmt::Display for Location {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "S{:#x}F{:#x}", self.sequence_number, self.flags)
    }
}

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

    #[test]
    fn test_invalid_location() {
        let loc = Location::invalid();
        assert!(!loc.is_valid());
    }

    #[test]
    fn test_valid_location() {
        let loc = Location::new(100, 0);
        assert!(loc.is_valid());
    }

    #[test]
    fn test_round_trip() {
        let loc = Location::new(0x12345678, 0x0003);
        let mut buf = Vec::new();
        loc.write(&mut buf).unwrap();
        assert_eq!(buf.len(), 6);

        let mut cursor = Cursor::new(&buf);
        let loc2 = Location::read(&mut cursor).unwrap();
        assert_eq!(loc, loc2);
    }

    #[test]
    fn test_itinerant_equality() {
        let loc1 = Location::new(100, 0);
        let loc2 = Location::new(100, LocFlags::ITINERANT);
        assert_eq!(loc1, loc2);
    }

    #[test]
    fn test_reserved() {
        let loc = Location::make_reserved(5);
        assert!(loc.is_reserved());
        assert!(loc.is_valid());
    }

    #[test]
    fn test_well_known() {
        assert!(Location::GLOBAL_FIXED.is_virtual());
        assert!(Location::GLOBAL_SERVER.is_reserved());
        assert!(!Location::INVALID.is_valid());
    }
}