plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plKey — reference-counted handle to a keyed object.
//!
//! In C++, plKey is a smart pointer to plKeyData (plKeyImp), which holds:
//!   - A plUoid identifying the object
//!   - An optional pointer to the loaded object
//!   - File position (start_pos, data_len) for demand loading
//!   - Active reference count
//!
//! In Rust, we represent this as Arc<RwLock<KeyData>>.
//!
//! C++ ref: pnKeyedObject/plKey.h, plKeyImp.h/.cpp

use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, RwLock};

use super::uoid::Uoid;

/// The inner data of a key — holds identity and optional object reference.
pub struct KeyData {
    pub uoid: Uoid,
    /// File offset where the object data starts (for demand loading).
    pub start_pos: u32,
    /// Length of the object data in the file.
    pub data_len: u32,
    /// Number of active references to this key's object.
    pub active_refs: u16,
    // Object pointer will be added when we implement KeyedObject trait in Step 4.
    // For now, this is just the identity + file position.
}

impl KeyData {
    pub fn name(&self) -> &str {
        &self.uoid.object_name
    }

    pub fn class_type(&self) -> u16 {
        self.uoid.class_type
    }
}

impl fmt::Debug for KeyData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "KeyData({:?}, pos={}, len={})",
            self.uoid, self.start_pos, self.data_len
        )
    }
}

/// A reference-counted handle to a keyed object.
///
/// This is the primary way objects reference each other in the Plasma engine.
/// A null key (None) represents a nil reference.
#[derive(Clone)]
pub struct Key(Option<Arc<RwLock<KeyData>>>);

impl Key {
    /// Create a null (nil) key.
    pub fn null() -> Self {
        Key(None)
    }

    /// Create a key from a Uoid (no file position).
    pub fn from_uoid(uoid: Uoid) -> Self {
        Key(Some(Arc::new(RwLock::new(KeyData {
            uoid,
            start_pos: 0,
            data_len: 0,
            active_refs: 0,
        }))))
    }

    /// Create a key with file position info (for demand loading).
    pub fn from_uoid_with_pos(uoid: Uoid, start_pos: u32, data_len: u32) -> Self {
        Key(Some(Arc::new(RwLock::new(KeyData {
            uoid,
            start_pos,
            data_len,
            active_refs: 0,
        }))))
    }

    /// Returns true if this is a null (nil) key.
    pub fn is_null(&self) -> bool {
        self.0.is_none()
    }

    /// Returns true if this key is non-null.
    pub fn is_some(&self) -> bool {
        self.0.is_some()
    }

    /// Get the inner Arc for this key. Returns None for null keys.
    pub fn inner(&self) -> Option<&Arc<RwLock<KeyData>>> {
        self.0.as_ref()
    }

    /// Get the Uoid of this key. Panics on null keys.
    pub fn uoid(&self) -> Uoid {
        self.0
            .as_ref()
            .expect("uoid() called on null key")
            .read()
            .unwrap()
            .uoid
            .clone()
    }

    /// Get the name of the referenced object. Panics on null keys.
    pub fn name(&self) -> String {
        self.0
            .as_ref()
            .expect("name() called on null key")
            .read()
            .unwrap()
            .uoid
            .object_name
            .clone()
    }

    /// Get the class type of the referenced object. Panics on null keys.
    pub fn class_type(&self) -> u16 {
        self.0
            .as_ref()
            .expect("class_type() called on null key")
            .read()
            .unwrap()
            .uoid
            .class_type
    }

    /// Try to get the name, returning None for null keys.
    pub fn try_name(&self) -> Option<String> {
        self.0
            .as_ref()
            .map(|arc| arc.read().unwrap().uoid.object_name.clone())
    }
}

impl Default for Key {
    fn default() -> Self {
        Key::null()
    }
}

impl PartialEq for Key {
    fn eq(&self, other: &Self) -> bool {
        match (&self.0, &other.0) {
            (None, None) => true,
            (Some(a), Some(b)) => Arc::ptr_eq(a, b),
            _ => false,
        }
    }
}

impl Eq for Key {}

impl Hash for Key {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match &self.0 {
            None => 0u8.hash(state),
            Some(arc) => {
                1u8.hash(state);
                Arc::as_ptr(arc).hash(state);
            }
        }
    }
}

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

impl Ord for Key {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        match (&self.0, &other.0) {
            (None, None) => std::cmp::Ordering::Equal,
            (None, Some(_)) => std::cmp::Ordering::Less,
            (Some(_), None) => std::cmp::Ordering::Greater,
            (Some(a), Some(b)) => Arc::as_ptr(a).cmp(&Arc::as_ptr(b)),
        }
    }
}

impl fmt::Debug for Key {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            None => write!(f, "Key(nil)"),
            Some(arc) => {
                let data = arc.read().unwrap();
                write!(f, "Key({:?})", data.uoid)
            }
        }
    }
}

impl fmt::Display for Key {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            None => write!(f, "(nil)"),
            Some(arc) => {
                let data = arc.read().unwrap();
                write!(f, "{}", data.uoid)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::location::Location;

    #[test]
    fn test_null_key() {
        let k = Key::null();
        assert!(k.is_null());
        assert!(!k.is_some());
    }

    #[test]
    fn test_key_from_uoid() {
        let uoid = Uoid::new(Location::new(100, 0), 0x004C, "TestDrawable".into());
        let k = Key::from_uoid(uoid);
        assert!(!k.is_null());
        assert_eq!(k.name(), "TestDrawable");
        assert_eq!(k.class_type(), 0x004C);
    }

    #[test]
    fn test_key_clone_shares_data() {
        let uoid = Uoid::new(Location::new(100, 0), 0x0001, "Object".into());
        let k1 = Key::from_uoid(uoid);
        let k2 = k1.clone();
        assert_eq!(k1, k2);
    }

    #[test]
    fn test_key_with_pos() {
        let uoid = Uoid::new(Location::new(100, 0), 0x0004, "Texture".into());
        let k = Key::from_uoid_with_pos(uoid, 1000, 500);
        let arc = k.inner().unwrap();
        let data = arc.read().unwrap();
        assert_eq!(data.start_pos, 1000);
        assert_eq!(data.data_len, 500);
    }

    #[test]
    fn test_null_keys_equal() {
        assert_eq!(Key::null(), Key::null());
    }

    #[test]
    fn test_different_keys_not_equal() {
        let k1 = Key::from_uoid(Uoid::new(Location::new(1, 0), 1, "A".into()));
        let k2 = Key::from_uoid(Uoid::new(Location::new(1, 0), 1, "A".into()));
        // Different Arc instances, so not equal by pointer
        assert_ne!(k1, k2);
    }
}