Skip to main content

plasma_prp/core/
synched_object.rs

1//! plSynchedObject — network synchronization base.
2//!
3//! Extends hsKeyedObject with synchronization flags and SDL state lists.
4//! This replaces the `skip_synched_object` hack in prp.rs with proper parsing.
5//!
6//! C++ ref: pnNetCommon/plSynchedObject.h/.cpp
7
8use std::io::Read;
9
10use anyhow::Result;
11
12use crate::resource::prp::PlasmaRead;
13
14/// Synchronization flags for plSynchedObject.
15#[allow(dead_code)]
16pub mod synch_flags {
17    pub const DONT_DIRTY: u32 = 0x1;
18    pub const SEND_RELIABLY: u32 = 0x2;
19    pub const HAS_CONSTANT_NET_GROUP: u32 = 0x4;
20    pub const DONT_SYNCH_GAME_MESSAGES: u32 = 0x8;
21    pub const EXCLUDE_PERSISTENT_STATE: u32 = 0x10;
22    pub const EXCLUDE_ALL_PERSISTENT_STATE: u32 = 0x20;
23    pub const LOCAL_ONLY: u32 = EXCLUDE_ALL_PERSISTENT_STATE | DONT_SYNCH_GAME_MESSAGES;
24    pub const HAS_VOLATILE_STATE: u32 = 0x40;
25    pub const ALL_STATE_IS_VOLATILE: u32 = 0x80;
26}
27
28/// SDL send flags.
29#[allow(dead_code)]
30pub mod sdl_send_flags {
31    pub const BCAST_TO_CLIENTS: u32 = 0x1;
32    pub const FORCE_FULL_SEND: u32 = 0x2;
33    pub const SKIP_LOCAL_OWNERSHIP_CHECK: u32 = 0x4;
34    pub const SEND_IMMEDIATELY: u32 = 0x8;
35    pub const DONT_PERSIST_ON_SERVER: u32 = 0x10;
36    pub const USE_RELEVANCE_REGIONS: u32 = 0x20;
37    pub const NEW_STATE: u32 = 0x40;
38    pub const IS_AVATAR_STATE: u32 = 0x80;
39}
40
41/// Parsed plSynchedObject data.
42#[derive(Debug, Clone, Default)]
43pub struct SynchedObjectData {
44    pub synch_flags: u32,
45    pub sdl_exclude_list: Vec<String>,
46    pub sdl_volatile_list: Vec<String>,
47}
48
49impl SynchedObjectData {
50    /// Read the plSynchedObject portion from a stream.
51    /// This reads ONLY the synched object fields, NOT the hsKeyedObject self-key
52    /// (that must be read before calling this).
53    pub fn read(reader: &mut impl Read) -> Result<Self> {
54        let synch_flags = reader.read_u32()?;
55
56        let mut sdl_exclude_list = Vec::new();
57        if synch_flags & synch_flags::EXCLUDE_PERSISTENT_STATE != 0 {
58            let count = reader.read_u16()?;
59            for _ in 0..count {
60                sdl_exclude_list.push(reader.read_safe_string()?);
61            }
62        }
63
64        let mut sdl_volatile_list = Vec::new();
65        if synch_flags & synch_flags::HAS_VOLATILE_STATE != 0 {
66            let count = reader.read_u16()?;
67            for _ in 0..count {
68                sdl_volatile_list.push(reader.read_safe_string()?);
69            }
70        }
71
72        Ok(Self {
73            synch_flags,
74            sdl_exclude_list,
75            sdl_volatile_list,
76        })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::io::Cursor;
84
85    #[test]
86    fn test_read_simple() {
87        // synch_flags = 0 (no exclude/volatile lists)
88        let data = [0x00, 0x00, 0x00, 0x00];
89        let mut cursor = Cursor::new(&data);
90        let synched = SynchedObjectData::read(&mut cursor).unwrap();
91        assert_eq!(synched.synch_flags, 0);
92        assert!(synched.sdl_exclude_list.is_empty());
93        assert!(synched.sdl_volatile_list.is_empty());
94    }
95
96    /// Parse synched object headers from all plSceneObjects in Cleft.
97    #[test]
98    fn test_parse_cleft_scene_objects() {
99        use crate::resource::prp::{PrpPage, class_types};
100        use crate::core::uoid::read_key_uoid;
101        use std::path::Path;
102
103        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
104        if !path.exists() {
105            eprintln!("Skipping test: {:?} not found", path);
106            return;
107        }
108
109        let page = PrpPage::from_file(path).unwrap();
110        let scene_keys: Vec<_> = page.keys_of_type(class_types::PL_SCENE_OBJECT);
111
112        let mut parsed = 0;
113        let mut failed = 0;
114        for key in &scene_keys {
115            if let Some(data) = page.object_data(key) {
116                let mut cursor = Cursor::new(data);
117
118                // Creatable class index (i16)
119                let class_idx = cursor.read_i16().unwrap();
120                if class_idx < 0 {
121                    continue;
122                }
123
124                // hsKeyedObject::Read — self-key
125                match read_key_uoid(&mut cursor) {
126                    Ok(Some(uoid)) => {
127                        assert_eq!(uoid.object_name, key.object_name);
128                    }
129                    Ok(None) => continue,
130                    Err(_) => {
131                        failed += 1;
132                        continue;
133                    }
134                }
135
136                // plSynchedObject::Read
137                match SynchedObjectData::read(&mut cursor) {
138                    Ok(synched) => {
139                        parsed += 1;
140                        // Verify we can continue reading after synched object
141                        // (the next fields would be the SceneObject interfaces)
142                    }
143                    Err(e) => {
144                        failed += 1;
145                        eprintln!("Failed to parse synched object for {}: {}", key.object_name, e);
146                    }
147                }
148            }
149        }
150
151        eprintln!(
152            "Parsed {}/{} plSceneObject synched headers ({} failed)",
153            parsed,
154            scene_keys.len(),
155            failed
156        );
157        assert!(parsed > 0, "Should have parsed at least some scene objects");
158        assert_eq!(failed, 0, "No synched object parses should fail");
159    }
160}