Skip to main content

plasma_prp/core/
scene_object.rs

1//! plSceneObject and its interfaces — the central game object type.
2//!
3//! A plSceneObject holds optional references to:
4//!   - plDrawInterface (rendering)
5//!   - plCoordinateInterface (transform hierarchy)
6//!   - plSimulationInterface (physics)
7//!   - plAudioInterface (sound)
8//!   - Generic interfaces (arbitrary)
9//!   - Modifiers (behavior)
10//!   - Scene node (page grouping)
11//!
12//! C++ ref: pnSceneObject/plSceneObject.h/.cpp,
13//!          plCoordinateInterface.h/.cpp, plDrawInterface.h/.cpp
14
15use std::io::Read;
16
17use anyhow::Result;
18
19use crate::resource::prp::PlasmaRead;
20
21use super::bit_vector::BitVector;
22use super::synched_object::SynchedObjectData;
23use super::uoid::{Uoid, read_key_uoid};
24
25/// Parsed plObjInterface base data.
26#[derive(Debug, Clone, Default)]
27pub struct ObjInterfaceData {
28    /// Self-key Uoid (from hsKeyedObject::Read).
29    pub self_key: Option<Uoid>,
30    /// Synched object data.
31    pub synched: SynchedObjectData,
32    /// Owner scene object key.
33    pub owner: Option<Uoid>,
34    /// Interface properties.
35    pub props: BitVector,
36}
37
38impl ObjInterfaceData {
39    /// Read the plObjInterface portion from a stream.
40    /// Reads: [creatable_class_index] [self_key] [synched_object] [owner_key] [props_bitvector]
41    pub fn read(reader: &mut impl Read) -> Result<Self> {
42        // Note: the creatable class index (i16) is read by the caller before this
43        let self_key = read_key_uoid(reader)?;
44        let synched = SynchedObjectData::read(reader)?;
45        let owner = read_key_uoid(reader)?;
46        let props = BitVector::read(reader)?;
47
48        Ok(Self {
49            self_key,
50            synched,
51            owner,
52            props,
53        })
54    }
55}
56
57/// Parsed plCoordinateInterface data.
58#[derive(Debug, Clone)]
59pub struct CoordinateInterfaceData {
60    pub base: ObjInterfaceData,
61    pub local_to_parent: [f32; 16],
62    pub parent_to_local: [f32; 16],
63    pub local_to_world: [f32; 16],
64    pub world_to_local: [f32; 16],
65    pub children: Vec<Option<Uoid>>,
66}
67
68impl CoordinateInterfaceData {
69    /// Read a plCoordinateInterface from the stream.
70    /// Format: [ObjInterface] [L2P matrix] [P2L matrix] [L2W matrix] [W2L matrix] [child_count] [child_keys...]
71    pub fn read(reader: &mut impl Read) -> Result<Self> {
72        let base = ObjInterfaceData::read(reader)?;
73
74        let local_to_parent = read_matrix44(reader)?;
75        let parent_to_local = read_matrix44(reader)?;
76        let local_to_world = read_matrix44(reader)?;
77        let world_to_local = read_matrix44(reader)?;
78
79        let num_children = reader.read_u32()?;
80        let mut children = Vec::with_capacity(num_children as usize);
81        for _ in 0..num_children {
82            children.push(read_key_uoid(reader)?);
83        }
84
85        Ok(Self {
86            base,
87            local_to_parent,
88            parent_to_local,
89            local_to_world,
90            world_to_local,
91            children,
92        })
93    }
94}
95
96/// Parsed plDrawInterface data.
97#[derive(Debug, Clone)]
98pub struct DrawInterfaceData {
99    pub base: ObjInterfaceData,
100    /// (drawable_index, drawable_key) pairs.
101    pub drawables: Vec<(u32, Option<Uoid>)>,
102    /// Visibility region keys.
103    pub regions: Vec<Option<Uoid>>,
104}
105
106impl DrawInterfaceData {
107    /// Read a plDrawInterface from the stream.
108    /// Format: [ObjInterface] [n_drawables] [foreach: index + key] [n_regions] [foreach: key]
109    pub fn read(reader: &mut impl Read) -> Result<Self> {
110        let base = ObjInterfaceData::read(reader)?;
111
112        let num_drawables = reader.read_u32()?;
113        let mut drawables = Vec::with_capacity(num_drawables as usize);
114        for _ in 0..num_drawables {
115            let index = reader.read_u32()?;
116            let key = read_key_uoid(reader)?;
117            drawables.push((index, key));
118        }
119
120        let num_regions = reader.read_u32()?;
121        let mut regions = Vec::with_capacity(num_regions as usize);
122        for _ in 0..num_regions {
123            regions.push(read_key_uoid(reader)?);
124        }
125
126        Ok(Self {
127            base,
128            drawables,
129            regions,
130        })
131    }
132}
133
134/// Parsed plSimulationInterface data (just the ObjInterface base for now).
135#[derive(Debug, Clone, Default)]
136pub struct SimulationInterfaceData {
137    pub base: ObjInterfaceData,
138    pub physical_key: Option<Uoid>,
139}
140
141impl SimulationInterfaceData {
142    pub fn read(reader: &mut impl Read) -> Result<Self> {
143        let base = ObjInterfaceData::read(reader)?;
144        // plSimulationInterface reads one key: the plPhysical
145        let physical_key = read_key_uoid(reader)?;
146        Ok(Self {
147            base,
148            physical_key,
149        })
150    }
151}
152
153/// Parsed plAudioInterface data.
154#[derive(Debug, Clone, Default)]
155pub struct AudioInterfaceData {
156    pub base: ObjInterfaceData,
157    pub audible_key: Option<Uoid>,
158}
159
160impl AudioInterfaceData {
161    pub fn read(reader: &mut impl Read) -> Result<Self> {
162        let base = ObjInterfaceData::read(reader)?;
163        // plAudioInterface reads one key: the plAudible
164        let audible_key = read_key_uoid(reader)?;
165        Ok(Self {
166            base,
167            audible_key,
168        })
169    }
170}
171
172/// Parsed plSceneObject data.
173#[derive(Debug, Clone)]
174pub struct SceneObjectData {
175    /// Self-key Uoid.
176    pub self_key: Option<Uoid>,
177    /// Synched object data.
178    pub synched: SynchedObjectData,
179    /// Draw interface key (optional).
180    pub draw_interface: Option<Uoid>,
181    /// Simulation interface key (optional).
182    pub sim_interface: Option<Uoid>,
183    /// Coordinate interface key (optional).
184    pub coord_interface: Option<Uoid>,
185    /// Audio interface key (optional).
186    pub audio_interface: Option<Uoid>,
187    /// Generic interface keys.
188    pub generics: Vec<Option<Uoid>>,
189    /// Modifier keys.
190    pub modifiers: Vec<Option<Uoid>>,
191    /// Scene node key.
192    pub scene_node: Option<Uoid>,
193}
194
195impl SceneObjectData {
196    /// Read a plSceneObject from the stream.
197    ///
198    /// Format:
199    ///   [creatable_class_idx (i16)] [self_key] [synched_object]
200    ///   [draw_iface_key] [sim_iface_key] [coord_iface_key] [audio_iface_key]
201    ///   [n_generics] [generic_keys...] [n_modifiers] [modifier_keys...] [scene_node_key]
202    pub fn read(reader: &mut impl Read) -> Result<Self> {
203        // Creatable class index already read by caller (part of "read creatable" dispatch)
204        // hsKeyedObject::Read — self-key
205        let self_key = read_key_uoid(reader)?;
206
207        // plSynchedObject::Read
208        let synched = SynchedObjectData::read(reader)?;
209
210        // plSceneObject::Read — interface keys
211        let draw_interface = read_key_uoid(reader)?;
212        let sim_interface = read_key_uoid(reader)?;
213        let coord_interface = read_key_uoid(reader)?;
214        let audio_interface = read_key_uoid(reader)?;
215
216        // Generic interfaces
217        let num_generics = reader.read_u32()?;
218        let mut generics = Vec::with_capacity(num_generics as usize);
219        for _ in 0..num_generics {
220            generics.push(read_key_uoid(reader)?);
221        }
222
223        // Modifiers
224        let num_modifiers = reader.read_u32()?;
225        let mut modifiers = Vec::with_capacity(num_modifiers as usize);
226        for _ in 0..num_modifiers {
227            modifiers.push(read_key_uoid(reader)?);
228        }
229
230        // Scene node
231        let scene_node = read_key_uoid(reader)?;
232
233        Ok(Self {
234            self_key,
235            synched,
236            draw_interface,
237            sim_interface,
238            coord_interface,
239            audio_interface,
240            generics,
241            modifiers,
242            scene_node,
243        })
244    }
245}
246
247/// Read an hsMatrix44 from a stream.
248/// Format: u8 flag (0 = identity), then 16 floats if non-identity.
249fn read_matrix44(reader: &mut impl Read) -> Result<[f32; 16]> {
250    let flag = reader.read_u8()?;
251    if flag == 0 {
252        return Ok([
253            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
254        ]);
255    }
256    let mut m = [0f32; 16];
257    for val in &mut m {
258        *val = reader.read_f32()?;
259    }
260    Ok(m)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use std::io::Cursor;
267
268    /// Parse all plSceneObjects in Cleft_District_Cleft.prp using our new parser.
269    #[test]
270    fn test_parse_cleft_scene_objects() {
271        use crate::resource::prp::{PrpPage, class_types};
272        use std::path::Path;
273
274        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
275        if !path.exists() {
276            eprintln!("Skipping test: {:?} not found", path);
277            return;
278        }
279
280        let page = PrpPage::from_file(path).unwrap();
281        let scene_keys: Vec<_> = page.keys_of_type(class_types::PL_SCENE_OBJECT);
282
283        let mut parsed = 0;
284        let mut with_draw = 0;
285        let mut with_coord = 0;
286        let mut with_sim = 0;
287        let mut with_audio = 0;
288        let mut total_modifiers = 0;
289
290        for key in &scene_keys {
291            if let Some(data) = page.object_data(key) {
292                let mut cursor = Cursor::new(data);
293
294                // Skip creatable class index (i16)
295                let _ = cursor.read_i16().unwrap();
296
297                match SceneObjectData::read(&mut cursor) {
298                    Ok(so) => {
299                        parsed += 1;
300                        if so.draw_interface.is_some() {
301                            with_draw += 1;
302                        }
303                        if so.coord_interface.is_some() {
304                            with_coord += 1;
305                        }
306                        if so.sim_interface.is_some() {
307                            with_sim += 1;
308                        }
309                        if so.audio_interface.is_some() {
310                            with_audio += 1;
311                        }
312                        total_modifiers += so.modifiers.len();
313
314                        // Verify self-key name matches
315                        if let Some(uoid) = &so.self_key {
316                            assert_eq!(
317                                uoid.object_name, key.object_name,
318                                "Self-key name mismatch for {}",
319                                key.object_name
320                            );
321                        }
322                    }
323                    Err(e) => {
324                        panic!(
325                            "Failed to parse SceneObject '{}': {}",
326                            key.object_name, e
327                        );
328                    }
329                }
330            }
331        }
332
333        eprintln!(
334            "Parsed {} plSceneObjects: {} with draw, {} with coord, {} with sim, {} with audio, {} total modifiers",
335            parsed, with_draw, with_coord, with_sim, with_audio, total_modifiers
336        );
337        assert!(parsed > 0, "Should have parsed at least some scene objects");
338        assert!(with_draw > 0, "Some objects should have draw interfaces");
339        assert!(with_coord > 0, "Some objects should have coordinate interfaces");
340    }
341
342    /// Parse plCoordinateInterface objects from Cleft.
343    #[test]
344    fn test_parse_cleft_coord_interfaces() {
345        use crate::core::class_index::ClassIndex;
346        use crate::resource::prp::PrpPage;
347        use std::path::Path;
348
349        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
350        if !path.exists() {
351            eprintln!("Skipping test: {:?} not found", path);
352            return;
353        }
354
355        let page = PrpPage::from_file(path).unwrap();
356        let coord_keys: Vec<_> = page.keys_of_type(ClassIndex::PL_COORDINATE_INTERFACE);
357
358        let mut parsed = 0;
359        for key in &coord_keys {
360            if let Some(data) = page.object_data(key) {
361                let mut cursor = Cursor::new(data);
362                let _ = cursor.read_i16().unwrap(); // creatable class index
363
364                match CoordinateInterfaceData::read(&mut cursor) {
365                    Ok(ci) => {
366                        parsed += 1;
367                        // Verify the transform matrices are plausible
368                        // (diagonal elements should be non-zero for valid transforms)
369                        let l2w = ci.local_to_world;
370                        let has_some_nonzero = l2w.iter().any(|&v| v != 0.0);
371                        assert!(
372                            has_some_nonzero,
373                            "L2W matrix for {} is all zeros",
374                            key.object_name
375                        );
376                    }
377                    Err(e) => {
378                        panic!(
379                            "Failed to parse CoordinateInterface '{}': {}",
380                            key.object_name, e
381                        );
382                    }
383                }
384            }
385        }
386
387        eprintln!(
388            "Parsed {} plCoordinateInterfaces from Cleft",
389            parsed
390        );
391        assert!(parsed > 0, "Should have parsed coordinate interfaces");
392    }
393
394    /// Parse plDrawInterface objects from Cleft.
395    #[test]
396    fn test_parse_cleft_draw_interfaces() {
397        use crate::core::class_index::ClassIndex;
398        use crate::resource::prp::PrpPage;
399        use std::path::Path;
400
401        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
402        if !path.exists() {
403            eprintln!("Skipping test: {:?} not found", path);
404            return;
405        }
406
407        let page = PrpPage::from_file(path).unwrap();
408        let draw_keys: Vec<_> = page.keys_of_type(ClassIndex::PL_DRAW_INTERFACE);
409
410        let mut parsed = 0;
411        let mut total_drawables = 0;
412        for key in &draw_keys {
413            if let Some(data) = page.object_data(key) {
414                let mut cursor = Cursor::new(data);
415                let _ = cursor.read_i16().unwrap(); // creatable class index
416
417                match DrawInterfaceData::read(&mut cursor) {
418                    Ok(di) => {
419                        parsed += 1;
420                        total_drawables += di.drawables.len();
421
422                        // Verify drawable references point to plDrawableSpans
423                        for (idx, key_ref) in &di.drawables {
424                            if let Some(uoid) = key_ref {
425                                assert_eq!(
426                                    uoid.class_type,
427                                    ClassIndex::PL_DRAWABLE_SPANS,
428                                    "DrawInterface should reference plDrawableSpans, got 0x{:04X} for {}",
429                                    uoid.class_type,
430                                    key.object_name
431                                );
432                            }
433                        }
434                    }
435                    Err(e) => {
436                        panic!(
437                            "Failed to parse DrawInterface '{}': {}",
438                            key.object_name, e
439                        );
440                    }
441                }
442            }
443        }
444
445        eprintln!(
446            "Parsed {} plDrawInterfaces ({} total drawable refs) from Cleft",
447            parsed, total_drawables
448        );
449        assert!(parsed > 0, "Should have parsed draw interfaces");
450        assert!(total_drawables > 0, "Should have drawable references");
451    }
452
453    /// Extract spawn point transforms by following SceneObject → modifier → CoordinateInterface chain.
454    #[test]
455    fn find_cleft_spawn_transforms() {
456        use crate::core::class_index::ClassIndex;
457        use crate::resource::prp::PrpPage;
458        use std::path::Path;
459
460        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Desert.prp");
461        if !path.exists() { return; }
462        let page = PrpPage::from_file(path).unwrap();
463
464        let spawn_names: std::collections::HashSet<String> = page.keys_of_type(ClassIndex::PL_SPAWN_MODIFIER)
465            .iter().map(|k| k.object_name.clone()).collect();
466        assert!(!spawn_names.is_empty());
467
468        let mut found = 0;
469        for so_key in page.keys_of_type(ClassIndex::PL_SCENE_OBJECT) {
470            let has_spawn = if let Some(data) = page.object_data(so_key) {
471                let mut c = Cursor::new(data);
472                let _ = c.read_i16().unwrap();
473                SceneObjectData::read(&mut c).map_or(false, |so| {
474                    so.modifiers.iter().any(|m| m.as_ref().map_or(false, |u| spawn_names.contains(&u.object_name)))
475                })
476            } else { false };
477            if !has_spawn { continue; }
478
479            if let Some(data) = page.object_data(so_key) {
480                let mut cursor = Cursor::new(data);
481                let _ = cursor.read_i16().unwrap();
482                if let Ok(so) = SceneObjectData::read(&mut cursor) {
483                    if let Some(ci_uoid) = &so.coord_interface {
484                        for ci_key in page.keys_of_type(ClassIndex::PL_COORDINATE_INTERFACE) {
485                            if ci_key.object_name != ci_uoid.object_name { continue; }
486                            if let Some(ci_data) = page.object_data(ci_key) {
487                                let mut ci_cursor = Cursor::new(ci_data);
488                                let _ = ci_cursor.read_i16().unwrap();
489                                if let Ok(ci) = CoordinateInterfaceData::read(&mut ci_cursor) {
490                                    let m = ci.local_to_world;
491                                    // Row-major: translation at m[3], m[7], m[11]
492                                    found += 1;
493                                    if so_key.object_name == "LinkInPointDefault" {
494                                        // Verify the known Cleft LinkInPointDefault position
495                                        assert!((m[3] - -147.78).abs() < 1.0, "X mismatch: {}", m[3]);
496                                        assert!((m[7] - -648.55).abs() < 1.0, "Y mismatch: {}", m[7]);
497                                    }
498                                }
499                            }
500                        }
501                    }
502                }
503            }
504        }
505        assert!(found > 0, "Should have found spawn transforms");
506    }
507}