plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! plSynchedObject — network synchronization base.
//!
//! Extends hsKeyedObject with synchronization flags and SDL state lists.
//! This replaces the `skip_synched_object` hack in prp.rs with proper parsing.
//!
//! C++ ref: pnNetCommon/plSynchedObject.h/.cpp

use std::io::Read;

use anyhow::Result;

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

/// Synchronization flags for plSynchedObject.
#[allow(dead_code)]
pub mod synch_flags {
    pub const DONT_DIRTY: u32 = 0x1;
    pub const SEND_RELIABLY: u32 = 0x2;
    pub const HAS_CONSTANT_NET_GROUP: u32 = 0x4;
    pub const DONT_SYNCH_GAME_MESSAGES: u32 = 0x8;
    pub const EXCLUDE_PERSISTENT_STATE: u32 = 0x10;
    pub const EXCLUDE_ALL_PERSISTENT_STATE: u32 = 0x20;
    pub const LOCAL_ONLY: u32 = EXCLUDE_ALL_PERSISTENT_STATE | DONT_SYNCH_GAME_MESSAGES;
    pub const HAS_VOLATILE_STATE: u32 = 0x40;
    pub const ALL_STATE_IS_VOLATILE: u32 = 0x80;
}

/// SDL send flags.
#[allow(dead_code)]
pub mod sdl_send_flags {
    pub const BCAST_TO_CLIENTS: u32 = 0x1;
    pub const FORCE_FULL_SEND: u32 = 0x2;
    pub const SKIP_LOCAL_OWNERSHIP_CHECK: u32 = 0x4;
    pub const SEND_IMMEDIATELY: u32 = 0x8;
    pub const DONT_PERSIST_ON_SERVER: u32 = 0x10;
    pub const USE_RELEVANCE_REGIONS: u32 = 0x20;
    pub const NEW_STATE: u32 = 0x40;
    pub const IS_AVATAR_STATE: u32 = 0x80;
}

/// Parsed plSynchedObject data.
#[derive(Debug, Clone, Default)]
pub struct SynchedObjectData {
    pub synch_flags: u32,
    pub sdl_exclude_list: Vec<String>,
    pub sdl_volatile_list: Vec<String>,
}

impl SynchedObjectData {
    /// Read the plSynchedObject portion from a stream.
    /// This reads ONLY the synched object fields, NOT the hsKeyedObject self-key
    /// (that must be read before calling this).
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let synch_flags = reader.read_u32()?;

        let mut sdl_exclude_list = Vec::new();
        if synch_flags & synch_flags::EXCLUDE_PERSISTENT_STATE != 0 {
            let count = reader.read_u16()?;
            for _ in 0..count {
                sdl_exclude_list.push(reader.read_safe_string()?);
            }
        }

        let mut sdl_volatile_list = Vec::new();
        if synch_flags & synch_flags::HAS_VOLATILE_STATE != 0 {
            let count = reader.read_u16()?;
            for _ in 0..count {
                sdl_volatile_list.push(reader.read_safe_string()?);
            }
        }

        Ok(Self {
            synch_flags,
            sdl_exclude_list,
            sdl_volatile_list,
        })
    }
}

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

    #[test]
    fn test_read_simple() {
        // synch_flags = 0 (no exclude/volatile lists)
        let data = [0x00, 0x00, 0x00, 0x00];
        let mut cursor = Cursor::new(&data);
        let synched = SynchedObjectData::read(&mut cursor).unwrap();
        assert_eq!(synched.synch_flags, 0);
        assert!(synched.sdl_exclude_list.is_empty());
        assert!(synched.sdl_volatile_list.is_empty());
    }

    /// Parse synched object headers from all plSceneObjects in Cleft.
    #[test]
    fn test_parse_cleft_scene_objects() {
        use crate::resource::prp::{PrpPage, class_types};
        use crate::core::uoid::read_key_uoid;
        use std::path::Path;

        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 scene_keys: Vec<_> = page.keys_of_type(class_types::PL_SCENE_OBJECT);

        let mut parsed = 0;
        let mut failed = 0;
        for key in &scene_keys {
            if let Some(data) = page.object_data(key) {
                let mut cursor = Cursor::new(data);

                // Creatable class index (i16)
                let class_idx = cursor.read_i16().unwrap();
                if class_idx < 0 {
                    continue;
                }

                // hsKeyedObject::Read — self-key
                match read_key_uoid(&mut cursor) {
                    Ok(Some(uoid)) => {
                        assert_eq!(uoid.object_name, key.object_name);
                    }
                    Ok(None) => continue,
                    Err(_) => {
                        failed += 1;
                        continue;
                    }
                }

                // plSynchedObject::Read
                match SynchedObjectData::read(&mut cursor) {
                    Ok(synched) => {
                        parsed += 1;
                        // Verify we can continue reading after synched object
                        // (the next fields would be the SceneObject interfaces)
                    }
                    Err(e) => {
                        failed += 1;
                        eprintln!("Failed to parse synched object for {}: {}", key.object_name, e);
                    }
                }
            }
        }

        eprintln!(
            "Parsed {}/{} plSceneObject synched headers ({} failed)",
            parsed,
            scene_keys.len(),
            failed
        );
        assert!(parsed > 0, "Should have parsed at least some scene objects");
        assert_eq!(failed, 0, "No synched object parses should fail");
    }
}