1use anyhow::{Context, Result, bail};
7use std::io::{Read, Write, Seek, SeekFrom, Cursor};
8use std::path::Path;
9
10pub trait PlasmaRead: Read {
15 fn read_u8(&mut self) -> Result<u8> {
16 let mut buf = [0u8; 1];
17 self.read_exact(&mut buf)?;
18 Ok(buf[0])
19 }
20
21 fn read_u16(&mut self) -> Result<u16> {
22 let mut buf = [0u8; 2];
23 self.read_exact(&mut buf)?;
24 Ok(u16::from_le_bytes(buf))
25 }
26
27 fn read_u32(&mut self) -> Result<u32> {
28 let mut buf = [0u8; 4];
29 self.read_exact(&mut buf)?;
30 Ok(u32::from_le_bytes(buf))
31 }
32
33 fn read_i16(&mut self) -> Result<i16> {
34 let mut buf = [0u8; 2];
35 self.read_exact(&mut buf)?;
36 Ok(i16::from_le_bytes(buf))
37 }
38
39 fn read_i32(&mut self) -> Result<i32> {
40 let mut buf = [0u8; 4];
41 self.read_exact(&mut buf)?;
42 Ok(i32::from_le_bytes(buf))
43 }
44
45 fn read_f32(&mut self) -> Result<f32> {
46 let mut buf = [0u8; 4];
47 self.read_exact(&mut buf)?;
48 Ok(f32::from_le_bytes(buf))
49 }
50
51 fn read_f64(&mut self) -> Result<f64> {
52 let mut buf = [0u8; 8];
53 self.read_exact(&mut buf)?;
54 Ok(f64::from_le_bytes(buf))
55 }
56
57 fn read_safe_string(&mut self) -> Result<String> {
58 let raw_len = self.read_u16()?;
59 let len = (raw_len & 0x0FFF) as usize;
60 if len == 0 {
61 return Ok(String::new());
62 }
63 let mut buf = vec![0u8; len];
64 self.read_exact(&mut buf)?;
65
66 if raw_len & 0xF000 == 0xF000 {
68 for byte in &mut buf {
69 *byte = !*byte;
70 }
71 }
72
73 if buf.last() == Some(&0) {
75 buf.pop();
76 }
77
78 Ok(String::from_utf8_lossy(&buf).into_owned())
79 }
80
81 fn read_matrix44(&mut self) -> Result<[f32; 16]> {
82 let mut m = [0f32; 16];
83 for val in &mut m {
84 *val = self.read_f32()?;
85 }
86 Ok(m)
87 }
88
89 fn skip(&mut self, n: usize) -> Result<()> {
90 let mut buf = vec![0u8; n];
91 self.read_exact(&mut buf)?;
92 Ok(())
93 }
94}
95
96impl<T: Read> PlasmaRead for T {}
97
98pub trait PlasmaWrite: Write {
103 fn write_u8(&mut self, v: u8) -> Result<()> {
104 self.write_all(&[v])?;
105 Ok(())
106 }
107
108 fn write_u16(&mut self, v: u16) -> Result<()> {
109 self.write_all(&v.to_le_bytes())?;
110 Ok(())
111 }
112
113 fn write_u32(&mut self, v: u32) -> Result<()> {
114 self.write_all(&v.to_le_bytes())?;
115 Ok(())
116 }
117
118 fn write_i16(&mut self, v: i16) -> Result<()> {
119 self.write_all(&v.to_le_bytes())?;
120 Ok(())
121 }
122
123 fn write_i32(&mut self, v: i32) -> Result<()> {
124 self.write_all(&v.to_le_bytes())?;
125 Ok(())
126 }
127
128 fn write_f32(&mut self, v: f32) -> Result<()> {
129 self.write_all(&v.to_le_bytes())?;
130 Ok(())
131 }
132
133 fn write_f64(&mut self, v: f64) -> Result<()> {
134 self.write_all(&v.to_le_bytes())?;
135 Ok(())
136 }
137
138 fn write_safe_string(&mut self, s: &str) -> Result<()> {
139 let bytes = s.as_bytes();
140 let len = bytes.len();
141 let raw_len = (len as u16) | 0xF000; self.write_all(&raw_len.to_le_bytes())?;
143
144 let inverted: Vec<u8> = bytes.iter().map(|b| !b).collect();
146 self.write_all(&inverted)?;
147 Ok(())
148 }
149
150 fn write_matrix44(&mut self, m: &[f32; 16]) -> Result<()> {
151 for val in m {
152 self.write_all(&val.to_le_bytes())?;
153 }
154 Ok(())
155 }
156}
157
158impl<T: Write> PlasmaWrite for T {}
159
160#[derive(Debug, Clone)]
165pub struct PageHeader {
166 pub version: u32,
167 pub sequence_number: u32,
168 pub flags: u16,
169 pub age_name: String,
170 pub page_name: String,
171 pub major_version: u16,
172 pub checksum: u32,
173 pub data_start: u32,
174 pub index_start: u32,
175 pub class_versions: Vec<(u16, u16)>,
178}
179
180impl PageHeader {
181 fn read(reader: &mut impl Read) -> Result<Self> {
182 let version = reader.read_u32()?;
183 if version != 6 {
184 bail!("Unsupported .prp version: {} (expected 6)", version);
185 }
186
187 let sequence_number = reader.read_u32()?;
188 let flags = reader.read_u16()?;
189 let age_name = reader.read_safe_string()?;
190 let page_name = reader.read_safe_string()?;
191 let major_version = reader.read_u16()?;
192 let checksum = reader.read_u32()?;
193 let data_start = reader.read_u32()?;
194 let index_start = reader.read_u32()?;
195
196 let mut class_versions = Vec::new();
198 if data_start > 0 {
199 let num_class_versions = reader.read_u16()?;
200 for _ in 0..num_class_versions {
201 let class_type = reader.read_u16()?;
202 let version = reader.read_u16()?;
203 class_versions.push((class_type, version));
204 }
205 }
206
207 Ok(Self {
208 version,
209 sequence_number,
210 flags,
211 age_name,
212 page_name,
213 major_version,
214 checksum,
215 data_start,
216 index_start,
217 class_versions,
218 })
219 }
220
221 fn write(&self, writer: &mut impl Write) -> Result<()> {
222 writer.write_u32(self.version)?;
223 writer.write_u32(self.sequence_number)?;
224 writer.write_u16(self.flags)?;
225 writer.write_safe_string(&self.age_name)?;
226 writer.write_safe_string(&self.page_name)?;
227 writer.write_u16(self.major_version)?;
228 writer.write_u32(self.checksum)?;
229 writer.write_u32(self.data_start)?;
230 writer.write_u32(self.index_start)?;
231
232 if self.data_start > 0 {
234 writer.write_u16(self.class_versions.len() as u16)?;
235 for &(class_type, version) in &self.class_versions {
236 writer.write_u16(class_type)?;
237 writer.write_u16(version)?;
238 }
239 }
240
241 Ok(())
242 }
243}
244
245#[derive(Debug, Clone)]
250pub struct ObjectKey {
251 pub class_type: u16,
252 pub object_name: String,
253 pub object_id: u32,
254 pub start_pos: u32,
255 pub data_len: u32,
256 pub location_sequence: u32,
257 pub location_flags: u16,
258 pub load_mask: u8,
261 pub clone_id: u16,
264 pub clone_player_id: u32,
267}
268
269impl ObjectKey {
270 pub fn to_uoid(&self) -> crate::core::uoid::Uoid {
272 crate::core::uoid::Uoid {
273 location: crate::core::location::Location {
274 sequence_number: self.location_sequence,
275 flags: self.location_flags,
276 },
277 class_type: self.class_type,
278 object_name: self.object_name.clone(),
279 object_id: self.object_id,
280 load_mask: crate::core::load_mask::LoadMask::from_byte(self.load_mask),
281 clone_id: self.clone_id,
282 clone_player_id: self.clone_player_id,
283 }
284 }
285
286 fn read(reader: &mut impl Read) -> Result<Self> {
287 let contents = reader.read_u8()?;
288
289 let location_sequence = reader.read_u32()?;
291 let location_flags = reader.read_u16()?;
292
293 let load_mask = if contents & 0x02 != 0 {
296 reader.read_u8()?
297 } else {
298 0xFF };
300
301 let class_type = reader.read_u16()?;
302 let object_id = reader.read_u32()?;
303 let object_name = reader.read_safe_string()?;
304
305 let (clone_id, clone_player_id) = if contents & 0x01 != 0 {
308 let clone_id = reader.read_u16()?;
309 let _reserved = reader.read_u16()?;
310 let clone_player_id = reader.read_u32()?;
311 (clone_id, clone_player_id)
312 } else {
313 (0, 0)
314 };
315
316 let start_pos = reader.read_u32()?;
318 let data_len = reader.read_u32()?;
319
320 Ok(Self {
321 class_type,
322 object_name,
323 object_id,
324 start_pos,
325 data_len,
326 location_sequence,
327 location_flags,
328 load_mask,
329 clone_id,
330 clone_player_id,
331 })
332 }
333
334 pub fn write(&self, writer: &mut impl Write) -> Result<()> {
336 let has_load_mask = self.load_mask != 0xFF;
338 let has_clone = self.clone_id != 0 || self.clone_player_id != 0;
339 let mut contents: u8 = 0;
340 if has_clone { contents |= 0x01; }
341 if has_load_mask { contents |= 0x02; }
342 writer.write_u8(contents)?;
343
344 writer.write_u32(self.location_sequence)?;
346 writer.write_u16(self.location_flags)?;
347
348 if has_load_mask {
350 writer.write_u8(self.load_mask)?;
351 }
352
353 writer.write_u16(self.class_type)?;
354 writer.write_u32(self.object_id)?;
355 writer.write_safe_string(&self.object_name)?;
356
357 if has_clone {
359 writer.write_u16(self.clone_id)?;
360 writer.write_u16(0u16)?; writer.write_u32(self.clone_player_id)?;
362 }
363
364 writer.write_u32(self.start_pos)?;
366 writer.write_u32(self.data_len)?;
367
368 Ok(())
369 }
370}
371
372#[allow(dead_code)]
377pub mod class_types {
378 pub const PL_SCENE_NODE: u16 = 0x0000;
380 pub const PL_SCENE_OBJECT: u16 = 0x0001;
381 pub const PL_MIPMAP: u16 = 0x0004;
382 pub const PL_CUBIC_ENVIRONMAP: u16 = 0x0005;
383 pub const PL_LAYER: u16 = 0x0006;
384 pub const HS_GMATERIAL: u16 = 0x0007;
385 pub const PL_DRAWABLE_SPANS: u16 = 0x004C;
386 pub const PL_CLUSTER_GROUP: u16 = 0x012B;
387 pub const PL_DYNAMIC_TEXT_MAP: u16 = 0x00AD;
388}
389
390#[derive(Debug, Clone)]
396pub struct KeyIndexGroup {
397 pub class_type: u16,
398 pub deprecated_flags: u8,
399}
400
401#[derive(Debug)]
402pub struct PrpPage {
403 pub header: PageHeader,
404 pub keys: Vec<ObjectKey>,
405 pub key_groups: Vec<KeyIndexGroup>,
408 data: Vec<u8>,
409}
410
411impl PrpPage {
412 fn parse_keys(cursor: &mut Cursor<&[u8]>, index_start: u64) -> Result<(Vec<ObjectKey>, Vec<KeyIndexGroup>)> {
413 cursor.seek(SeekFrom::Start(index_start))?;
414 let mut keys = Vec::new();
415 let mut key_groups = Vec::new();
416
417 let num_class_types = cursor.read_u32()?;
418 for _ in 0..num_class_types {
419 let class_type = cursor.read_u16()?;
420 let key_list_len = cursor.read_u32()?;
421 let pos_before = cursor.position();
422 let pos_end = pos_before + key_list_len as u64;
423
424 let deprecated_flags = cursor.read_u8()?;
425 let num_keys = cursor.read_u32()?;
426
427 key_groups.push(KeyIndexGroup { class_type, deprecated_flags });
428
429 for _ in 0..num_keys {
430 if cursor.position() >= pos_end { break; }
431 match ObjectKey::read(cursor) {
432 Ok(key) => keys.push(key),
433 Err(_) => break,
434 }
435 }
436 cursor.seek(SeekFrom::Start(pos_end))?;
437 }
438
439 Ok((keys, key_groups))
440 }
441
442 pub fn from_file(path: &Path) -> Result<Self> {
443 let data = std::fs::read(path)
444 .with_context(|| format!("Failed to read {:?}", path))?;
445 let mut cursor = Cursor::new(data.as_slice());
446
447 let header = PageHeader::read(&mut cursor)
448 .with_context(|| format!("Failed to parse header of {:?}", path))?;
449
450 let (keys, key_groups) = Self::parse_keys(&mut cursor, header.index_start as u64)?;
451
452 Ok(Self { header, keys, key_groups, data })
453 }
454
455 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
457 let mut cursor = Cursor::new(data.as_slice());
458 let header = PageHeader::read(&mut cursor)
459 .with_context(|| "Failed to parse PRP header from bytes")?;
460
461 let (keys, key_groups) = Self::parse_keys(&mut cursor, header.index_start as u64)?;
462
463 Ok(Self { header, keys, key_groups, data })
464 }
465
466 pub fn object_data(&self, key: &ObjectKey) -> Option<&[u8]> {
468 let start = key.start_pos as usize;
469 let end = start + key.data_len as usize;
470 if end <= self.data.len() {
471 Some(&self.data[start..end])
472 } else {
473 None
474 }
475 }
476
477 pub fn raw_data(&self) -> &[u8] {
479 &self.data
480 }
481
482 pub fn keys_of_type(&self, class_type: u16) -> Vec<&ObjectKey> {
484 self.keys.iter().filter(|k| k.class_type == class_type).collect()
485 }
486
487 pub fn count_by_type(&self, class_type: u16) -> usize {
489 self.keys.iter().filter(|k| k.class_type == class_type).count()
490 }
491
492 pub fn to_bytes(&self) -> Result<Vec<u8>> {
496 let mut buf: Vec<u8> = Vec::with_capacity(self.data.len());
497
498 self.header.write(&mut buf)?;
500
501 let data_start = self.header.data_start as usize;
503 let index_start = self.header.index_start as usize;
504 if data_start <= self.data.len() && index_start <= self.data.len() {
505 while buf.len() < data_start {
507 buf.push(0);
508 }
509 buf.extend_from_slice(&self.data[data_start..index_start]);
510 }
511
512 self.write_key_index(&mut buf)?;
514
515 Ok(buf)
516 }
517
518 fn write_key_index(&self, writer: &mut impl Write) -> Result<()> {
520 let num_class_types = self.key_groups.len() as u32;
522 writer.write_u32(num_class_types)?;
523
524 for group in &self.key_groups {
525 let class_keys: Vec<&ObjectKey> = self.keys.iter()
526 .filter(|k| k.class_type == group.class_type)
527 .collect();
528
529 let mut key_payload = Vec::new();
532 key_payload.write_u8(group.deprecated_flags)?;
533 key_payload.write_u32(class_keys.len() as u32)?;
534 for key in &class_keys {
535 key.write(&mut key_payload)?;
536 }
537
538 writer.write_u16(group.class_type)?;
539 writer.write_u32(key_payload.len() as u32)?;
540 writer.write_all(&key_payload)?;
541 }
542
543 Ok(())
544 }
545
546 pub fn save(&self, path: &Path) -> Result<()> {
548 let bytes = self.to_bytes()?;
549 std::fs::write(path, &bytes)
550 .with_context(|| format!("Failed to write {:?}", path))?;
551 Ok(())
552 }
553}
554
555pub fn read_key_name(reader: &mut (impl Read + Seek)) -> Result<Option<String>> {
560 let non_nil = reader.read_u8()?;
561 if non_nil == 0 {
562 return Ok(None);
563 }
564 let contents = reader.read_u8()?;
565 reader.skip(6)?; if contents & 0x02 != 0 {
568 reader.skip(1)?; }
570 let _class_type = reader.read_u16()?;
571 let _object_id = reader.read_u32()?;
572 let name = reader.read_safe_string()?;
573 if contents & 0x01 != 0 {
574 reader.skip(8)?; }
576 Ok(Some(name))
577}
578
579pub fn skip_synched_object_pub(reader: &mut (impl Read + Seek)) -> Result<()> {
581 skip_synched_object(reader)
582}
583
584fn skip_synched_object(reader: &mut (impl Read + Seek)) -> Result<()> {
585 let synch_flags = reader.read_u32()?;
586 if synch_flags & 0x10 != 0 { let n = reader.read_u16()?;
588 for _ in 0..n { reader.read_safe_string()?; }
589 }
590 if synch_flags & 0x20 != 0 { let n = reader.read_u16()?;
592 for _ in 0..n { reader.read_safe_string()?; }
593 }
594 Ok(())
595}
596
597pub fn parse_material_layers(data: &[u8]) -> Result<Vec<String>> {
603 let mut cursor = Cursor::new(data);
604
605 let class_idx = cursor.read_i16()?;
607 if class_idx < 0 { bail!("Null creatable"); }
608
609 read_key_name(&mut cursor)?;
611
612 skip_synched_object(&mut cursor)?;
614
615 let _load_flags = cursor.read_u32()?;
617 let _comp_flags = cursor.read_u32()?;
618 let num_layers = cursor.read_u32()?;
619 let _num_piggybacks = cursor.read_u32()?;
620
621 let mut layer_names = Vec::new();
622 for _ in 0..num_layers {
623 if let Some(name) = read_key_name(&mut cursor)? {
624 layer_names.push(name);
625 }
626 }
627
628 Ok(layer_names)
629}
630
631pub fn parse_layer_texture(data: &[u8]) -> Result<Option<String>> {
637 let mut cursor = Cursor::new(data);
638
639 let class_idx = cursor.read_i16()?;
641 if class_idx < 0 { bail!("Null creatable"); }
642
643 read_key_name(&mut cursor)?;
647 skip_synched_object(&mut cursor)?;
648 read_key_name(&mut cursor)?;
650
651 cursor.skip(20)?;
654 let has_matrix = cursor.read_u8()?;
656 if has_matrix != 0 {
657 cursor.skip(64)?; }
659 cursor.skip(16)?;
661 cursor.skip(16)?;
663 cursor.skip(16)?;
665 cursor.skip(16)?;
667 cursor.skip(4)?;
669 cursor.skip(4)?;
671 cursor.skip(4)?;
673 cursor.skip(4)?;
675
676 let texture_name = read_key_name(&mut cursor)?;
678
679 Ok(texture_name)
680}
681
682#[derive(Debug, Clone)]
684pub struct LayerState {
685 pub name: String,
686 pub texture_name: Option<String>,
687 pub blend_flags: u32,
688 pub shade_flags: u32,
689 pub misc_flags: u32,
690 pub z_flags: u32,
691 pub uv_channel: u8,
692 pub uvw_src_full: u32,
694 pub opacity: f32,
695 pub uv_transform: Option<[[f32; 4]; 4]>,
699 pub preshade_color: [f32; 4],
702 pub runtime_color: [f32; 4],
703 pub ambient_color: [f32; 4],
704 pub specular_color: [f32; 4],
705}
706
707pub fn parse_layer_state(data: &[u8]) -> Result<LayerState> {
709 use crate::core::uoid::read_key_uoid;
710 use crate::core::synched_object::SynchedObjectData;
711
712 let mut cursor = Cursor::new(data);
713 let class_idx = cursor.read_i16()?;
714 if class_idx < 0 { bail!("Null creatable"); }
715
716 let self_key = read_key_uoid(&mut cursor)?;
717 let name = self_key.map(|u| u.object_name).unwrap_or_default();
718 let _synched = SynchedObjectData::read(&mut cursor)?;
719 let _underlay = read_key_uoid(&mut cursor)?;
720
721 let blend_flags = cursor.read_u32()?;
723 let _clamp_flags = cursor.read_u32()?;
724 let shade_flags = cursor.read_u32()?;
725 let z_flags = cursor.read_u32()?;
726 let misc_flags = cursor.read_u32()?;
727
728 let has_matrix = cursor.read_u8()?;
731 let uv_transform = if has_matrix != 0 {
732 let mut m = [[0.0f32; 4]; 4];
733 for row in &mut m {
734 for val in row.iter_mut() {
735 *val = cursor.read_f32()?;
736 }
737 }
738 Some(m)
739 } else {
740 None
741 };
742 let mut preshade_color = [0.0f32; 4];
745 let mut runtime_color = [0.0f32; 4];
746 let mut ambient_color = [0.0f32; 4];
747 let mut specular_color = [0.0f32; 4];
748 for c in &mut preshade_color { *c = cursor.read_f32()?; }
749 for c in &mut runtime_color { *c = cursor.read_f32()?; }
750 for c in &mut ambient_color { *c = cursor.read_f32()?; }
751 for c in &mut specular_color { *c = cursor.read_f32()?; }
752 let uvwsrc = cursor.read_u32()?;
754 let opacity = cursor.read_f32()?;
755 cursor.skip(8)?; let tex = read_key_uoid(&mut cursor)?;
759 let texture_name = tex.map(|u| u.object_name);
760
761 Ok(LayerState {
762 name,
763 texture_name,
764 blend_flags,
765 shade_flags,
766 misc_flags,
767 z_flags,
768 uv_channel: (uvwsrc & 0xFFFF) as u8,
769 uvw_src_full: uvwsrc,
770 opacity,
771 uv_transform,
772 preshade_color,
773 runtime_color,
774 ambient_color,
775 specular_color,
776 })
777}
778
779pub fn parse_material_comp_flags(data: &[u8]) -> Result<u32> {
781 use crate::core::uoid::read_key_uoid;
782 use crate::core::synched_object::SynchedObjectData;
783
784 let mut cursor = Cursor::new(data);
785 let class_idx = cursor.read_i16()?;
786 if class_idx < 0 { bail!("Null creatable"); }
787
788 let _self_key = read_key_uoid(&mut cursor)?;
789 let _synched = SynchedObjectData::read(&mut cursor)?;
790
791 let _load_flags = cursor.read_u32()?;
792 let comp_flags = cursor.read_u32()?;
793 Ok(comp_flags)
794}
795
796pub fn parse_layer_uv_channel(data: &[u8]) -> Result<u8> {
799 use crate::core::uoid::read_key_uoid;
800 use crate::core::synched_object::SynchedObjectData;
801
802 let mut cursor = Cursor::new(data);
803 let class_idx = cursor.read_i16()?;
804 if class_idx < 0 { bail!("Null creatable"); }
805
806 let _self_key = read_key_uoid(&mut cursor)?;
808 let _synched = SynchedObjectData::read(&mut cursor)?;
809 let _underlay = read_key_uoid(&mut cursor)?;
810
811 cursor.skip(20)?; let has_matrix = cursor.read_u8()?;
814 if has_matrix != 0 { cursor.skip(64)?; }
815 cursor.skip(64)?; let uvwsrc = cursor.read_u32()?;
817
818 Ok((uvwsrc & 0xFFFF) as u8)
819}
820
821#[derive(Debug, Clone, Copy)]
824pub struct UvScrollRate {
825 pub u: f32,
826 pub v: f32,
827}
828
829#[allow(dead_code)]
831mod key_types {
832 pub const POINT3: u8 = 1;
833 pub const BEZ_POINT3: u8 = 2;
834 pub const SCALAR: u8 = 3;
835 pub const BEZ_SCALAR: u8 = 4;
836 pub const SCALE: u8 = 5;
837 pub const BEZ_SCALE: u8 = 6;
838 pub const QUAT: u8 = 7;
839 pub const COMPRESSED_QUAT32: u8 = 8;
840 pub const COMPRESSED_QUAT64: u8 = 9;
841 pub const MAX_KEY: u8 = 10;
842 pub const MATRIX33: u8 = 11;
843 pub const MATRIX44: u8 = 12;
844}
845
846fn key_size(key_type: u8) -> Option<usize> {
848 match key_type {
849 key_types::POINT3 => Some(2 + 12), key_types::BEZ_POINT3 => Some(2 + 36), key_types::SCALAR => Some(2 + 4), key_types::BEZ_SCALAR => Some(2 + 12), key_types::SCALE => Some(2 + 28), key_types::BEZ_SCALE => Some(2 + 52), key_types::QUAT => Some(2 + 16), key_types::COMPRESSED_QUAT32 => Some(2 + 4), key_types::COMPRESSED_QUAT64 => Some(2 + 8), key_types::MATRIX33 => Some(2 + 36), key_types::MATRIX44 => Some(2 + 64), _ => None,
861 }
862}
863
864const LEAF_CONTROLLER: u16 = 0x0230; const COMPOUND_CONTROLLER: u16 = 0x0231; fn skip_creatable(cursor: &mut Cursor<&[u8]>) -> Result<bool> {
875 let class_idx = cursor.read_u16()?;
876 if class_idx == 0x8000 { return Ok(false); } match class_idx {
879 LEAF_CONTROLLER => {
880 skip_leaf_controller(cursor)?;
881 }
882 COMPOUND_CONTROLLER => {
883 for _ in 0..3 {
886 skip_creatable(cursor)?;
887 }
888 }
889 _ => {
890 bail!("Unknown controller class 0x{:04X}", class_idx);
893 }
894 }
895 Ok(true)
896}
897
898fn skip_leaf_controller(cursor: &mut Cursor<&[u8]>) -> Result<()> {
900 let key_type = cursor.read_u8()?;
901 let num_keys = cursor.read_u32()?;
902 if let Some(ks) = key_size(key_type) {
903 cursor.skip(ks * num_keys as usize)?;
904 } else if key_type == key_types::MAX_KEY {
905 cursor.skip(62 * num_keys as usize)?;
908 } else {
909 bail!("Unknown key type {} with {} keys", key_type, num_keys);
910 }
911 Ok(())
912}
913
914fn read_transform_leaf_controller(cursor: &mut Cursor<&[u8]>) -> Result<Option<UvScrollRate>> {
918 let key_type = cursor.read_u8()?;
919 let num_keys = cursor.read_u32()?;
920
921 if key_type != key_types::MATRIX44 || num_keys < 2 {
922 if let Some(ks) = key_size(key_type) {
924 cursor.skip(ks * num_keys as usize)?;
925 } else if key_type == key_types::MAX_KEY {
926 cursor.skip(62 * num_keys as usize)?;
927 } else {
928 bail!("Unknown key type {} with {} keys", key_type, num_keys);
929 }
930 return Ok(None);
931 }
932
933 let frame0 = cursor.read_u16()?;
936 let mat0 = cursor.read_matrix44()?;
937
938 if num_keys > 2 {
940 let skip_bytes = 66 * (num_keys as usize - 2);
941 let pos = cursor.position() as usize;
942 let data_len = cursor.get_ref().len();
943 if pos + skip_bytes + 66 > data_len {
944 return Ok(None);
946 }
947 cursor.skip(skip_bytes)?;
948 }
949
950 let frame_last = cursor.read_u16()?;
951 let mat_last = cursor.read_matrix44()?;
952
953 let dt_frames = frame_last as f32 - frame0 as f32;
954 if dt_frames <= 0.0 { return Ok(None); }
955
956 let dt_secs = dt_frames / 30.0;
958
959 let du = mat_last[3] - mat0[3];
962 let dv = mat_last[7] - mat0[7];
963
964 let rate_u = du / dt_secs;
965 let rate_v = dv / dt_secs;
966
967 if (rate_u.abs() < 1e-6 && rate_v.abs() < 1e-6) || rate_u.abs() > 100.0 || rate_v.abs() > 100.0 {
969 return Ok(None);
970 }
971
972 Ok(Some(UvScrollRate {
973 u: rate_u,
974 v: rate_v,
975 }))
976}
977
978#[derive(Debug, Clone)]
987pub struct LayerAnimData {
988 pub self_key: Option<crate::core::uoid::Uoid>,
989 pub underlay_key: Option<crate::core::uoid::Uoid>,
990 pub opacity_keys: Vec<(f32, f32)>,
992 pub has_opacity: bool,
994 pub preshade_color_keys: Vec<(f32, [f32; 3])>,
997 pub runtime_color_keys: Vec<(f32, [f32; 3])>,
999 pub ambient_color_keys: Vec<(f32, [f32; 3])>,
1001 pub specular_color_keys: Vec<(f32, [f32; 3])>,
1003 pub length: f32,
1006 pub sdl_var_name: Option<String>,
1009}
1010
1011fn read_color_keys(cursor: &mut Cursor<&[u8]>) -> Result<Vec<(f32, [f32; 3])>> {
1014 let mut keys = Vec::new();
1015 let class_id = cursor.read_u16()?;
1016 if class_id == 0x8000 { return Ok(keys); }
1017
1018 if class_id == LEAF_CONTROLLER {
1019 let key_type = cursor.read_u8()?;
1020 let num_keys = cursor.read_u32()?;
1021 if key_type == key_types::POINT3 {
1022 for _ in 0..num_keys {
1023 let frame = cursor.read_u16()? as f32 / 30.0;
1024 let x = cursor.read_f32()?;
1025 let y = cursor.read_f32()?;
1026 let z = cursor.read_f32()?;
1027 keys.push((frame, [x, y, z]));
1028 }
1029 } else if key_type == key_types::BEZ_POINT3 {
1030 for _ in 0..num_keys {
1031 let frame = cursor.read_u16()? as f32 / 30.0;
1032 cursor.skip(24)?; let x = cursor.read_f32()?;
1034 let y = cursor.read_f32()?;
1035 let z = cursor.read_f32()?;
1036 keys.push((frame, [x, y, z]));
1037 }
1038 } else {
1039 if let Some(ks) = key_size(key_type) {
1040 cursor.skip(ks * num_keys as usize)?;
1041 }
1042 }
1043 } else if class_id == COMPOUND_CONTROLLER {
1044 for _ in 0..3 { skip_creatable(cursor)?; }
1045 }
1046 Ok(keys)
1047}
1048
1049fn read_opacity_keys(cursor: &mut Cursor<&[u8]>) -> Result<(bool, Vec<(f32, f32)>)> {
1051 let mut keys = Vec::new();
1052 let class_id = cursor.read_u16()?;
1053 if class_id == 0x8000 { return Ok((false, keys)); }
1054
1055 if class_id == LEAF_CONTROLLER {
1056 let key_type = cursor.read_u8()?;
1057 let num_keys = cursor.read_u32()?;
1058 if key_type == key_types::SCALAR {
1059 for _ in 0..num_keys {
1060 let frame = cursor.read_u16()? as f32 / 30.0;
1061 let value = cursor.read_f32()?;
1062 keys.push((frame, value));
1063 }
1064 } else if key_type == key_types::BEZ_SCALAR {
1065 for _ in 0..num_keys {
1066 let frame = cursor.read_u16()? as f32 / 30.0;
1067 let _in_tan = cursor.read_f32()?;
1068 let _out_tan = cursor.read_f32()?;
1069 let value = cursor.read_f32()?;
1070 keys.push((frame, value));
1071 }
1072 } else {
1073 if let Some(ks) = key_size(key_type) {
1074 cursor.skip(ks * num_keys as usize)?;
1075 }
1076 }
1077 } else if class_id == COMPOUND_CONTROLLER {
1078 for _ in 0..3 { skip_creatable(cursor)?; }
1079 }
1080 Ok((true, keys))
1081}
1082
1083pub fn parse_layer_animation_full(data: &[u8]) -> Result<LayerAnimData> {
1086 use crate::core::uoid::read_key_uoid;
1087 use crate::core::synched_object::SynchedObjectData;
1088
1089 let mut cursor = Cursor::new(data);
1090 let class_idx = cursor.read_i16()?;
1091 if class_idx < 0 { bail!("Null creatable"); }
1092
1093 let self_key = read_key_uoid(&mut cursor)?;
1094 let _synched = SynchedObjectData::read(&mut cursor)?;
1095 let underlay_key = read_key_uoid(&mut cursor)?;
1096
1097 let preshade_color_keys = read_color_keys(&mut cursor)?;
1099 let runtime_color_keys = read_color_keys(&mut cursor)?;
1100 let ambient_color_keys = read_color_keys(&mut cursor)?;
1101 let specular_color_keys = read_color_keys(&mut cursor)?;
1102
1103 let (has_opacity, opacity_keys) = read_opacity_keys(&mut cursor)?;
1105
1106 skip_creatable(&mut cursor)?;
1108
1109 let mut length: f32 = 0.0;
1111 if let Some(last) = preshade_color_keys.last() { length = length.max(last.0); }
1112 if let Some(last) = runtime_color_keys.last() { length = length.max(last.0); }
1113 if let Some(last) = ambient_color_keys.last() { length = length.max(last.0); }
1114 if let Some(last) = specular_color_keys.last() { length = length.max(last.0); }
1115 if let Some(last) = opacity_keys.last() { length = length.max(last.0); }
1116
1117 Ok(LayerAnimData {
1118 self_key,
1119 underlay_key,
1120 opacity_keys,
1121 has_opacity,
1122 preshade_color_keys,
1123 runtime_color_keys,
1124 ambient_color_keys,
1125 specular_color_keys,
1126 length,
1127 sdl_var_name: None,
1128 })
1129}
1130
1131pub fn parse_layer_animation_scroll(data: &[u8]) -> Result<Option<UvScrollRate>> {
1132 use crate::core::uoid::read_key_uoid;
1133 use crate::core::synched_object::SynchedObjectData;
1134
1135 let mut cursor = Cursor::new(data);
1136 let class_idx = cursor.read_i16()?;
1137 if class_idx < 0 { bail!("Null creatable"); }
1138
1139 let _self_key = read_key_uoid(&mut cursor)?;
1141 let _synched = SynchedObjectData::read(&mut cursor)?;
1142 let _underlay = read_key_uoid(&mut cursor)?;
1143
1144 for _ in 0..5 {
1146 skip_creatable(&mut cursor)?;
1147 }
1148
1149 let xform_class = cursor.read_u16()?;
1151 if xform_class == 0x8000 { return Ok(None); } match xform_class {
1154 LEAF_CONTROLLER => {
1155 read_transform_leaf_controller(&mut cursor)
1156 }
1157 COMPOUND_CONTROLLER => {
1158 Ok(None)
1161 }
1162 _ => {
1163 Ok(None)
1165 }
1166 }
1167}
1168
1169pub fn parse_layer_sdl_animation_full(data: &[u8]) -> Result<LayerAnimData> {
1172 let mut la = parse_layer_animation_full(data)?;
1173 use crate::core::uoid::read_key_uoid;
1174 use crate::core::synched_object::SynchedObjectData;
1175
1176 let mut cursor = Cursor::new(data);
1177 let _class_idx = cursor.read_i16()?;
1178 let _self_key = read_key_uoid(&mut cursor)?;
1179 let _synched = SynchedObjectData::read(&mut cursor)?;
1180 let _underlay = read_key_uoid(&mut cursor)?;
1181 for _ in 0..6 { skip_creatable(&mut cursor)?; }
1182 match cursor.read_safe_string() {
1183 Ok(name) if !name.is_empty() => { la.sdl_var_name = Some(name); }
1184 _ => {}
1185 }
1186 Ok(la)
1187}
1188
1189pub fn parse_layer_animation_underlay(data: &[u8]) -> Result<Option<crate::core::uoid::Uoid>> {
1198 use crate::core::uoid::read_key_uoid;
1199 use crate::core::synched_object::SynchedObjectData;
1200
1201 let mut cursor = Cursor::new(data);
1202 let class_idx = cursor.read_i16()?;
1203 if class_idx < 0 { bail!("Null creatable"); }
1204
1205 let _self_key = read_key_uoid(&mut cursor)?;
1208 let _synched = SynchedObjectData::read(&mut cursor)?;
1209 let underlay = read_key_uoid(&mut cursor)?;
1210 Ok(underlay)
1211}
1212
1213#[cfg(test)]
1214mod scroll_tests {
1215 use super::*;
1216 use std::path::Path;
1217
1218 #[test]
1219 fn test_parse_cleft_layer_animation_scroll() {
1220 let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
1221 if !path.exists() {
1222 eprintln!("Skipping test: {:?} not found", path);
1223 return;
1224 }
1225 let page = PrpPage::from_file(path).unwrap();
1226 let mut parsed = 0;
1227 let mut ok = 0;
1228 let mut errs = 0;
1229 let mut with_scroll = 0;
1230
1231 for key in page.keys_of_type(0x0043) { if let Some(data) = page.object_data(key) {
1233 parsed += 1;
1234 match parse_layer_animation_scroll(data) {
1235 Ok(Some(scroll)) => {
1236 with_scroll += 1;
1237 ok += 1;
1238 eprintln!(" scroll '{}': u={:.4} v={:.4}", key.object_name, scroll.u, scroll.v);
1239 }
1240 Ok(None) => { ok += 1; }
1241 Err(e) => {
1242 errs += 1;
1243 if errs <= 3 {
1244 let hex: Vec<String> = data.iter().take(100).map(|b| format!("{:02x}", b)).collect();
1245 eprintln!(" ERR '{}': {} (data: {}...)", key.object_name, e, hex.join(" "));
1246 }
1247 }
1248 }
1249 }
1250 }
1251 eprintln!("plLayerAnimation: {} total, {} ok, {} with scroll, {} errors",
1252 parsed, ok, with_scroll, errs);
1253
1254 let path2 = Path::new("../../Plasma/staging/client/dat/Teledahn_District_Teledahn.prp");
1256 if !path2.exists() {
1257 eprintln!("Skipping Teledahn test");
1258 return;
1259 }
1260 let page2 = PrpPage::from_file(path2).unwrap();
1261 let mut t_parsed = 0;
1262 let mut t_ok = 0;
1263 let mut t_scroll = 0;
1264 let mut t_err = 0;
1265 for key in page2.keys_of_type(0x0043) {
1266 if let Some(data) = page2.object_data(key) {
1267 t_parsed += 1;
1268 match parse_layer_animation_scroll(data) {
1269 Ok(Some(scroll)) => {
1270 t_scroll += 1;
1271 t_ok += 1;
1272 eprintln!(" Teledahn scroll '{}': u={:.4} v={:.4}", key.object_name, scroll.u, scroll.v);
1273 }
1274 Ok(None) => { t_ok += 1; }
1275 Err(_) => { t_err += 1; }
1276 }
1277 }
1278 }
1279 eprintln!("Teledahn plLayerAnimation: {} total, {} ok, {} with scroll, {} errors",
1280 t_parsed, t_ok, t_scroll, t_err);
1281 }
1282
1283 #[test]
1284 fn test_parse_layer_animation_color_keys() {
1285 let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
1286 if !path.exists() { eprintln!("Skipping: {:?} not found", path); return; }
1287 let page = PrpPage::from_file(path).unwrap();
1288 let mut parsed = 0;
1289 let mut with_color = 0;
1290 let mut errors = 0;
1291 for key in page.keys_of_type(0x0043) {
1292 if let Some(data) = page.object_data(key) {
1293 parsed += 1;
1294 match parse_layer_animation_full(data) {
1295 Ok(la) => {
1296 let c = la.preshade_color_keys.len() + la.runtime_color_keys.len()
1297 + la.ambient_color_keys.len() + la.specular_color_keys.len();
1298 if c > 0 { with_color += 1; }
1299 }
1300 Err(_) => { errors += 1; }
1301 }
1302 }
1303 }
1304 eprintln!("Color test: {} total, {} with color, {} errors", parsed, with_color, errors);
1305 assert!(errors <= 2);
1306 }
1307
1308 #[test]
1309 fn test_parse_layer_sdl_animation() {
1310 let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
1311 if !path.exists() { eprintln!("Skipping: {:?} not found", path); return; }
1312 let page = PrpPage::from_file(path).unwrap();
1313 let mut parsed = 0;
1314 let mut errors = 0;
1315 for key in page.keys_of_type(0x00F0) {
1316 if let Some(data) = page.object_data(key) {
1317 parsed += 1;
1318 match parse_layer_sdl_animation_full(data) {
1319 Ok(la) => {
1320 eprintln!(" SDL '{}': var={:?} len={:.2}s", key.object_name, la.sdl_var_name, la.length);
1321 }
1322 Err(_) => { errors += 1; }
1323 }
1324 }
1325 }
1326 eprintln!("SDL anim: {} total, {} errors", parsed, errors);
1327 if parsed > 0 { assert_eq!(errors, 0); }
1328 }
1329}
1330
1331pub mod bitmap_compression {
1339 pub const UNCOMPRESSED: u8 = 0x0;
1340 pub const DIRECTX_COMPRESSION: u8 = 0x1;
1341 pub const JPEG_COMPRESSION: u8 = 0x2;
1342 pub const PNG_COMPRESSION: u8 = 0x3;
1343}
1344
1345pub mod dxt_type {
1348 pub const ERROR: u8 = 0x0;
1349 pub const DXT1: u8 = 0x1;
1350 pub const DXT5: u8 = 0x5;
1352}
1353
1354pub mod uncompressed_type {
1357 pub const RGB8888: u8 = 0x00;
1358 pub const RGB4444: u8 = 0x01;
1359 pub const RGB1555: u8 = 0x02;
1360 pub const INTEN8: u8 = 0x03;
1361 pub const A_INTEN88: u8 = 0x04;
1362}
1363
1364#[derive(Debug, Clone)]
1365pub struct MipmapData {
1366 pub name: String,
1367 pub width: u32,
1368 pub height: u32,
1369 pub compression: u8,
1371 pub pixel_format: u8,
1373 pub block_size: u8,
1375 pub pixel_size: u8,
1377 pub num_levels: u8,
1378 pub pixel_data: Vec<u8>,
1381 pub level_sizes: Vec<u32>,
1384 pub row_bytes: u32,
1386}
1387
1388impl MipmapData {
1389 pub fn decode_to_rgba(&self) -> Option<Vec<u8>> {
1392 if self.compression != 1 { if self.pixel_size == 32 && !self.pixel_data.is_empty() {
1396 let pixel_count = (self.width * self.height) as usize;
1397 let expected = pixel_count * 4;
1398 if self.pixel_data.len() < expected { return None; }
1399 let mut rgba = vec![0u8; expected];
1400 for i in 0..pixel_count {
1401 let s = i * 4;
1402 rgba[s] = self.pixel_data[s + 2]; rgba[s + 1] = self.pixel_data[s + 1]; rgba[s + 2] = self.pixel_data[s]; rgba[s + 3] = self.pixel_data[s + 3]; }
1407 return Some(rgba);
1408 }
1409 return None;
1410 }
1411 let w = self.width as usize;
1412 let h = self.height as usize;
1413 if w == 0 || h == 0 { return None; }
1414 let blocks_wide = (w + 3) / 4;
1415 let blocks_high = (h + 3) / 4;
1416 let is_dxt5 = self.pixel_format == 5;
1417 let block_size: usize = if is_dxt5 { 16 } else { 8 };
1418 let needed = blocks_wide * blocks_high * block_size;
1419 if self.pixel_data.len() < needed { return None; }
1420
1421 let mut rgba = vec![0u8; w * h * 4];
1422 let expand5 = |v: u8| -> u8 { (v << 3) | (v >> 2) };
1423 let expand6 = |v: u8| -> u8 { (v << 2) | (v >> 4) };
1424
1425 for by in 0..blocks_high {
1426 for bx in 0..blocks_wide {
1427 let block_off = (by * blocks_wide + bx) * block_size;
1428 let color_off = if is_dxt5 { block_off + 8 } else { block_off };
1429 let cb = &self.pixel_data[color_off..color_off + 8];
1430 let c0 = u16::from_le_bytes([cb[0], cb[1]]);
1431 let c1 = u16::from_le_bytes([cb[2], cb[3]]);
1432 let (r0, g0, b0) = ((c0 >> 11) as u8 & 0x1F, (c0 >> 5) as u8 & 0x3F, c0 as u8 & 0x1F);
1433 let (r1, g1, b1) = ((c1 >> 11) as u8 & 0x1F, (c1 >> 5) as u8 & 0x3F, c1 as u8 & 0x1F);
1434
1435 let colors: [[u8; 4]; 4] = if c0 > c1 {
1436 let a = [expand5(r0), expand6(g0), expand5(b0), 255];
1437 let b = [expand5(r1), expand6(g1), expand5(b1), 255];
1438 let c2 = [((2*a[0] as u16+b[0] as u16)/3) as u8, ((2*a[1] as u16+b[1] as u16)/3) as u8, ((2*a[2] as u16+b[2] as u16)/3) as u8, 255];
1439 let c3 = [((a[0] as u16+2*b[0] as u16)/3) as u8, ((a[1] as u16+2*b[1] as u16)/3) as u8, ((a[2] as u16+2*b[2] as u16)/3) as u8, 255];
1440 [a, b, c2, c3]
1441 } else {
1442 let a = [expand5(r0), expand6(g0), expand5(b0), 255];
1443 let b = [expand5(r1), expand6(g1), expand5(b1), 255];
1444 let c2 = [((a[0] as u16+b[0] as u16)/2) as u8, ((a[1] as u16+b[1] as u16)/2) as u8, ((a[2] as u16+b[2] as u16)/2) as u8, 255];
1445 [a, b, c2, [0, 0, 0, 0]]
1446 };
1447 let indices = u32::from_le_bytes([cb[4], cb[5], cb[6], cb[7]]);
1448
1449 let alpha_block: [u8; 16] = if is_dxt5 {
1450 let ab = &self.pixel_data[block_off..block_off + 8];
1451 let (a0, a1) = (ab[0], ab[1]);
1452 let mut alphas = [0u8; 8];
1453 alphas[0] = a0; alphas[1] = a1;
1454 if a0 > a1 {
1455 for i in 1..7u16 { alphas[(i+1) as usize] = (((7-i)*a0 as u16 + i*a1 as u16)/7) as u8; }
1456 } else {
1457 for i in 1..5u16 { alphas[(i+1) as usize] = (((5-i)*a0 as u16 + i*a1 as u16)/5) as u8; }
1458 alphas[6] = 0; alphas[7] = 255;
1459 }
1460 let idx_bits = (ab[2] as u64)|((ab[3] as u64)<<8)|((ab[4] as u64)<<16)|((ab[5] as u64)<<24)|((ab[6] as u64)<<32)|((ab[7] as u64)<<40);
1461 let mut result = [255u8; 16];
1462 for i in 0..16 { result[i] = alphas[((idx_bits >> (i as u64 * 3)) & 7) as usize]; }
1463 result
1464 } else { [255; 16] };
1465
1466 for py in 0..4 {
1467 for px in 0..4 {
1468 let (x, y) = (bx * 4 + px, by * 4 + py);
1469 if x >= w || y >= h { continue; }
1470 let pi = py * 4 + px;
1471 let ci = ((indices >> (pi * 2)) & 3) as usize;
1472 let out = y * w * 4 + x * 4;
1473 rgba[out] = colors[ci][0]; rgba[out+1] = colors[ci][1];
1474 rgba[out+2] = colors[ci][2]; rgba[out+3] = alpha_block[pi];
1475 }
1476 }
1477 }
1478 }
1479 Some(rgba)
1480 }
1481
1482 pub fn from_object_data(data: &[u8], name: &str) -> Result<Self> {
1486 let mut cursor = Cursor::new(data);
1487
1488 let class_idx = cursor.read_i16()?;
1490 if class_idx < 0 {
1491 bail!("Null creatable");
1492 }
1493
1494 read_key_name(&mut cursor)?;
1496
1497 let version = cursor.read_u8()?; if version != 2 {
1501 bail!("Unsupported bitmap version: {} (expected 2)", version);
1502 }
1503 let pixel_size = cursor.read_u8()?; let _space = cursor.read_u8()?; let _flags = cursor.read_u16()?; let compression_type = cursor.read_u8()?; let (pixel_format, block_size) = match compression_type {
1511 bitmap_compression::UNCOMPRESSED |
1512 bitmap_compression::JPEG_COMPRESSION |
1513 bitmap_compression::PNG_COMPRESSION => {
1514 let fmt = cursor.read_u8()?;
1516 (fmt, 0u8)
1517 }
1518 bitmap_compression::DIRECTX_COMPRESSION => {
1519 let bs = cursor.read_u8()?;
1521 let ct = cursor.read_u8()?;
1522 (ct, bs)
1523 }
1524 _ => {
1525 bail!("Unknown compression type: {}", compression_type);
1526 }
1527 };
1528
1529 let _low_modified_time = cursor.read_u32()?;
1531 let _high_modified_time = cursor.read_u32()?;
1532
1533 let width = cursor.read_u32()?;
1536 let height = cursor.read_u32()?;
1537 let row_bytes = cursor.read_u32()?;
1538 let total_size = cursor.read_u32()?;
1539 let num_levels = cursor.read_u8()?;
1540
1541 if width == 0 || height == 0 {
1542 return Ok(Self {
1543 name: name.to_string(), width, height,
1544 compression: compression_type, pixel_format, block_size,
1545 pixel_size, num_levels, pixel_data: Vec::new(),
1546 level_sizes: Vec::new(), row_bytes,
1547 });
1548 }
1549
1550 let level_sizes = build_level_sizes(
1553 width, height, row_bytes, num_levels,
1554 compression_type, block_size,
1555 );
1556
1557 let mut pixel_data = Vec::new();
1560 if total_size > 0 {
1561 let remaining = data.len().saturating_sub(cursor.position() as usize);
1562 let read_size = (total_size as usize).min(remaining);
1563
1564 match compression_type {
1565 bitmap_compression::DIRECTX_COMPRESSION => {
1566 pixel_data = vec![0u8; read_size];
1569 cursor.read_exact(&mut pixel_data)?;
1570 }
1571 bitmap_compression::UNCOMPRESSED => {
1572 pixel_data = vec![0u8; read_size];
1577 cursor.read_exact(&mut pixel_data)?;
1578 }
1579 bitmap_compression::JPEG_COMPRESSION => {
1580 pixel_data = vec![0u8; read_size];
1583 cursor.read_exact(&mut pixel_data)?;
1584 log::debug!("JPEG texture '{}' ({}x{}) — decompression not implemented",
1585 name, width, height);
1586 }
1587 bitmap_compression::PNG_COMPRESSION => {
1588 pixel_data = vec![0u8; read_size];
1590 cursor.read_exact(&mut pixel_data)?;
1591 log::debug!("PNG texture '{}' ({}x{}) — decompression not implemented",
1592 name, width, height);
1593 }
1594 _ => {
1595 bail!("Unknown compression type: {}", compression_type);
1596 }
1597 }
1598 }
1599
1600 Ok(Self {
1601 name: name.to_string(),
1602 width,
1603 height,
1604 compression: compression_type,
1605 pixel_format,
1606 block_size,
1607 pixel_size,
1608 num_levels,
1609 pixel_data,
1610 level_sizes,
1611 row_bytes,
1612 })
1613 }
1614
1615 pub fn from_cubic_envmap_data(data: &[u8], name: &str) -> Result<Vec<Self>> {
1623 let mut cursor = Cursor::new(data);
1624
1625 let class_idx = cursor.read_i16()?;
1627 if class_idx < 0 { bail!("Null creatable"); }
1628 read_key_name(&mut cursor)?;
1629
1630 Self::skip_bitmap_header(&mut cursor)?;
1632
1633 let mut faces = Vec::with_capacity(6);
1635 for i in 0..6 {
1636 let face_name = format!("{}_face{}", name, i);
1637 let face = Self::read_mipmap_from_cursor(&mut cursor, &face_name)?;
1638 faces.push(face);
1639 }
1640 Ok(faces)
1641 }
1642
1643 fn skip_bitmap_header(cursor: &mut Cursor<&[u8]>) -> Result<()> {
1645 let _version = cursor.read_u8()?;
1646 let _pixel_size = cursor.read_u8()?;
1647 let _space = cursor.read_u8()?;
1648 let _flags = cursor.read_u16()?;
1649 let compression_type = cursor.read_u8()?;
1650 match compression_type {
1651 bitmap_compression::UNCOMPRESSED |
1652 bitmap_compression::JPEG_COMPRESSION |
1653 bitmap_compression::PNG_COMPRESSION => { cursor.skip(1)?; }
1654 bitmap_compression::DIRECTX_COMPRESSION => { cursor.skip(2)?; }
1655 _ => { bail!("Unknown compression type: {}", compression_type); }
1656 }
1657 cursor.skip(8)?; Ok(())
1659 }
1660
1661 fn read_mipmap_from_cursor(cursor: &mut Cursor<&[u8]>, name: &str) -> Result<Self> {
1663 let version = cursor.read_u8()?;
1665 if version != 2 { bail!("Unsupported bitmap version: {}", version); }
1666 let pixel_size = cursor.read_u8()?;
1667 let _space = cursor.read_u8()?;
1668 let _flags = cursor.read_u16()?;
1669 let compression_type = cursor.read_u8()?;
1670 let (pixel_format, block_size) = match compression_type {
1671 bitmap_compression::UNCOMPRESSED |
1672 bitmap_compression::JPEG_COMPRESSION |
1673 bitmap_compression::PNG_COMPRESSION => { (cursor.read_u8()?, 0u8) }
1674 bitmap_compression::DIRECTX_COMPRESSION => {
1675 let bs = cursor.read_u8()?;
1676 let ct = cursor.read_u8()?;
1677 (ct, bs)
1678 }
1679 _ => { bail!("Unknown compression type: {}", compression_type); }
1680 };
1681 cursor.skip(8)?; let width = cursor.read_u32()?;
1685 let height = cursor.read_u32()?;
1686 let row_bytes = cursor.read_u32()?;
1687 let total_size = cursor.read_u32()?;
1688 let num_levels = cursor.read_u8()?;
1689
1690 if width == 0 || height == 0 || total_size == 0 {
1691 return Ok(Self {
1692 name: name.to_string(), width, height,
1693 compression: compression_type, pixel_format, block_size,
1694 pixel_size, num_levels, pixel_data: Vec::new(),
1695 level_sizes: Vec::new(), row_bytes,
1696 });
1697 }
1698
1699 let level_sizes = build_level_sizes(width, height, row_bytes, num_levels, compression_type, block_size);
1700 let remaining = cursor.get_ref().len().saturating_sub(cursor.position() as usize);
1701 let read_size = (total_size as usize).min(remaining);
1702 let mut pixel_data = vec![0u8; read_size];
1703 cursor.read_exact(&mut pixel_data)?;
1704
1705 Ok(Self {
1706 name: name.to_string(), width, height,
1707 compression: compression_type, pixel_format, block_size,
1708 pixel_size, num_levels, pixel_data, level_sizes, row_bytes,
1709 })
1710 }
1711}
1712
1713fn build_level_sizes(
1716 width: u32, height: u32, row_bytes: u32, num_levels: u8,
1717 compression_type: u8, block_size: u8,
1718) -> Vec<u32> {
1719 let mut sizes = Vec::with_capacity(num_levels as usize);
1720 let mut w = width;
1721 let mut h = height;
1722 let mut rb = row_bytes;
1723
1724 for _ in 0..num_levels {
1725 let level_size = if compression_type == bitmap_compression::DIRECTX_COMPRESSION {
1726 if (w | h) & 0x03 != 0 {
1728 h * rb
1730 } else {
1731 (h * w * block_size as u32) >> 4
1733 }
1734 } else {
1735 h * rb
1737 };
1738
1739 sizes.push(level_size);
1740
1741 if w > 1 { w >>= 1; rb >>= 1; }
1744 if h > 1 { h >>= 1; }
1745 }
1746
1747 sizes
1748}
1749
1750#[derive(Debug, Clone)]
1757pub enum PythonParamValue {
1758 Int(i32),
1759 Float(f32),
1760 Bool(bool),
1761 String(String),
1762 Key(Option<crate::core::uoid::Uoid>),
1763 None,
1764}
1765
1766#[derive(Debug, Clone)]
1768pub struct PythonParam {
1769 pub id: i32,
1770 pub value_type: i32,
1771 pub value: PythonParamValue,
1772}
1773
1774#[derive(Debug, Clone)]
1776pub struct PythonFileModData {
1777 pub script_file: String,
1779 pub receivers: Vec<crate::core::uoid::Uoid>,
1781 pub parameters: Vec<PythonParam>,
1783 pub self_key: Option<crate::core::uoid::Uoid>,
1785}
1786
1787pub fn parse_python_file_mod(data: &[u8]) -> Result<PythonFileModData> {
1795 use crate::core::uoid::read_key_uoid;
1796
1797 let mut cursor = Cursor::new(data);
1798
1799 let _class_idx = cursor.read_i16()?;
1801
1802 let self_key = read_key_uoid(&mut cursor)?;
1804
1805 skip_synched_object(&mut cursor)?;
1807
1808 let num_bit_vectors = cursor.read_u32()?;
1811 for _ in 0..num_bit_vectors {
1812 let _word = cursor.read_u32()?;
1813 }
1814
1815 let script_file = cursor.read_safe_string()?;
1817
1818 let num_receivers = cursor.read_u32()?;
1820 let mut receivers = Vec::with_capacity(num_receivers as usize);
1821 for _ in 0..num_receivers {
1822 if let Some(uoid) = read_key_uoid(&mut cursor)? {
1823 receivers.push(uoid);
1824 }
1825 }
1826
1827 let num_params = cursor.read_u32()?;
1829 let mut parameters = Vec::with_capacity(num_params as usize);
1830 for _ in 0..num_params {
1831 let id = cursor.read_i32()?;
1832 let value_type = cursor.read_i32()?;
1833
1834 let value = match value_type {
1836 1 => { let v = cursor.read_i32()?;
1838 PythonParamValue::Int(v)
1839 }
1840 2 => { let v = cursor.read_f32()?;
1842 PythonParamValue::Float(v)
1843 }
1844 3 => { let v = cursor.read_u32()?;
1846 PythonParamValue::Bool(v != 0)
1847 }
1848 4 | 13 => { let count = cursor.read_u32()? as usize;
1850 if count > 0 {
1851 let mut buf = vec![0u8; count - 1];
1852 cursor.read_exact(&mut buf)?;
1853 let _null = cursor.read_u8()?; PythonParamValue::String(String::from_utf8_lossy(&buf).into_owned())
1855 } else {
1856 PythonParamValue::String(String::new())
1857 }
1858 }
1859 5..=12 | 14..=23 => { let uoid = read_key_uoid(&mut cursor)?;
1861 PythonParamValue::Key(uoid)
1862 }
1863 24 => { PythonParamValue::None
1865 }
1866 _ => {
1867 log::warn!("Unknown plPythonParameter type {}", value_type);
1868 PythonParamValue::None
1869 }
1870 };
1871
1872 parameters.push(PythonParam { id, value_type, value });
1873 }
1874
1875 Ok(PythonFileModData {
1876 script_file,
1877 receivers,
1878 parameters,
1879 self_key,
1880 })
1881}
1882
1883#[derive(Debug, Clone)]
1890pub struct InterfaceInfoModData {
1891 pub self_key: Option<crate::core::uoid::Uoid>,
1892 pub logic_keys: Vec<crate::core::uoid::Uoid>,
1894}
1895
1896pub fn parse_interface_info_modifier(data: &[u8]) -> Result<InterfaceInfoModData> {
1899 use crate::core::uoid::read_key_uoid;
1900 let mut cursor = Cursor::new(data);
1901
1902 let _class_idx = cursor.read_i16()?;
1903 let self_key = read_key_uoid(&mut cursor)?;
1904 skip_synched_object(&mut cursor)?;
1905 let n_words = cursor.read_u32()?;
1907 for _ in 0..n_words { let _ = cursor.read_u32()?; }
1908
1909 let key_count = cursor.read_u32()?;
1910 let mut logic_keys = Vec::with_capacity(key_count as usize);
1911 for _ in 0..key_count {
1912 if let Some(uoid) = read_key_uoid(&mut cursor)? {
1913 logic_keys.push(uoid);
1914 }
1915 }
1916
1917 Ok(InterfaceInfoModData { self_key, logic_keys })
1918}
1919
1920#[derive(Debug, Clone)]
1927pub struct OneShotModData {
1928 pub self_key: Option<crate::core::uoid::Uoid>,
1929 pub anim_name: String,
1931 pub seek_duration: f32,
1933 pub drivable: bool,
1935 pub reversable: bool,
1937 pub smart_seek: bool,
1939 pub no_seek: bool,
1941}
1942
1943pub fn parse_one_shot_mod(data: &[u8]) -> Result<OneShotModData> {
1948 use crate::core::uoid::read_key_uoid;
1949 let mut cursor = Cursor::new(data);
1950
1951 let _class_idx = cursor.read_i16()?;
1953
1954 let self_key = read_key_uoid(&mut cursor)?;
1956
1957 skip_synched_object(&mut cursor)?;
1959
1960 let num_bit_vectors = cursor.read_u32()?;
1962 for _ in 0..num_bit_vectors {
1963 let _word = cursor.read_u32()?;
1964 }
1965
1966 let anim_name = cursor.read_safe_string()?;
1968 let seek_duration = cursor.read_f32()?;
1969 let drivable = cursor.read_u8()? != 0; let reversable = cursor.read_u8()? != 0;
1971 let smart_seek = cursor.read_u8()? != 0;
1972 let no_seek = cursor.read_u8()? != 0;
1973
1974 Ok(OneShotModData {
1975 self_key,
1976 anim_name,
1977 seek_duration,
1978 drivable,
1979 reversable,
1980 smart_seek,
1981 no_seek,
1982 })
1983}
1984
1985#[derive(Debug, Clone)]
1992pub struct LogicModData {
1993 pub self_key: Option<crate::core::uoid::Uoid>,
1994 pub notify_receivers: Vec<crate::core::uoid::Uoid>,
1996 pub notify_id: i32,
1999 pub cursor_type: i32,
2001 pub condition_keys: Vec<crate::core::uoid::Uoid>,
2004 pub flags: u32,
2007 pub disabled: bool,
2010}
2011
2012pub fn parse_logic_modifier(data: &[u8]) -> Result<LogicModData> {
2019 use crate::core::uoid::read_key_uoid;
2020 let mut cursor = Cursor::new(data);
2021
2022 let _class_idx = cursor.read_i16()?;
2023 let self_key = read_key_uoid(&mut cursor)?;
2024 skip_synched_object(&mut cursor)?;
2025 let n_words = cursor.read_u32()?;
2027 for _ in 0..n_words { let _ = cursor.read_u32()?; }
2028
2029 let cmd_count = cursor.read_u32()?;
2032 for _ in 0..cmd_count {
2033 if cmd_count > 0 {
2037 return Ok(LogicModData {
2039 self_key,
2040 notify_receivers: Vec::new(),
2041 notify_id: 0,
2042 cursor_type: 0,
2043 condition_keys: Vec::new(),
2044 flags: 0,
2045 disabled: false,
2046 });
2047 }
2048 }
2049
2050 let notify_class = cursor.read_i16()?;
2053 let mut notify_receivers = Vec::new();
2054 let mut notify_id = 0i32;
2055 if notify_class >= 0 {
2056 let _sender = read_key_uoid(&mut cursor)?;
2060 let num_receivers = cursor.read_u32()?;
2061 for _ in 0..num_receivers {
2062 if let Some(uoid) = read_key_uoid(&mut cursor)? {
2063 notify_receivers.push(uoid);
2064 }
2065 }
2066 let _timestamp = cursor.read_f32()?; let _ = cursor.read_f32()?; let _bcast_flags = cursor.read_u32()?;
2068
2069 let _notify_type = cursor.read_i32()?;
2072 let _notify_state = cursor.read_f32()?;
2073 notify_id = cursor.read_i32()?;
2074
2075 let num_events = cursor.read_u32()?;
2077 for _ in 0..num_events {
2078 let event_type = cursor.read_i32()?;
2079 match event_type {
2080 1 => cursor.skip(1 + 1)?, 7 => cursor.skip(1 + 1)?, 8 => cursor.skip(4)?, 9 => cursor.skip(4)?, _ => {} }
2086 }
2087 }
2088
2089 let n_words2 = cursor.read_u32()?;
2091 let flags = if n_words2 > 0 { cursor.read_u32()? } else { 0 };
2092 for _ in 1..n_words2 { let _ = cursor.read_u32()?; }
2093
2094 let disabled = cursor.read_u8()? != 0;
2096
2097 let cond_count = cursor.read_u32()?;
2100 let mut condition_keys = Vec::with_capacity(cond_count as usize);
2101 for _ in 0..cond_count {
2102 if let Some(key) = read_key_uoid(&mut cursor)? {
2103 condition_keys.push(key);
2104 }
2105 }
2106
2107 let cursor_type = cursor.read_i32()?;
2109
2110 Ok(LogicModData {
2111 self_key,
2112 notify_receivers,
2113 notify_id,
2114 cursor_type,
2115 condition_keys,
2116 flags,
2117 disabled,
2118 })
2119}
2120
2121#[derive(Debug, Clone)]
2128pub struct OneShotCallback {
2129 pub marker: String,
2131 pub receiver: Option<crate::core::uoid::Uoid>,
2133 pub user: i16,
2135}
2136
2137#[derive(Debug, Clone)]
2139pub struct ResponderCmd {
2140 pub class_id: u16,
2142 pub wait_on: i8,
2144 pub anim_name: Option<String>,
2146 pub anim_flags: u32,
2148 pub notify_receivers: Vec<crate::core::uoid::Uoid>,
2150 pub msg_receivers: Vec<crate::core::uoid::Uoid>,
2152 pub sound_index: i32,
2154 pub sound_flags: u32,
2156 pub oneshot_callbacks: Vec<OneShotCallback>,
2159 pub timer_id: i32,
2162 pub timer_delay: f32,
2165 pub enable_cmd: u32,
2168}
2169
2170#[derive(Debug, Clone)]
2172pub struct ResponderState {
2173 pub num_callbacks: u8,
2174 pub switch_to_state: u8,
2175 pub commands: Vec<ResponderCmd>,
2176}
2177
2178#[derive(Debug, Clone)]
2181pub struct ResponderModData {
2182 pub self_key: Option<crate::core::uoid::Uoid>,
2183 pub states: Vec<ResponderState>,
2184 pub cur_state: u8,
2185 pub enabled: bool,
2186 pub flags: u8,
2187 pub cur_command: i32,
2190 pub completed_events: u64,
2193 pub triggerer: Option<crate::core::uoid::Uoid>,
2196}
2197
2198fn read_msg_base(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, Vec<crate::core::uoid::Uoid>)> {
2201 use crate::core::uoid::read_key_uoid;
2202 let sender = read_key_uoid(cursor)?;
2203 let num_receivers = cursor.read_u32()?;
2204 let mut receivers = Vec::with_capacity(num_receivers as usize);
2205 for _ in 0..num_receivers {
2206 if let Some(u) = read_key_uoid(cursor)? {
2207 receivers.push(u);
2208 }
2209 }
2210 cursor.skip(12)?;
2212 Ok((sender, receivers))
2213}
2214
2215fn read_msg_with_callbacks(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, Vec<crate::core::uoid::Uoid>)> {
2219 let (sender, receivers) = read_msg_base(cursor)?;
2220 let num_callbacks = cursor.read_u32()?;
2221 for _ in 0..num_callbacks {
2223 let cb_class = cursor.read_i16()?;
2224 if cb_class >= 0 {
2225 let _ = read_msg_base(cursor)?;
2227 cursor.skip(12)?; }
2229 }
2230 Ok((sender, receivers))
2231}
2232
2233fn parse_responder_cmd(cursor: &mut Cursor<&[u8]>) -> Result<Option<ResponderCmd>> {
2236 let class_id = cursor.read_u16()?;
2237
2238 let mut cmd = ResponderCmd {
2239 class_id,
2240 wait_on: -1,
2241 anim_name: None,
2242 anim_flags: 0,
2243 notify_receivers: Vec::new(),
2244 msg_receivers: Vec::new(),
2245 sound_index: -1,
2246 sound_flags: 0,
2247 oneshot_callbacks: Vec::new(),
2248 timer_id: -1,
2249 timer_delay: 0.0,
2250 enable_cmd: 0,
2251 };
2252
2253 match class_id {
2254 0x0206 => { let (_, receivers) = read_msg_with_callbacks(cursor)?;
2256 cmd.msg_receivers = receivers;
2257 let n = cursor.read_u32()?;
2259 cmd.anim_flags = if n > 0 { cursor.read_u32()? } else { 0 };
2260 for _ in 1..n { let _ = cursor.read_u32()?; }
2261 cursor.skip(28)?;
2263 cmd.anim_name = Some(cursor.read_safe_string()?);
2265 let _loop_name = cursor.read_safe_string()?;
2266 }
2267 0x025A => { let (_, receivers) = read_msg_with_callbacks(cursor)?;
2269 cmd.msg_receivers = receivers;
2270 let n = cursor.read_u32()?;
2272 cmd.sound_flags = if n > 0 { cursor.read_u32()? } else { 0 };
2273 for _ in 1..n { let _ = cursor.read_u32()?; }
2274 cursor.skip(8 + 8 + 1 + 1 + 4 + 8)?;
2276 cmd.sound_index = cursor.read_i32()?;
2278 cursor.skip(4 + 4 + 4 + 1)?;
2279 }
2280 0x02ED => { let (_, receivers) = read_msg_base(cursor)?;
2282 cmd.msg_receivers = receivers.clone();
2283 cmd.notify_receivers = receivers;
2284 cursor.skip(12)?;
2286 let num_events = cursor.read_u32()?;
2288 for _ in 0..num_events {
2289 let event_type = cursor.read_i32()?;
2290 match event_type {
2292 1 => cursor.skip(1 + 1)?, 2 => cursor.skip(1 + 12)?, 3 => cursor.skip(4 + 1)?, 7 => cursor.skip(1 + 1)?, 8 => cursor.skip(4)?, 9 => cursor.skip(4)?, _ => {
2299 return Ok(Some(cmd));
2301 }
2302 }
2303 }
2304 }
2305 0x0254 => { let (_, receivers) = read_msg_base(cursor)?;
2307 cmd.msg_receivers = receivers;
2308 let n1 = cursor.read_u32()?;
2310 cmd.enable_cmd = if n1 > 0 { cursor.read_u32()? } else { 0 };
2311 for _ in 1..n1 { let _ = cursor.read_u32()?; }
2312 let n2 = cursor.read_u32()?;
2314 for _ in 0..n2 { let _ = cursor.read_u32()?; }
2315 }
2316 0x024F => { let (_, receivers) = read_msg_base(cursor)?;
2318 cmd.msg_receivers = receivers;
2319 cmd.timer_id = cursor.read_i32()?;
2320 cmd.timer_delay = cursor.read_f32()?;
2321 }
2322 0x020A => { let (_, receivers) = read_msg_base(cursor)?;
2324 cmd.msg_receivers = receivers;
2325 let n = cursor.read_u32()?;
2327 for _ in 0..n { let _ = cursor.read_u32()?; }
2328 cursor.skip(8 + 1)?;
2330 let _ = crate::core::uoid::read_key_uoid(cursor)?;
2332 let _ = crate::core::uoid::read_key_uoid(cursor)?;
2333 cursor.skip(44 + 1)?;
2335 }
2336 0x0302 => { let (_, receivers) = read_msg_base(cursor)?;
2338 cmd.msg_receivers = receivers;
2339 cursor.skip(1)?; }
2341 0x0306 => { let (_, receivers) = read_msg_base(cursor)?;
2343 cmd.msg_receivers = receivers;
2344 }
2346 0x0332 => { let (_, receivers) = read_msg_base(cursor)?;
2348 cmd.msg_receivers = receivers;
2349 let n = cursor.read_u32()?;
2351 for _ in 0..n { let _ = cursor.read_u32()?; }
2352 cursor.skip(16)?;
2354 cmd.anim_name = Some(cursor.read_safe_string()?);
2356 }
2357 0x0335 => { let (_, receivers) = read_msg_base(cursor)?;
2359 cmd.msg_receivers = receivers;
2360 cursor.skip(1 + 4)?;
2362 }
2363 0x0307 => { let (_, receivers) = read_msg_base(cursor)?;
2365 cmd.msg_receivers = receivers;
2366 let n = cursor.read_u32()?;
2369 for _ in 0..n {
2370 let marker = cursor.read_safe_string()?;
2371 let receiver = crate::core::uoid::read_key_uoid(cursor)?;
2372 let user = cursor.read_i16()?;
2373 cmd.oneshot_callbacks.push(OneShotCallback { marker, receiver, user });
2374 }
2375 }
2376 0x03BF => { let (_, receivers) = read_msg_base(cursor)?;
2378 cmd.msg_receivers = receivers;
2379 let _ = crate::core::uoid::read_key_uoid(cursor)?; }
2381 0x0453 => { let (_, receivers) = read_msg_base(cursor)?;
2383 cmd.msg_receivers = receivers;
2384 cursor.skip(1)?; }
2386 0x0393 => { let (_, receivers) = read_msg_base(cursor)?;
2389 cmd.msg_receivers = receivers;
2390 cursor.skip(1 + 1)?; }
2392 _ => {
2393 log::debug!("Unknown responder command class 0x{:04X}", class_id);
2395 return Ok(None);
2396 }
2397 }
2398
2399 Ok(Some(cmd))
2400}
2401
2402pub fn parse_responder_modifier(data: &[u8]) -> Result<ResponderModData> {
2405 use crate::core::uoid::read_key_uoid;
2406 let mut cursor = Cursor::new(data);
2407
2408 let _class_idx = cursor.read_i16()?;
2410 let self_key = read_key_uoid(&mut cursor)?;
2411 skip_synched_object(&mut cursor)?;
2412 let n_flags = cursor.read_u32()?;
2413 for _ in 0..n_flags { let _ = cursor.read_u32()?; }
2414
2415 let num_states = cursor.read_u8()?;
2416 let mut states = Vec::with_capacity(num_states as usize);
2417 let mut parse_ok = true;
2418
2419 for _si in 0..num_states {
2420 if !parse_ok { break; }
2421
2422 let num_callbacks = cursor.read_u8()?;
2423 let switch_to_state = cursor.read_u8()?;
2424 let num_cmds = cursor.read_u8()?;
2425
2426 let mut commands = Vec::with_capacity(num_cmds as usize);
2427 for _ in 0..num_cmds {
2428 if !parse_ok { break; }
2429 match parse_responder_cmd(&mut cursor) {
2430 Ok(Some(mut cmd)) => {
2431 let wait_on = cursor.read_u8()? as i8;
2432 cmd.wait_on = wait_on;
2433 commands.push(cmd);
2434 }
2435 Ok(None) => {
2436 parse_ok = false;
2438 break;
2439 }
2440 Err(e) => {
2441 log::debug!("Responder cmd parse error: {}", e);
2442 parse_ok = false;
2443 break;
2444 }
2445 }
2446 }
2447
2448 if parse_ok {
2449 let map_size = cursor.read_u8()?;
2451 for _ in 0..map_size {
2452 let _wait = cursor.read_u8()?;
2453 let _cmd = cursor.read_u8()?;
2454 }
2455 }
2456
2457 states.push(ResponderState {
2458 num_callbacks,
2459 switch_to_state,
2460 commands,
2461 });
2462 }
2463
2464 let (cur_state, enabled, flags) = if parse_ok {
2466 let cs = cursor.read_u8()?;
2467 let en = cursor.read_u8()? != 0; let fl = cursor.read_u8()?;
2469 (cs, en, fl)
2470 } else {
2471 (0, true, 0x01) };
2473
2474 Ok(ResponderModData {
2475 self_key,
2476 states,
2477 cur_state,
2478 enabled,
2479 flags,
2480 cur_command: -1,
2481 completed_events: 0,
2482 triggerer: None,
2483 })
2484}
2485
2486#[derive(Debug, Clone)]
2493pub struct VolumeDetectorData {
2494 pub name: String,
2495 pub collision_type: u8,
2497 pub receivers: Vec<crate::core::uoid::Uoid>,
2499 pub proxy_key: Option<crate::core::uoid::Uoid>,
2501}
2502
2503pub fn parse_volume_detector(data: &[u8]) -> Result<VolumeDetectorData> {
2510 use crate::core::uoid::read_key_uoid;
2511 let mut cursor = Cursor::new(data);
2512
2513 let _class_idx = cursor.read_i16()?;
2515
2516 let self_key = read_key_uoid(&mut cursor)?;
2518 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
2519
2520 skip_synched_object(&mut cursor)?;
2522
2523 let num_bit_vectors = cursor.read_u32()?;
2525 for _ in 0..num_bit_vectors {
2526 let _word = cursor.read_u32()?;
2527 }
2528
2529 let receiver_count = cursor.read_u32()?;
2532 let mut receivers = Vec::with_capacity(receiver_count as usize);
2533 for _ in 0..receiver_count {
2534 if let Some(uoid) = read_key_uoid(&mut cursor)? {
2535 receivers.push(uoid);
2536 }
2537 }
2538 let _remote_mod = read_key_uoid(&mut cursor)?;
2540 let proxy_key = read_key_uoid(&mut cursor)?;
2542
2543 let collision_type = cursor.read_u8()?;
2545
2546 Ok(VolumeDetectorData {
2547 name,
2548 collision_type,
2549 receivers,
2550 proxy_key,
2551 })
2552}
2553
2554fn read_hs_matrix44(reader: &mut (impl std::io::Read + Seek)) -> Result<[f32; 16]> {
2562 let has_data = reader.read_u8()? != 0;
2563 if has_data {
2564 let mut m = [0f32; 16];
2565 for val in &mut m {
2566 *val = reader.read_f32()?;
2567 }
2568 Ok(m)
2569 } else {
2570 Ok([
2572 1.0, 0.0, 0.0, 0.0,
2573 0.0, 1.0, 0.0, 0.0,
2574 0.0, 0.0, 1.0, 0.0,
2575 0.0, 0.0, 0.0, 1.0,
2576 ])
2577 }
2578}
2579
2580#[derive(Debug, Clone)]
2582pub struct PostEffectModData {
2583 pub name: String,
2584 pub hither: f32,
2585 pub yon: f32,
2586 pub fov_x: f32,
2587 pub fov_y: f32,
2588 pub w2c: [f32; 16],
2590 pub c2w: [f32; 16],
2592}
2593
2594pub fn parse_post_effect_mod(data: &[u8]) -> Result<PostEffectModData> {
2598 use crate::core::uoid::read_key_uoid;
2599 let mut cursor = Cursor::new(data);
2600
2601 let _class_idx = cursor.read_i16()?;
2602 let self_key = read_key_uoid(&mut cursor)?;
2603 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
2604 skip_synched_object(&mut cursor)?;
2605
2606 let num_bv = cursor.read_u32()?;
2608 for _ in 0..num_bv { let _w = cursor.read_u32()?; }
2609
2610 let num_sv = cursor.read_u32()?;
2612 let mut state_bits = 0u32;
2613 for i in 0..num_sv {
2614 let w = cursor.read_u32()?;
2615 if i == 0 { state_bits = w; }
2616 }
2617 log::debug!(" PostEffectMod flags_bv={} state_bv={} state_bits=0x{:X} pos={}",
2618 num_bv, num_sv, state_bits, cursor.position());
2619
2620 let hither = cursor.read_f32()?;
2622 let yon = cursor.read_f32()?;
2623 let fov_x = cursor.read_f32()?;
2624 let fov_y = cursor.read_f32()?;
2625 log::debug!(" hither={} yon={} fov_x={} fov_y={}", hither, yon, fov_x, fov_y);
2626
2627 let _node_key = read_key_uoid(&mut cursor)?;
2629
2630 let w2c = read_hs_matrix44(&mut cursor)?;
2632 let c2w = read_hs_matrix44(&mut cursor)?;
2633
2634 log::debug!(" W2C row0=({:.3},{:.3},{:.3},{:.3})", w2c[0], w2c[1], w2c[2], w2c[3]);
2635 log::debug!(" W2C row1=({:.3},{:.3},{:.3},{:.3})", w2c[4], w2c[5], w2c[6], w2c[7]);
2636 log::debug!(" W2C row2=({:.3},{:.3},{:.3},{:.3})", w2c[8], w2c[9], w2c[10], w2c[11]);
2637 log::debug!(" W2C row3=({:.3},{:.3},{:.3},{:.3})", w2c[12], w2c[13], w2c[14], w2c[15]);
2638
2639 Ok(PostEffectModData {
2640 name, hither, yon, fov_x, fov_y, w2c, c2w,
2641 })
2642}
2643
2644#[derive(Debug, Clone)]
2651pub struct GuiControlData {
2652 pub name: String,
2653 pub control_type: String,
2654 pub tag_id: u32,
2655 pub visible: bool,
2656 pub text: Option<String>,
2657}
2658
2659fn skip_gui_proc(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
2662 let proc_type = reader.read_u32()?; match proc_type {
2664 3 => {} 0 => { let len = reader.read_u32()? as usize;
2667 if len > 0 { reader.skip(len)?; }
2668 }
2669 1 => {} 2 => {} _ => {} }
2673 Ok(())
2674}
2675
2676fn parse_gui_control_base(cursor: &mut Cursor<&[u8]>) -> Result<(u32, bool)> {
2680 use crate::core::uoid::read_key_uoid;
2681
2682 let num_bv = cursor.read_u32()?;
2684 let mut flags = 0u32;
2685 for i in 0..num_bv {
2686 let w = cursor.read_u32()?;
2687 if i == 0 { flags = w; }
2688 }
2689
2690 let tag_id = cursor.read_u32()?;
2692 let visible = cursor.read_u8()? != 0;
2693
2694 skip_gui_proc(cursor)?;
2696
2697 let has_dyn_text = cursor.read_u8()? != 0;
2699 if has_dyn_text {
2700 let _layer_key = read_key_uoid(cursor)?;
2701 let _dyn_text_key = read_key_uoid(cursor)?;
2702 }
2703
2704 let has_color = cursor.read_u8()? != 0;
2707 if has_color {
2708 cursor.skip(4 * 4 * 4)?; cursor.read_u32()?; cursor.read_safe_string()?; cursor.read_u8()?; cursor.read_u8()?; }
2714
2715 let sound_count = cursor.read_u8()? as usize;
2717 cursor.skip(sound_count * 4)?; if flags & (1 << 21) != 0 {
2721 let _proxy_key = read_key_uoid(cursor)?;
2722 }
2723
2724 let _skin_key = read_key_uoid(cursor)?;
2726
2727 Ok((tag_id, visible))
2728}
2729
2730pub fn parse_gui_button(data: &[u8]) -> Result<GuiControlData> {
2734 use crate::core::uoid::read_key_uoid;
2735 let mut cursor = Cursor::new(data);
2736
2737 let _class_idx = cursor.read_i16()?;
2738 let self_key = read_key_uoid(&mut cursor)?;
2739 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
2740 skip_synched_object(&mut cursor)?;
2741
2742 match parse_gui_control_base(&mut cursor) {
2744 Ok((tag_id, visible)) => Ok(GuiControlData {
2745 name,
2746 control_type: "Button".to_string(),
2747 tag_id,
2748 visible,
2749 text: None,
2750 }),
2751 Err(_) => Ok(GuiControlData {
2752 name,
2753 control_type: "Button".to_string(),
2754 tag_id: 0,
2755 visible: true,
2756 text: None,
2757 }),
2758 }
2759}
2760
2761pub fn parse_gui_textbox(data: &[u8]) -> Result<GuiControlData> {
2765 use crate::core::uoid::read_key_uoid;
2766 let mut cursor = Cursor::new(data);
2767
2768 let _class_idx = cursor.read_i16()?;
2769 let self_key = read_key_uoid(&mut cursor)?;
2770 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
2771 skip_synched_object(&mut cursor)?;
2772
2773 let (tag_id, visible) = match parse_gui_control_base(&mut cursor) {
2774 Ok(v) => v,
2775 Err(_) => return Ok(GuiControlData {
2776 name, control_type: "TextBox".to_string(), tag_id: 0, visible: true, text: None,
2777 }),
2778 };
2779
2780 let text = if let Ok(text_len) = cursor.read_u32() {
2782 let text_len = text_len as usize;
2783 if text_len > 0 && text_len < 65536 {
2784 let mut text_bytes = vec![0u8; text_len];
2785 if cursor.read_exact(&mut text_bytes).is_ok() {
2786 let s = String::from_utf8_lossy(&text_bytes).trim_end_matches('\0').to_string();
2787 if s.is_empty() { None } else { Some(s) }
2788 } else { None }
2789 } else { None }
2790 } else { None };
2791
2792 Ok(GuiControlData {
2793 name,
2794 control_type: "TextBox".to_string(),
2795 tag_id,
2796 visible,
2797 text,
2798 })
2799}
2800
2801#[derive(Debug, Clone)]
2808pub struct ParticleSystemData {
2809 pub name: String,
2810 pub material_name: Option<String>,
2812 pub x_tiles: u32,
2814 pub y_tiles: u32,
2815 pub max_particles: u32,
2817 pub accel: [f32; 3],
2819 pub pre_sim: f32,
2821 pub drag: f32,
2823 pub wind_mult: f32,
2825 pub emitter_sources: Vec<[f32; 3]>,
2827 pub particle_size: [f32; 2],
2829 pub num_emitters: u32,
2831 pub particle_life_range: [f32; 2],
2833 pub particles_per_second: f32,
2835 pub velocity_range: [f32; 2],
2837 pub angle_range: f32,
2839 pub scale_range: [f32; 2],
2841 pub emitter_misc_flags: u32,
2846 pub emitter_color: [f32; 4],
2848}
2849
2850fn skip_creatable_controller(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
2854 let class_idx = reader.read_u16()?;
2855 if class_idx & 0x8000 != 0 {
2856 return Ok(()); }
2858
2859 if class_idx == 0x01A9 {
2861 let key_type = reader.read_u8()?;
2863 let num_keys = reader.read_u32()?;
2864 let key_size: usize = match key_type {
2865 0 => 0, 1 => 14, 2 => 38, 3 => 6, 4 => 14, 5 => 30, 6 => 54, 7 => 18, 8 => 6, 9 => 10, 10 => 42, 11 => 38, 12 => 66, _ => bail!("Unknown keyframe type {}", key_type),
2879 };
2880 reader.seek(SeekFrom::Current((num_keys as usize * key_size) as i64))?;
2881 } else if class_idx == 0x01AA {
2882 for _ in 0..3 {
2884 skip_creatable_controller(reader)?;
2885 }
2886 } else {
2887 bail!("Unsupported controller class 0x{:04X}", class_idx);
2888 }
2889 Ok(())
2890}
2891
2892pub fn parse_particle_system(data: &[u8]) -> Result<ParticleSystemData> {
2898 use crate::core::uoid::read_key_uoid;
2899 let mut cursor = Cursor::new(data);
2900
2901 let _class_idx = cursor.read_i16()?;
2903
2904 let self_key = read_key_uoid(&mut cursor)?;
2906 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
2907
2908 skip_synched_object(&mut cursor)?;
2910
2911 let mat_key = read_key_uoid(&mut cursor)?;
2916 let material_name = mat_key.map(|k| k.object_name);
2917
2918 for _i in 0..5 {
2922 skip_creatable_controller(&mut cursor)?;
2923 }
2924
2925 let x_tiles = cursor.read_u32()?;
2927 let y_tiles = cursor.read_u32()?;
2928 let max_particles = cursor.read_u32()?;
2929 let _max_emitters = cursor.read_u32()?;
2930 let pre_sim = cursor.read_f32()?;
2931 let accel_x = cursor.read_f32()?;
2932 let accel_y = cursor.read_f32()?;
2933 let accel_z = cursor.read_f32()?;
2934 let drag = cursor.read_f32()?;
2935 let wind_mult = cursor.read_f32()?;
2936
2937 let num_emitters = cursor.read_u32()?;
2939 let mut emitter_sources = Vec::new();
2940 let mut particle_size = [1.0_f32, 1.0];
2941 let mut particle_life_range = [1.0_f32, 1.0];
2942 let mut particles_per_second = 0.0_f32;
2943 let mut velocity_range = [0.0_f32, 0.0];
2944 let mut angle_range = 0.0_f32;
2945 let mut scale_range = [1.0_f32, 1.0];
2946 let mut emitter_misc_flags = 0u32;
2947 let mut emitter_color = [1.0_f32, 1.0, 1.0, 1.0];
2948
2949 for _ei in 0..num_emitters {
2950 let emitter_class = cursor.read_u16()?;
2952 if emitter_class & 0x8000 != 0 { continue; }
2953
2954 let gen_class = cursor.read_u16()?;
2956 if gen_class & 0x8000 == 0 {
2957 log::debug!(" Emitter generator class: 0x{:04X}", gen_class);
2958 if gen_class == 0x02D8 {
2959 let _gen_life = cursor.read_f32()?;
2961 let part_life_min = cursor.read_f32()?;
2962 let part_life_max = cursor.read_f32()?;
2963 particle_life_range = [part_life_min, part_life_max];
2964 let pps = cursor.read_f32()?;
2965 particles_per_second = pps;
2966 let num_sources = cursor.read_u32()?;
2967 for _ in 0..num_sources {
2968 let px = cursor.read_f32()?;
2969 let py = cursor.read_f32()?;
2970 let pz = cursor.read_f32()?;
2971 emitter_sources.push([px, py, pz]);
2972 let _pitch = cursor.read_f32()?;
2973 let _yaw = cursor.read_f32()?;
2974 }
2975 let ang = cursor.read_f32()?;
2976 angle_range = ang;
2977 let vel_min = cursor.read_f32()?;
2978 let vel_max = cursor.read_f32()?;
2979 velocity_range = [vel_min, vel_max];
2980 let x_size = cursor.read_f32()?;
2981 let y_size = cursor.read_f32()?;
2982 particle_size = [x_size, y_size];
2983 let sc_min = cursor.read_f32()?;
2984 let sc_max = cursor.read_f32()?;
2985 scale_range = [sc_min, sc_max];
2986 cursor.skip(4 * 2)?; } else if gen_class == 0x0336 {
2988 let count = cursor.read_u32()?;
2990 let x_size = cursor.read_f32()?;
2991 let y_size = cursor.read_f32()?;
2992 particle_size = [x_size, y_size];
2993 let sc_min = cursor.read_f32()?;
2994 let sc_max = cursor.read_f32()?;
2995 scale_range = [sc_min, sc_max];
2996 cursor.skip(4)?; for _ in 0..count {
2998 let px = cursor.read_f32()?;
2999 let py = cursor.read_f32()?;
3000 let pz = cursor.read_f32()?;
3001 emitter_sources.push([px, py, pz]);
3002 }
3003 cursor.skip((count as usize) * 12)?; } else {
3005 break;
3007 }
3008 }
3009 let _span_idx = cursor.read_u32()?;
3012 let _max_parts = cursor.read_u32()?;
3013 let misc_flags = cursor.read_u32()?;
3014 emitter_misc_flags = misc_flags;
3015 let cr = cursor.read_f32()?;
3016 let cg = cursor.read_f32()?;
3017 let cb = cursor.read_f32()?;
3018 let ca = cursor.read_f32()?;
3019 emitter_color = [cr, cg, cb, ca];
3020 }
3021
3022 Ok(ParticleSystemData {
3023 name,
3024 material_name,
3025 x_tiles, y_tiles,
3026 max_particles,
3027 accel: [accel_x, accel_y, accel_z],
3028 pre_sim,
3029 drag,
3030 wind_mult,
3031 emitter_sources,
3032 particle_size,
3033 num_emitters,
3034 particle_life_range,
3035 particles_per_second,
3036 velocity_range,
3037 angle_range,
3038 scale_range,
3039 emitter_misc_flags,
3040 emitter_color,
3041 })
3042}
3043
3044#[derive(Debug, Clone)]
3052pub struct WaveSetData {
3053 pub name: String,
3054 pub water_height: f32,
3055 pub water_tint: [f32; 4], pub opacity: f32,
3057 pub max_length: f32,
3058 pub geo_max_len: f32,
3060 pub geo_min_len: f32,
3061 pub geo_amp_over_len: f32,
3062 pub geo_chop: f32,
3063 pub geo_angle_dev: f32,
3064 pub wind_dir: [f32; 3],
3066}
3067
3068pub fn parse_wave_set(data: &[u8]) -> Result<WaveSetData> {
3073 use crate::core::uoid::read_key_uoid;
3074 let mut cursor = Cursor::new(data);
3075
3076 let _class_idx = cursor.read_i16()?;
3077 let self_key = read_key_uoid(&mut cursor)?;
3078 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
3079
3080 skip_synched_object(&mut cursor)?;
3082
3083 let num_bv = cursor.read_u32()?;
3085 let mut flags = 0u32;
3086 for i in 0..num_bv {
3087 let w = cursor.read_u32()?;
3088 if i == 0 { flags = w; }
3089 }
3090
3091 let max_length = cursor.read_f32()?;
3093
3094 let geo_max_len = cursor.read_f32()?;
3099 let geo_min_len = cursor.read_f32()?;
3100 let geo_amp_over_len = cursor.read_f32()?;
3101 let geo_chop = cursor.read_f32()?;
3102 let geo_angle_dev = cursor.read_f32()?;
3103
3104 cursor.skip(5 * 4)?;
3106
3107 cursor.skip(4)?;
3109
3110 let wind_x = cursor.read_f32()?;
3112 let wind_y = cursor.read_f32()?;
3113 let wind_z = cursor.read_f32()?;
3114
3115 cursor.skip(3 * 4)?;
3117
3118 let water_height = cursor.read_f32()?;
3120
3121 cursor.skip(3 * 4)?;
3123
3124 cursor.skip(3 * 4)?;
3126
3127 cursor.skip(3 * 4)?;
3129
3130 cursor.skip(3 * 4)?;
3132
3133 cursor.skip(4)?;
3135
3136 cursor.skip(4 * 4)?;
3138
3139 cursor.skip(4 * 4)?;
3141
3142 cursor.skip(4 * 4)?;
3144
3145 let opacity = cursor.read_f32()?;
3147
3148 cursor.skip(4)?;
3150
3151 cursor.skip(2 * 4)?;
3153
3154 let r = cursor.read_f32()?;
3156 let g = cursor.read_f32()?;
3157 let b = cursor.read_f32()?;
3158 let a = cursor.read_f32()?;
3159
3160 Ok(WaveSetData {
3161 name,
3162 water_height,
3163 water_tint: [r, g, b, a],
3164 opacity,
3165 max_length,
3166 geo_max_len,
3167 geo_min_len,
3168 geo_amp_over_len,
3169 geo_chop,
3170 geo_angle_dev,
3171 wind_dir: [wind_x, wind_y, wind_z],
3172 })
3173}
3174
3175#[derive(Debug, Clone)]
3182pub struct SoundData {
3183 pub name: String,
3184 pub volume: f32,
3185 pub looping: bool,
3186 pub is_3d: bool,
3187 pub auto_start: bool,
3188 pub sound_type: u8, pub buffer_name: Option<String>,
3191 pub position: [f32; 3],
3193 pub min_falloff: f32,
3195 pub max_falloff: f32,
3197 pub soft_region: Option<String>,
3200}
3201
3202pub fn parse_win32_sound(data: &[u8]) -> Result<SoundData> {
3207 use crate::core::uoid::read_key_uoid;
3208 let mut cursor = Cursor::new(data);
3209
3210 let _class_idx = cursor.read_i16()?;
3212
3213 let self_key = read_key_uoid(&mut cursor)?;
3215 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
3216
3217 skip_synched_object(&mut cursor)?;
3219
3220 let _playing = cursor.read_u8()?; let _time = cursor.read_f32()?; cursor.skip(4)?; let max_falloff = cursor.read_i32()? as f32;
3226 let min_falloff = cursor.read_i32()? as f32;
3227 let _curr_volume = cursor.read_f32()?;
3228 let desired_vol = cursor.read_f32()?;
3229 let _outer_vol = cursor.read_i32()?;
3230 let _inner_cone = cursor.read_i32()?;
3231 let _outer_cone = cursor.read_i32()?;
3232 let _faded_volume = cursor.read_f32()?;
3233 let properties = cursor.read_u32()?;
3234 let sound_type = cursor.read_u8()?;
3235 let _priority = cursor.read_u8()?;
3236
3237 cursor.skip(4 + 4 + 4 + 1 + 4 + 4 + 4)?;
3239 cursor.skip(4 + 4 + 4 + 1 + 4 + 4 + 4)?;
3241
3242 let soft_region_key = read_key_uoid(&mut cursor)?;
3244 let soft_region = soft_region_key.map(|k| k.object_name);
3245 let buffer_key = read_key_uoid(&mut cursor)?;
3246 let buffer_name = buffer_key.map(|k| k.object_name);
3247
3248 let looping = properties & 0x00000004 != 0; let is_3d = properties & 0x00000001 != 0; let auto_start = properties & 0x00000008 != 0; Ok(SoundData {
3253 name,
3254 volume: desired_vol,
3255 looping,
3256 is_3d,
3257 auto_start,
3258 sound_type,
3259 buffer_name,
3260 position: [0.0, 0.0, 0.0], min_falloff,
3262 max_falloff,
3263 soft_region,
3264 })
3265}
3266
3267#[cfg(test)]
3268mod tests {
3269 use super::*;
3270 use std::path::Path;
3271
3272 #[test]
3273 fn test_parse_one_shot_mods() {
3274 let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
3275 if !prp_path.exists() { return; }
3276
3277 let page = PrpPage::from_file(prp_path).unwrap();
3278 let keys: Vec<_> = page.keys_of_type(crate::core::class_index::ClassIndex::PL_ONE_SHOT_MOD)
3279 .iter().cloned().cloned().collect();
3280
3281 assert!(keys.len() >= 10, "Cleft should have 14+ OneShotMod objects, got {}", keys.len());
3282
3283 let mut ok = 0;
3284 for key in &keys {
3285 if let Some(data) = page.object_data(key) {
3286 let osm = parse_one_shot_mod(data)
3287 .unwrap_or_else(|e| panic!("Failed to parse OneShotMod '{}': {}", key.object_name, e));
3288 assert!(!osm.anim_name.is_empty(), "OneShotMod '{}' has empty anim_name", key.object_name);
3289 assert!(osm.seek_duration >= 0.0, "OneShotMod '{}' has negative seek_duration", key.object_name);
3290 ok += 1;
3291 }
3292 }
3293 assert_eq!(ok, keys.len(), "All OneShotMod objects should parse successfully");
3294 }
3295
3296 #[test]
3297 fn test_responder_oneshot_callbacks_parse() {
3298 let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
3300 if !prp_path.exists() { return; }
3301
3302 let page = PrpPage::from_file(prp_path).unwrap();
3303 let mut ok = 0;
3304 let mut err = 0;
3305 for key in page.keys_of_type(crate::core::class_index::ClassIndex::PL_RESPONDER_MODIFIER) {
3306 if let Some(data) = page.object_data(key) {
3307 match parse_responder_modifier(data) {
3308 Ok(resp) => {
3309 let n_cmds: usize = resp.states.iter().map(|s| s.commands.len()).sum();
3311 if n_cmds > 0 || resp.enabled {
3312 ok += 1;
3313 }
3314 }
3315 Err(_) => err += 1,
3316 }
3317 }
3318 }
3319 assert!(ok >= 80, "At least 80 responders should parse Ok, got {} (err={})", ok, err);
3322 }
3323
3324 #[test]
3325 fn test_px_physical_parse() {
3326 let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
3327 if !prp_path.exists() { return; }
3328
3329 let page = PrpPage::from_file(prp_path).unwrap();
3330 let keys: Vec<_> = page.keys_of_type(crate::core::class_index::ClassIndex::PL_PXPHYSICAL)
3331 .iter().cloned().cloned().collect();
3332
3333 assert!(keys.len() >= 50, "Cleft should have 100+ PXPhysical objects, got {}", keys.len());
3334
3335 let mut ok = 0;
3336 let mut fail = 0;
3337 let mut trimesh_count = 0;
3338 let mut hull_count = 0;
3339 let mut total_verts = 0;
3340 for key in &keys {
3341 if let Some(data) = page.object_data(key) {
3342 match parse_px_physical(data) {
3343 Ok(phys) => {
3344 match &phys.shape {
3345 PhysShapeData::TriMesh { vertices, indices } => {
3346 trimesh_count += 1;
3347 total_verts += vertices.len();
3348 assert!(indices.len() % 3 == 0, "trimesh {} indices not multiple of 3", phys.name);
3349 }
3350 PhysShapeData::Hull { vertices } => {
3351 hull_count += 1;
3352 total_verts += vertices.len();
3353 }
3354 _ => {}
3355 }
3356 ok += 1;
3357 }
3358 Err(e) => {
3359 eprintln!("FAIL: {} — {}", key.object_name, e);
3360 fail += 1;
3361 }
3362 }
3363 }
3364 }
3365 eprintln!("PXPhysical: {} ok, {} fail, {} trimesh, {} hull, {} total verts",
3366 ok, fail, trimesh_count, hull_count, total_verts);
3367 assert!(ok >= 100, "At least 100 PXPhysical should parse, got {} (fail={})", ok, fail);
3369 }
3370
3371}
3372
3373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3381pub enum PhysBoundsType {
3382 Box = 1,
3383 Sphere = 2,
3384 Hull = 3,
3385 Proxy = 4,
3386 Explicit = 5,
3387}
3388
3389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3392pub enum PhysGroup {
3393 Static = 0,
3394 AvatarBlocker = 1,
3395 DynamicBlocker = 2,
3396 Avatar = 3,
3397 Dynamic = 4,
3398 Detector = 5,
3399 LOSOnly = 6,
3400 ExcludeRegion = 7,
3401 Max = 255,
3402}
3403
3404#[derive(Debug, Clone)]
3406pub enum PhysShapeData {
3407 Sphere { radius: f32, offset: [f32; 3] },
3408 Box { dimensions: [f32; 3], offset: [f32; 3] },
3409 Hull { vertices: Vec<[f32; 3]> },
3410 TriMesh { vertices: Vec<[f32; 3]>, indices: Vec<u32> },
3411}
3412
3413#[derive(Debug, Clone)]
3415pub struct PxPhysicalData {
3416 pub name: String,
3417 pub mass: f32,
3418 pub friction: f32,
3419 pub restitution: f32,
3420 pub bounds: PhysBoundsType,
3421 pub group: PhysGroup,
3422 pub reports_on: u32,
3423 pub los_dbs: u16,
3424 pub position: [f32; 3],
3425 pub rotation: [f32; 4], pub shape: PhysShapeData,
3427}
3428
3429fn read_phys_group(val: u8) -> PhysGroup {
3430 match val {
3431 0 => PhysGroup::Static,
3432 1 => PhysGroup::AvatarBlocker,
3433 2 => PhysGroup::DynamicBlocker,
3434 3 => PhysGroup::Avatar,
3435 4 => PhysGroup::Dynamic,
3436 5 => PhysGroup::Detector,
3437 6 => PhysGroup::LOSOnly,
3438 7 => PhysGroup::ExcludeRegion,
3439 _ => PhysGroup::Max,
3440 }
3441}
3442
3443fn read_phys_bounds(val: u8) -> Result<PhysBoundsType> {
3444 match val {
3445 1 => Ok(PhysBoundsType::Box),
3446 2 => Ok(PhysBoundsType::Sphere),
3447 3 => Ok(PhysBoundsType::Hull),
3448 4 => Ok(PhysBoundsType::Proxy),
3449 5 => Ok(PhysBoundsType::Explicit),
3450 _ => bail!("Unknown bounds type: {}", val),
3451 }
3452}
3453
3454fn read_point3(reader: &mut impl std::io::Read) -> Result<[f32; 3]> {
3456 let mut buf = [0u8; 12];
3457 reader.read_exact(&mut buf)?;
3458 Ok([
3459 f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
3460 f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
3461 f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
3462 ])
3463}
3464
3465fn read_quat(reader: &mut impl std::io::Read) -> Result<[f32; 4]> {
3468 let mut buf = [0u8; 16];
3469 reader.read_exact(&mut buf)?;
3470 let mut q = [
3471 f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
3472 f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
3473 f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
3474 f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
3475 ];
3476 if q[0] == 0.0 && q[1] == 0.0 && q[2] == 0.0 && q[3] == 0.0 {
3478 q[3] = 1.0;
3479 }
3480 Ok(q)
3481}
3482
3483fn skip_bit_vector(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
3486 let mut buf = [0u8; 4];
3487 reader.read_exact(&mut buf)?;
3488 let count = u32::from_le_bytes(buf) as usize;
3489 if count > 0 {
3490 let mut skip_buf = vec![0u8; count * 4];
3491 reader.read_exact(&mut skip_buf)?;
3492 }
3493 Ok(())
3494}
3495
3496fn read_bit_vector_words(reader: &mut impl std::io::Read) -> Result<Vec<u32>> {
3499 let mut buf = [0u8; 4];
3500 reader.read_exact(&mut buf)?;
3501 let count = u32::from_le_bytes(buf) as usize;
3502 let mut words = Vec::with_capacity(count);
3503 for _ in 0..count {
3504 reader.read_exact(&mut buf)?;
3505 words.push(u32::from_le_bytes(buf));
3506 }
3507 Ok(words)
3508}
3509
3510fn read_uncooked_hull(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
3513 let mut buf4 = [0u8; 4];
3514 reader.read_exact(&mut buf4)?;
3515 let nverts = u32::from_le_bytes(buf4) as usize;
3516 let mut vertices = Vec::with_capacity(nverts);
3517 for _ in 0..nverts {
3518 vertices.push(read_point3(reader)?);
3519 }
3520 Ok(PhysShapeData::Hull { vertices })
3521}
3522
3523fn read_uncooked_trimesh(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
3524 let mut buf4 = [0u8; 4];
3525 reader.read_exact(&mut buf4)?;
3526 let nverts = u32::from_le_bytes(buf4) as usize;
3527 let mut vertices = Vec::with_capacity(nverts);
3528 for _ in 0..nverts {
3529 vertices.push(read_point3(reader)?);
3530 }
3531 reader.read_exact(&mut buf4)?;
3532 let nfaces = u32::from_le_bytes(buf4) as usize;
3533 let mut indices = Vec::with_capacity(nfaces * 3);
3534 for _ in 0..(nfaces * 3) {
3535 reader.read_exact(&mut buf4)?;
3536 indices.push(u32::from_le_bytes(buf4));
3537 }
3538 Ok(PhysShapeData::TriMesh { vertices, indices })
3539}
3540
3541fn skip_max_dependent_list(reader: &mut (impl std::io::Read + Seek), size: usize) -> Result<()> {
3544 let mut buf4 = [0u8; 4];
3545 reader.read_exact(&mut buf4)?;
3546 let max_val = u32::from_le_bytes(buf4);
3547 let bytes_per_elem = if max_val > 0xFFFF { 4 } else if max_val > 0xFF { 2 } else { 1 };
3548 reader.seek(SeekFrom::Current((size * bytes_per_elem) as i64))?;
3549 Ok(())
3550}
3551
3552fn read_cooked_suffix(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
3555 let mut buf4 = [0u8; 4];
3556 reader.read_exact(&mut buf4)?;
3558 let hbm_size = u32::from_le_bytes(buf4) as i64;
3559 reader.seek(SeekFrom::Current(hbm_size))?;
3560 reader.seek(SeekFrom::Current(11 * 4))?;
3562 let mut fbuf = [0u8; 4];
3564 reader.read_exact(&mut fbuf)?;
3565 let val = f32::from_le_bytes(fbuf);
3566 if val > -1.0 {
3567 reader.seek(SeekFrom::Current(12 * 4))?;
3568 }
3569 Ok(())
3570}
3571
3572fn read_cooked_hull(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
3575 let mut tag = [0u8; 4];
3576 let mut buf4 = [0u8; 4];
3577
3578 reader.read_exact(&mut tag)?;
3580 if &tag != b"CVXM" { bail!("Expected CVXM, got {:?}", tag); }
3581 reader.read_exact(&mut buf4)?; reader.read_exact(&mut buf4)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut buf4)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut buf4)?; reader.read_exact(&mut buf4)?;
3595 let num_verts = u32::from_le_bytes(buf4) as usize;
3596 reader.read_exact(&mut buf4)?;
3597 let num_tris = u32::from_le_bytes(buf4) as usize;
3598 reader.read_exact(&mut buf4)?;
3599 let unk2 = u32::from_le_bytes(buf4) as usize;
3600 reader.read_exact(&mut buf4)?;
3601 let _unk3 = u32::from_le_bytes(buf4) as usize;
3602 reader.read_exact(&mut buf4)?;
3603 let unk4 = u32::from_le_bytes(buf4) as usize;
3604 reader.read_exact(&mut buf4)?;
3605 let _unk5 = u32::from_le_bytes(buf4) as usize;
3606
3607 let mut vertices = Vec::with_capacity(num_verts);
3609 for _ in 0..num_verts {
3610 vertices.push(read_point3(reader)?);
3611 }
3612
3613 reader.read_exact(&mut buf4)?;
3615 let max_vert_index = u32::from_le_bytes(buf4);
3616 let idx_size = if max_vert_index > 0xFFFF { 4 } else if max_vert_index > 0xFF { 2 } else { 1 };
3617 reader.seek(SeekFrom::Current((num_tris * 3 * idx_size) as i64))?;
3618
3619 reader.seek(SeekFrom::Current(2))?; reader.seek(SeekFrom::Current((num_verts * 2) as i64))?;
3622 reader.seek(SeekFrom::Current(12))?; reader.seek(SeekFrom::Current((_unk3 * 36) as i64))?;
3624 reader.seek(SeekFrom::Current(unk4 as i64))?;
3625
3626 skip_max_dependent_list(reader, unk4)?;
3627
3628 reader.seek(SeekFrom::Current(8))?; reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
3630 reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
3631
3632 skip_max_dependent_list(reader, unk2)?;
3633 skip_max_dependent_list(reader, unk2)?;
3634 skip_max_dependent_list(reader, unk2)?;
3635 reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
3636
3637 reader.read_exact(&mut tag)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut buf4)?; reader.read_exact(&mut buf4)?;
3643 let vale_unk1 = u32::from_le_bytes(buf4) as usize;
3644 reader.read_exact(&mut buf4)?;
3645 let vale_unk2 = u32::from_le_bytes(buf4) as usize;
3646
3647 skip_max_dependent_list(reader, vale_unk1)?;
3648 reader.seek(SeekFrom::Current(vale_unk2 as i64))?;
3649
3650 read_cooked_suffix(reader)?;
3651
3652 if num_verts > 0x20 {
3654 reader.read_exact(&mut tag)?;
3656 reader.read_exact(&mut tag)?;
3657 reader.read_exact(&mut buf4)?;
3658 reader.read_exact(&mut tag)?;
3660 reader.read_exact(&mut tag)?;
3661 reader.read_exact(&mut buf4)?;
3662
3663 reader.read_exact(&mut buf4)?; reader.read_exact(&mut buf4)?;
3665 let gaus_unk2 = u32::from_le_bytes(buf4) as usize;
3666 reader.seek(SeekFrom::Current((gaus_unk2 * 2) as i64))?;
3667 }
3668
3669 Ok(PhysShapeData::Hull { vertices })
3670}
3671
3672fn read_cooked_trimesh(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
3675 let mut tag = [0u8; 4];
3676 let mut buf4 = [0u8; 4];
3677
3678 reader.read_exact(&mut tag)?;
3680 if &tag != b"MESH" { bail!("Expected MESH, got {:?}", tag); }
3681 reader.read_exact(&mut buf4)?; reader.read_exact(&mut buf4)?;
3684 let flags = u32::from_le_bytes(buf4);
3685 reader.seek(SeekFrom::Current(4))?; reader.seek(SeekFrom::Current(4))?; reader.seek(SeekFrom::Current(4))?; reader.read_exact(&mut buf4)?;
3690 let num_verts = u32::from_le_bytes(buf4) as usize;
3691 reader.read_exact(&mut buf4)?;
3692 let num_tris = u32::from_le_bytes(buf4) as usize;
3693
3694 let mut vertices = Vec::with_capacity(num_verts);
3696 for _ in 0..num_verts {
3697 vertices.push(read_point3(reader)?);
3698 }
3699
3700 let mut indices = Vec::with_capacity(num_tris * 3);
3702 for _ in 0..(num_tris * 3) {
3703 let idx = if flags & 0x08 != 0 {
3704 let mut b = [0u8; 1];
3705 reader.read_exact(&mut b)?;
3706 b[0] as u32
3707 } else if flags & 0x10 != 0 {
3708 let mut b = [0u8; 2];
3709 reader.read_exact(&mut b)?;
3710 u16::from_le_bytes(b) as u32
3711 } else {
3712 reader.read_exact(&mut buf4)?;
3713 u32::from_le_bytes(buf4)
3714 };
3715 indices.push(idx);
3716 }
3717
3718 if flags & 0x01 != 0 {
3720 reader.seek(SeekFrom::Current((num_tris * 2) as i64))?;
3721 }
3722 if flags & 0x02 != 0 {
3723 reader.read_exact(&mut buf4)?;
3724 let max_val = u32::from_le_bytes(buf4);
3725 let elem_size = if max_val > 0xFFFF { 4 } else if max_val > 0xFF { 2 } else { 1 };
3726 reader.seek(SeekFrom::Current((num_tris * elem_size) as i64))?;
3727 }
3728
3729 reader.read_exact(&mut buf4)?;
3731 let num_convex_parts = u32::from_le_bytes(buf4) as usize;
3732 reader.read_exact(&mut buf4)?;
3733 let num_flat_parts = u32::from_le_bytes(buf4) as usize;
3734
3735 if num_convex_parts > 0 {
3736 reader.seek(SeekFrom::Current((num_tris * 2) as i64))?;
3737 }
3738 if num_flat_parts > 0 {
3739 let elem_size = if num_flat_parts > 0xFF { 2 } else { 1 };
3740 reader.seek(SeekFrom::Current((num_tris * elem_size) as i64))?;
3741 }
3742
3743 read_cooked_suffix(reader)?;
3744
3745 reader.read_exact(&mut buf4)?;
3747 let extra = u32::from_le_bytes(buf4);
3748 if extra != 0 {
3749 reader.seek(SeekFrom::Current(num_tris as i64))?;
3750 }
3751
3752 Ok(PhysShapeData::TriMesh { vertices, indices })
3753}
3754
3755fn read_mesh_shape(reader: &mut (impl std::io::Read + Seek), is_hull: bool) -> Result<PhysShapeData> {
3757 let mut magic = [0u8; 4];
3758 reader.read_exact(&mut magic)?;
3759
3760 if &magic == b"HSP\x01" {
3761 if is_hull {
3763 read_uncooked_hull(reader)
3764 } else {
3765 read_uncooked_trimesh(reader)
3766 }
3767 } else if &magic == b"NXS\x01" {
3768 if is_hull {
3770 read_cooked_hull(reader)
3771 } else {
3772 read_cooked_trimesh(reader)
3773 }
3774 } else {
3775 bail!("Unknown mesh magic: {:02x}{:02x}{:02x}{:02x}", magic[0], magic[1], magic[2], magic[3]);
3776 }
3777}
3778
3779pub fn parse_px_physical(data: &[u8]) -> Result<PxPhysicalData> {
3784 use crate::core::uoid::read_key_uoid;
3785 let mut cursor = Cursor::new(data);
3786
3787 let _class_idx = cursor.read_i16()?;
3789
3790 let self_key = read_key_uoid(&mut cursor)?;
3792 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
3793
3794 skip_synched_object(&mut cursor)?;
3796
3797 let mass = cursor.read_f32()?;
3799 let friction = cursor.read_f32()?;
3800 let restitution = cursor.read_f32()?;
3801 let bounds_raw = cursor.read_u8()?;
3802 let group_raw = cursor.read_u8()?;
3803 let reports_on = cursor.read_u32()?;
3804 let los_dbs = cursor.read_u16()?;
3805
3806 let mut group = read_phys_group(group_raw);
3808 if los_dbs == 0x0080 { group = PhysGroup::Max;
3810 }
3811
3812 let bounds = read_phys_bounds(bounds_raw)?;
3813
3814 let _object_key = read_key_uoid(&mut cursor)?;
3816 let _scene_node = read_key_uoid(&mut cursor)?;
3817 let _world_key = read_key_uoid(&mut cursor)?;
3818 let _sound_group = read_key_uoid(&mut cursor)?;
3819
3820 let position = read_point3(&mut cursor)?;
3822 let rotation = read_quat(&mut cursor)?;
3823
3824 skip_bit_vector(&mut cursor)?;
3826
3827 let shape = match bounds {
3830 PhysBoundsType::Sphere => {
3831 let radius = cursor.read_f32()?;
3832 let offset = read_point3(&mut cursor)?;
3833 PhysShapeData::Sphere { radius, offset }
3834 }
3835 PhysBoundsType::Box => {
3836 let dimensions = read_point3(&mut cursor)?;
3837 let offset = read_point3(&mut cursor)?;
3838 PhysShapeData::Box { dimensions, offset }
3839 }
3840 PhysBoundsType::Hull => {
3841 read_mesh_shape(&mut cursor, true)?
3842 }
3843 PhysBoundsType::Proxy | PhysBoundsType::Explicit => {
3844 read_mesh_shape(&mut cursor, false)?
3845 }
3846 };
3847
3848 Ok(PxPhysicalData {
3849 name,
3850 mass,
3851 friction,
3852 restitution,
3853 bounds,
3854 group,
3855 reports_on,
3856 los_dbs,
3857 position,
3858 rotation,
3859 shape,
3860 })
3861}
3862
3863#[derive(Debug, Clone)]
3870pub struct VisRegionData {
3871 pub self_key: Option<crate::core::uoid::Uoid>,
3872 pub region_key: Option<crate::core::uoid::Uoid>,
3874 pub disable_normal: bool,
3876 pub is_not: bool,
3878 pub replace_normal: bool,
3880 pub disabled: bool,
3882}
3883
3884impl Default for VisRegionData {
3885 fn default() -> Self {
3886 Self {
3887 self_key: None,
3888 region_key: None,
3889 disable_normal: false,
3890 is_not: false,
3891 replace_normal: true,
3892 disabled: false,
3893 }
3894 }
3895}
3896
3897pub fn parse_vis_region(data: &[u8]) -> Result<VisRegionData> {
3910 use crate::core::uoid::read_key_uoid;
3911 let mut cursor = Cursor::new(data);
3912
3913 let class_idx = cursor.read_i16()?;
3915 if class_idx < 0 { bail!("Null creatable in plVisRegion"); }
3916
3917 let self_key = read_key_uoid(&mut cursor)?;
3919
3920 skip_synched_object(&mut cursor)?;
3922
3923 let _owner_key = read_key_uoid(&mut cursor)?;
3925
3926 let prop_words = read_bit_vector_words(&mut cursor)?;
3928
3929 let get_bit = |bit: usize| -> bool {
3932 let word_idx = bit / 32;
3933 let bit_idx = bit % 32;
3934 word_idx < prop_words.len() && (prop_words[word_idx] & (1 << bit_idx)) != 0
3935 };
3936
3937 let disabled = get_bit(0); let is_not = get_bit(1); let replace_normal = get_bit(2); let disable_normal = get_bit(3); let region_key = read_key_uoid(&mut cursor)?;
3944
3945 let _vis_mgr_key = read_key_uoid(&mut cursor)?;
3947
3948 Ok(VisRegionData {
3949 self_key,
3950 region_key,
3951 disable_normal,
3952 is_not,
3953 replace_normal,
3954 disabled,
3955 })
3956}
3957
3958#[derive(Debug, Clone)]
3965pub enum VolumeIsectData {
3966 Convex {
3969 planes: Vec<ConvexPlane>,
3970 },
3971 Sphere {
3974 center: [f32; 3],
3975 world_center: [f32; 3],
3976 radius: f32,
3977 mins: [f32; 3],
3978 maxs: [f32; 3],
3979 },
3980 Cylinder {
3983 top: [f32; 3],
3984 bot: [f32; 3],
3985 radius: f32,
3986 world_bot: [f32; 3],
3987 world_norm: [f32; 3],
3988 length: f32,
3989 min: f32,
3990 max: f32,
3991 },
3992 Parallel {
3995 planes: Vec<ParallelPlane>,
3996 },
3997 Cone {
4000 capped: bool,
4001 rad_angle: f32,
4002 length: f32,
4003 world_tip: [f32; 3],
4004 world_norm: [f32; 3],
4005 norms: Vec<[f32; 3]>,
4006 dists: Vec<f32>,
4007 },
4008}
4009
4010#[derive(Debug, Clone)]
4011pub struct ConvexPlane {
4012 pub norm: [f32; 3],
4013 pub pos: [f32; 3],
4014 pub dist: f32,
4015 pub world_norm: [f32; 3],
4016 pub world_dist: f32,
4017}
4018
4019#[derive(Debug, Clone)]
4020pub struct ParallelPlane {
4021 pub norm: [f32; 3],
4022 pub min: f32,
4023 pub max: f32,
4024 pub pos_one: [f32; 3],
4025 pub pos_two: [f32; 3],
4026}
4027
4028fn read_volume_isect(reader: &mut (impl std::io::Read + Seek)) -> Result<Option<VolumeIsectData>> {
4031 use crate::core::class_index::ClassIndex;
4032
4033 let class_idx = reader.read_u16()?;
4034 if class_idx == 0x8000 {
4035 return Ok(None); }
4037
4038 match class_idx {
4039 ClassIndex::PL_CONVEX_ISECT => {
4040 let n = reader.read_u16()? as usize;
4042 let mut planes = Vec::with_capacity(n);
4043 for _ in 0..n {
4044 let norm = read_point3(reader)?;
4045 let pos = read_point3(reader)?;
4046 let dist = reader.read_f32()?;
4047 let world_norm = read_point3(reader)?;
4048 let world_dist = reader.read_f32()?;
4049 planes.push(ConvexPlane { norm, pos, dist, world_norm, world_dist });
4050 }
4051 Ok(Some(VolumeIsectData::Convex { planes }))
4052 }
4053 ClassIndex::PL_SPHERE_ISECT => {
4054 let center = read_point3(reader)?;
4056 let world_center = read_point3(reader)?;
4057 let radius = reader.read_f32()?;
4058 let mins = read_point3(reader)?;
4059 let maxs = read_point3(reader)?;
4060 Ok(Some(VolumeIsectData::Sphere { center, world_center, radius, mins, maxs }))
4061 }
4062 ClassIndex::PL_CYLINDER_ISECT => {
4063 let top = read_point3(reader)?;
4065 let bot = read_point3(reader)?;
4066 let radius = reader.read_f32()?;
4067 let world_bot = read_point3(reader)?;
4068 let world_norm = read_point3(reader)?;
4069 let length = reader.read_f32()?;
4070 let min = reader.read_f32()?;
4071 let max = reader.read_f32()?;
4072 Ok(Some(VolumeIsectData::Cylinder { top, bot, radius, world_bot, world_norm, length, min, max }))
4073 }
4074 ClassIndex::PL_PARALLEL_ISECT => {
4075 let n = reader.read_u16()? as usize;
4077 let mut planes = Vec::with_capacity(n);
4078 for _ in 0..n {
4079 let norm = read_point3(reader)?;
4080 let min = reader.read_f32()?;
4081 let max = reader.read_f32()?;
4082 let pos_one = read_point3(reader)?;
4083 let pos_two = read_point3(reader)?;
4084 planes.push(ParallelPlane { norm, min, max, pos_one, pos_two });
4085 }
4086 Ok(Some(VolumeIsectData::Parallel { planes }))
4087 }
4088 ClassIndex::PL_CONE_ISECT => {
4089 let capped = reader.read_u32()? != 0; let rad_angle = reader.read_f32()?;
4092 let length = reader.read_f32()?;
4093 let world_tip = read_point3(reader)?;
4094 let world_norm = read_point3(reader)?;
4095 let has_w2ndc = reader.read_u8()?;
4097 if has_w2ndc != 0 { reader.skip(64)?; }
4098 let has_l2ndc = reader.read_u8()?;
4099 if has_l2ndc != 0 { reader.skip(64)?; }
4100 let n = if capped { 5 } else { 4 };
4101 let mut norms = Vec::with_capacity(n);
4102 let mut dists = Vec::with_capacity(n);
4103 for _ in 0..n {
4104 norms.push(read_point3(reader)?);
4105 dists.push(reader.read_f32()?);
4106 }
4107 Ok(Some(VolumeIsectData::Cone { capped, rad_angle, length, world_tip, world_norm, norms, dists }))
4108 }
4109 _ => {
4110 bail!("Unknown plVolumeIsect subtype: 0x{:04X}", class_idx);
4111 }
4112 }
4113}
4114
4115#[derive(Debug, Clone)]
4122pub enum SoftVolume {
4123 Simple {
4124 key: Option<crate::core::uoid::Uoid>,
4125 inside_strength: f32,
4126 outside_strength: f32,
4127 soft_dist: f32,
4128 bounds_min: [f32; 3],
4129 bounds_max: [f32; 3],
4130 disabled: bool,
4131 },
4132 Union {
4133 key: Option<crate::core::uoid::Uoid>,
4134 inside_strength: f32,
4135 outside_strength: f32,
4136 sub_keys: Vec<crate::core::uoid::Uoid>,
4137 },
4138 Intersect {
4139 key: Option<crate::core::uoid::Uoid>,
4140 inside_strength: f32,
4141 outside_strength: f32,
4142 sub_keys: Vec<crate::core::uoid::Uoid>,
4143 },
4144 Invert {
4145 key: Option<crate::core::uoid::Uoid>,
4146 inside_strength: f32,
4147 outside_strength: f32,
4148 sub_key: Option<crate::core::uoid::Uoid>,
4149 },
4150}
4151
4152impl SoftVolume {
4153 pub fn key(&self) -> &Option<crate::core::uoid::Uoid> {
4154 match self {
4155 SoftVolume::Simple { key, .. } => key,
4156 SoftVolume::Union { key, .. } => key,
4157 SoftVolume::Intersect { key, .. } => key,
4158 SoftVolume::Invert { key, .. } => key,
4159 }
4160 }
4161}
4162
4163fn read_obj_interface_header(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, bool)> {
4167 use crate::core::uoid::read_key_uoid;
4168
4169 let class_idx = cursor.read_i16()?;
4171 if class_idx < 0 { bail!("Null creatable in soft volume"); }
4172
4173 let self_key = read_key_uoid(cursor)?;
4175
4176 skip_synched_object(cursor)?;
4178
4179 let _owner_key = read_key_uoid(cursor)?;
4181
4182 let prop_words = read_bit_vector_words(cursor)?;
4184 let disabled = !prop_words.is_empty() && (prop_words[0] & 1) != 0; Ok((self_key, disabled))
4187}
4188
4189fn read_soft_volume_base(cursor: &mut Cursor<&[u8]>) -> Result<(u32, f32, f32)> {
4193 let listen_state = cursor.read_u32()?;
4194 let inside_strength = cursor.read_f32()?;
4195 let outside_strength = cursor.read_f32()?;
4196 Ok((listen_state, inside_strength, outside_strength))
4197}
4198
4199pub fn parse_soft_volume_simple(data: &[u8]) -> Result<(SoftVolume, Option<VolumeIsectData>)> {
4202 let mut cursor = Cursor::new(data);
4203
4204 let (self_key, disabled) = read_obj_interface_header(&mut cursor)?;
4205 let (_listen_state, inside_strength, outside_strength) = read_soft_volume_base(&mut cursor)?;
4206
4207 let soft_dist = cursor.read_f32()?;
4209
4210 let volume = read_volume_isect(&mut cursor)?;
4212
4213 let (bounds_min, bounds_max) = match &volume {
4215 Some(VolumeIsectData::Convex { planes }) => compute_convex_bounds(planes),
4216 Some(VolumeIsectData::Sphere { world_center, radius, .. }) => {
4217 let r = *radius;
4218 (
4219 [world_center[0] - r, world_center[1] - r, world_center[2] - r],
4220 [world_center[0] + r, world_center[1] + r, world_center[2] + r],
4221 )
4222 }
4223 Some(VolumeIsectData::Cylinder { world_bot, world_norm, length, radius, .. }) => {
4224 let r = *radius;
4225 let top = [
4226 world_bot[0] + world_norm[0] * length,
4227 world_bot[1] + world_norm[1] * length,
4228 world_bot[2] + world_norm[2] * length,
4229 ];
4230 (
4231 [
4232 world_bot[0].min(top[0]) - r,
4233 world_bot[1].min(top[1]) - r,
4234 world_bot[2].min(top[2]) - r,
4235 ],
4236 [
4237 world_bot[0].max(top[0]) + r,
4238 world_bot[1].max(top[1]) + r,
4239 world_bot[2].max(top[2]) + r,
4240 ],
4241 )
4242 }
4243 _ => ([f32::MIN, f32::MIN, f32::MIN], [f32::MAX, f32::MAX, f32::MAX]),
4244 };
4245
4246 let sv = SoftVolume::Simple {
4247 key: self_key,
4248 inside_strength,
4249 outside_strength,
4250 soft_dist,
4251 bounds_min,
4252 bounds_max,
4253 disabled,
4254 };
4255
4256 Ok((sv, volume))
4257}
4258
4259fn compute_convex_bounds(planes: &[ConvexPlane]) -> ([f32; 3], [f32; 3]) {
4262 if planes.is_empty() {
4263 return ([0.0; 3], [0.0; 3]);
4264 }
4265 let mut min = [f32::MAX; 3];
4266 let mut max = [f32::MIN; 3];
4267 for p in planes {
4268 for i in 0..3 {
4269 min[i] = min[i].min(p.pos[i]);
4270 max[i] = max[i].max(p.pos[i]);
4271 }
4272 }
4273 (min, max)
4274}
4275
4276fn parse_soft_volume_complex_base(data: &[u8]) -> Result<(
4279 Option<crate::core::uoid::Uoid>,
4280 f32, f32,
4281 Vec<crate::core::uoid::Uoid>,
4282)> {
4283 use crate::core::uoid::read_key_uoid;
4284 let mut cursor = Cursor::new(data);
4285
4286 let (self_key, _disabled) = read_obj_interface_header(&mut cursor)?;
4287 let (_listen_state, inside_strength, outside_strength) = read_soft_volume_base(&mut cursor)?;
4288
4289 let n = cursor.read_u32()? as usize;
4291 let mut sub_keys = Vec::with_capacity(n);
4292 for _ in 0..n {
4293 if let Some(uoid) = read_key_uoid(&mut cursor)? {
4294 sub_keys.push(uoid);
4295 }
4296 }
4297
4298 Ok((self_key, inside_strength, outside_strength, sub_keys))
4299}
4300
4301pub fn parse_soft_volume_union(data: &[u8]) -> Result<SoftVolume> {
4304 let (self_key, inside_strength, outside_strength, sub_keys) =
4305 parse_soft_volume_complex_base(data)?;
4306 Ok(SoftVolume::Union { key: self_key, inside_strength, outside_strength, sub_keys })
4307}
4308
4309pub fn parse_soft_volume_intersect(data: &[u8]) -> Result<SoftVolume> {
4312 let (self_key, inside_strength, outside_strength, sub_keys) =
4313 parse_soft_volume_complex_base(data)?;
4314 Ok(SoftVolume::Intersect { key: self_key, inside_strength, outside_strength, sub_keys })
4315}
4316
4317pub fn parse_soft_volume_invert(data: &[u8]) -> Result<SoftVolume> {
4321 let (self_key, inside_strength, outside_strength, sub_keys) =
4322 parse_soft_volume_complex_base(data)?;
4323 let sub_key = sub_keys.into_iter().next();
4324 Ok(SoftVolume::Invert { key: self_key, inside_strength, outside_strength, sub_key })
4325}
4326
4327pub fn read_bit_vector(data: &[u8], offset: &mut usize) -> Vec<u32> {
4329 if *offset + 4 > data.len() { return Vec::new(); }
4330 let count = u32::from_le_bytes([data[*offset], data[*offset+1], data[*offset+2], data[*offset+3]]) as usize;
4331 *offset += 4;
4332 let mut words = Vec::with_capacity(count);
4333 for _ in 0..count {
4334 if *offset + 4 > data.len() { break; }
4335 words.push(u32::from_le_bytes([data[*offset], data[*offset+1], data[*offset+2], data[*offset+3]]));
4336 *offset += 4;
4337 }
4338 words
4339}
4340
4341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4347pub enum DecalManagerType {
4348 Foot,
4349 Ripple,
4350 Puddle,
4351 Bullet,
4352 Wake,
4353 Torpedo,
4354 RippleVS,
4355 TorpedoVS,
4356}
4357
4358#[derive(Debug, Clone)]
4360pub struct DecalManagerData {
4361 pub name: String,
4362 pub manager_type: DecalManagerType,
4363 pub mat_pre_shade: Option<String>,
4364 pub mat_rt_shade: Option<String>,
4365 pub target_names: Vec<String>,
4366 pub max_num_verts: u32,
4367 pub max_num_idx: u32,
4368 pub wait_on_enable: u32,
4369 pub intensity: f32,
4370 pub wet_length: f32,
4371 pub ramp_end: f32,
4372 pub decay_start: f32,
4373 pub life_span: f32,
4374 pub grid_size_u: f32,
4375 pub grid_size_v: f32,
4376 pub scale: [f32; 3],
4377 pub party_time: f32,
4378 pub notify_names: Vec<String>,
4379 pub init_uvw: Option<[f32; 3]>,
4380 pub final_uvw: Option<[f32; 3]>,
4381 pub wake_default_dir: Option<[f32; 3]>,
4382 pub wake_anim_wgt: Option<f32>,
4383 pub wake_vel_wgt: Option<f32>,
4384}
4385
4386fn parse_dyna_decal_mgr_base(cursor: &mut Cursor<&[u8]>) -> Result<DecalManagerData> {
4388 use crate::core::uoid::read_key_uoid;
4389 let self_key = read_key_uoid(cursor)?;
4390 let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
4391 skip_synched_object(cursor)?;
4392 let mat_pre = read_key_name(cursor)?;
4393 let mat_rt = read_key_name(cursor)?;
4394 let num_targets = cursor.read_u32()?;
4395 let mut target_names = Vec::new();
4396 for _ in 0..num_targets { if let Some(n) = read_key_name(cursor)? { target_names.push(n); } }
4397 let num_party = cursor.read_u32()?;
4398 for _ in 0..num_party { let _ = read_key_name(cursor)?; }
4399 let max_num_verts = cursor.read_u32()?;
4400 let max_num_idx = cursor.read_u32()?;
4401 let wait_on_enable = cursor.read_u32()?;
4402 let intensity = cursor.read_f32()?;
4403 let wet_length = cursor.read_f32()?;
4404 let ramp_end = cursor.read_f32()?;
4405 let decay_start = cursor.read_f32()?;
4406 let life_span = cursor.read_f32()?;
4407 let grid_size_u = cursor.read_f32()?;
4408 let grid_size_v = cursor.read_f32()?;
4409 let sx = cursor.read_f32()?; let sy = cursor.read_f32()?; let sz = cursor.read_f32()?;
4410 let party_time = cursor.read_f32()?;
4411 let num_notifies = cursor.read_u32()?;
4412 let mut notify_names = Vec::new();
4413 for _ in 0..num_notifies { if let Some(n) = read_key_name(cursor)? { notify_names.push(n); } }
4414 Ok(DecalManagerData {
4415 name, manager_type: DecalManagerType::Foot,
4416 mat_pre_shade: mat_pre, mat_rt_shade: mat_rt, target_names,
4417 max_num_verts, max_num_idx, wait_on_enable,
4418 intensity, wet_length, ramp_end, decay_start, life_span,
4419 grid_size_u, grid_size_v, scale: [sx, sy, sz], party_time, notify_names,
4420 init_uvw: None, final_uvw: None,
4421 wake_default_dir: None, wake_anim_wgt: None, wake_vel_wgt: None,
4422 })
4423}
4424
4425fn read_ripple_uvw(c: &mut Cursor<&[u8]>, m: &mut DecalManagerData) -> Result<()> {
4426 let ix = c.read_f32()?; let iy = c.read_f32()?; let iz = c.read_f32()?;
4427 m.init_uvw = Some([ix, iy, iz]);
4428 let fx = c.read_f32()?; let fy = c.read_f32()?; let fz = c.read_f32()?;
4429 m.final_uvw = Some([fx, fy, fz]);
4430 Ok(())
4431}
4432
4433pub fn parse_dyna_foot_mgr(data: &[u8]) -> Result<DecalManagerData> {
4435 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4436 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Foot; Ok(m)
4437}
4438pub fn parse_dyna_ripple_mgr(data: &[u8]) -> Result<DecalManagerData> {
4440 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4441 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Ripple;
4442 read_ripple_uvw(&mut c, &mut m)?; Ok(m)
4443}
4444pub fn parse_dyna_bullet_mgr(data: &[u8]) -> Result<DecalManagerData> {
4446 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4447 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Bullet; Ok(m)
4448}
4449pub fn parse_dyna_puddle_mgr(data: &[u8]) -> Result<DecalManagerData> {
4451 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4452 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Puddle;
4453 read_ripple_uvw(&mut c, &mut m)?; Ok(m)
4454}
4455pub fn parse_dyna_wake_mgr(data: &[u8]) -> Result<DecalManagerData> {
4457 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4458 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Wake;
4459 read_ripple_uvw(&mut c, &mut m)?;
4460 let dx = c.read_f32()?; let dy = c.read_f32()?; let dz = c.read_f32()?;
4461 m.wake_default_dir = Some([dx, dy, dz]);
4462 let ac = c.read_u16()?; if ac != 0x8000 { return Ok(m); }
4463 m.wake_anim_wgt = Some(c.read_f32()?); m.wake_vel_wgt = Some(c.read_f32()?); Ok(m)
4464}
4465pub fn parse_dyna_torpedo_mgr(data: &[u8]) -> Result<DecalManagerData> {
4467 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4468 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Torpedo;
4469 read_ripple_uvw(&mut c, &mut m)?; Ok(m)
4470}
4471pub fn parse_dyna_ripple_vs_mgr(data: &[u8]) -> Result<DecalManagerData> {
4473 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4474 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::RippleVS;
4475 read_ripple_uvw(&mut c, &mut m)?; let _ = read_key_name(&mut c)?; Ok(m)
4476}
4477pub fn parse_dyna_torpedo_vs_mgr(data: &[u8]) -> Result<DecalManagerData> {
4479 let mut c = Cursor::new(data); let _ = c.read_i16()?;
4480 let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::TorpedoVS;
4481 read_ripple_uvw(&mut c, &mut m)?; let _ = read_key_name(&mut c)?; Ok(m)
4482}
4483
4484#[derive(Debug, Clone)]
4492pub struct EaxListenerModData {
4493 pub self_key: Option<crate::core::uoid::Uoid>,
4494 pub soft_region_key: Option<String>,
4497 pub environment: u32,
4499 pub environment_size: f32,
4501 pub environment_diffusion: f32,
4503 pub room: i32,
4505 pub room_hf: i32,
4507 pub room_lf: i32,
4509 pub decay_time: f32,
4511 pub decay_hf_ratio: f32,
4513 pub decay_lf_ratio: f32,
4515 pub reflections: i32,
4517 pub reflections_delay: f32,
4519 pub reverb: i32,
4521 pub reverb_delay: f32,
4523 pub echo_time: f32,
4525 pub echo_depth: f32,
4527 pub modulation_time: f32,
4529 pub modulation_depth: f32,
4531 pub air_absorption_hf: f32,
4533 pub hf_reference: f32,
4535 pub lf_reference: f32,
4537 pub room_rolloff_factor: f32,
4539 pub flags: u32,
4541}
4542
4543pub fn parse_eax_listener_mod(data: &[u8]) -> Result<EaxListenerModData> {
4548 use crate::core::uoid::read_key_uoid;
4549 let mut cursor = Cursor::new(data);
4550
4551 let _class_idx = cursor.read_i16()?;
4553
4554 let self_key = read_key_uoid(&mut cursor)?;
4556
4557 skip_synched_object(&mut cursor)?;
4559
4560 let num_bit_vectors = cursor.read_u32()?;
4562 for _ in 0..num_bit_vectors {
4563 let _word = cursor.read_u32()?;
4564 }
4565
4566 let soft_region_uoid = read_key_uoid(&mut cursor)?;
4569 let soft_region_key = soft_region_uoid.map(|u| u.object_name.clone());
4570
4571 let environment = cursor.read_u32()?;
4574 let environment_size = cursor.read_f32()?;
4575 let environment_diffusion = cursor.read_f32()?;
4576 let room = cursor.read_i32()?;
4577 let room_hf = cursor.read_i32()?;
4578 let room_lf = cursor.read_i32()?;
4579 let decay_time = cursor.read_f32()?;
4580 let decay_hf_ratio = cursor.read_f32()?;
4581 let decay_lf_ratio = cursor.read_f32()?;
4582 let reflections = cursor.read_i32()?;
4583 let reflections_delay = cursor.read_f32()?;
4584 let reverb = cursor.read_i32()?;
4586 let reverb_delay = cursor.read_f32()?;
4587 let echo_time = cursor.read_f32()?;
4589 let echo_depth = cursor.read_f32()?;
4590 let modulation_time = cursor.read_f32()?;
4591 let modulation_depth = cursor.read_f32()?;
4592 let air_absorption_hf = cursor.read_f32()?;
4593 let hf_reference = cursor.read_f32()?;
4594 let lf_reference = cursor.read_f32()?;
4595 let room_rolloff_factor = cursor.read_f32()?;
4596 let flags = cursor.read_u32()?;
4597
4598 Ok(EaxListenerModData {
4599 self_key,
4600 soft_region_key,
4601 environment,
4602 environment_size,
4603 environment_diffusion,
4604 room,
4605 room_hf,
4606 room_lf,
4607 decay_time,
4608 decay_hf_ratio,
4609 decay_lf_ratio,
4610 reflections,
4611 reflections_delay,
4612 reverb,
4613 reverb_delay,
4614 echo_time,
4615 echo_depth,
4616 modulation_time,
4617 modulation_depth,
4618 air_absorption_hf,
4619 hf_reference,
4620 lf_reference,
4621 room_rolloff_factor,
4622 flags,
4623 })
4624}
4625
4626#[cfg(test)]
4631mod round_trip_tests {
4632 use super::*;
4633 use std::path::Path;
4634
4635 fn round_trip_file(path: &Path) -> Result<()> {
4637 let original = std::fs::read(path)?;
4638 let page = PrpPage::from_file(path)?;
4639 let written = page.to_bytes()?;
4640
4641 if original != written {
4642 let min_len = original.len().min(written.len());
4644 for i in 0..min_len {
4645 if original[i] != written[i] {
4646 bail!(
4647 "{}: first byte diff at offset 0x{:X} (orig={:#04X}, written={:#04X}), \
4648 original={} bytes, written={} bytes",
4649 path.display(), i, original[i], written[i],
4650 original.len(), written.len()
4651 );
4652 }
4653 }
4654 if original.len() != written.len() {
4655 bail!(
4656 "{}: length mismatch: original={} bytes, written={} bytes",
4657 path.display(), original.len(), written.len()
4658 );
4659 }
4660 }
4661 Ok(())
4662 }
4663
4664 #[test]
4665 fn test_round_trip_cleft() {
4666 let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
4667 if !path.exists() {
4668 eprintln!("Skipping: {:?} not found", path);
4669 return;
4670 }
4671 round_trip_file(path).unwrap();
4672 eprintln!("Round-trip OK: Cleft_District_Cleft.prp");
4673 }
4674
4675 #[test]
4676 fn test_round_trip_all_ages() {
4677 let dat_dir = Path::new("../../Plasma/staging/client/dat");
4678 if !dat_dir.exists() {
4679 eprintln!("Skipping: {:?} not found", dat_dir);
4680 return;
4681 }
4682
4683 let mut total = 0;
4684 let mut passed = 0;
4685 let mut failed = 0;
4686 let mut failures: Vec<String> = Vec::new();
4687
4688 let mut entries: Vec<_> = std::fs::read_dir(dat_dir).unwrap()
4689 .filter_map(|e| e.ok())
4690 .filter(|e| e.path().extension().is_some_and(|ext| ext == "prp"))
4691 .collect();
4692 entries.sort_by_key(|e| e.file_name());
4693
4694 for entry in &entries {
4695 total += 1;
4696 match round_trip_file(&entry.path()) {
4697 Ok(()) => passed += 1,
4698 Err(e) => {
4699 failed += 1;
4700 let msg = format!("{}", e);
4701 if failures.len() < 10 {
4702 failures.push(msg.clone());
4703 }
4704 eprintln!("FAIL: {}", msg);
4705 }
4706 }
4707 }
4708
4709 eprintln!("\nRound-trip results: {}/{} passed, {} failed", passed, total, failed);
4710
4711 if failed > 0 {
4712 panic!("{} PRP files failed round-trip. First failures:\n{}",
4713 failed, failures.join("\n"));
4714 }
4715 }
4716}