use anyhow::{Context, Result, bail};
use std::io::{Read, Write, Seek, SeekFrom, Cursor};
use std::path::Path;
pub trait PlasmaRead: Read {
fn read_u8(&mut self) -> Result<u8> {
let mut buf = [0u8; 1];
self.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_u16(&mut self) -> Result<u16> {
let mut buf = [0u8; 2];
self.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
fn read_u32(&mut self) -> Result<u32> {
let mut buf = [0u8; 4];
self.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_i16(&mut self) -> Result<i16> {
let mut buf = [0u8; 2];
self.read_exact(&mut buf)?;
Ok(i16::from_le_bytes(buf))
}
fn read_i32(&mut self) -> Result<i32> {
let mut buf = [0u8; 4];
self.read_exact(&mut buf)?;
Ok(i32::from_le_bytes(buf))
}
fn read_f32(&mut self) -> Result<f32> {
let mut buf = [0u8; 4];
self.read_exact(&mut buf)?;
Ok(f32::from_le_bytes(buf))
}
fn read_f64(&mut self) -> Result<f64> {
let mut buf = [0u8; 8];
self.read_exact(&mut buf)?;
Ok(f64::from_le_bytes(buf))
}
fn read_safe_string(&mut self) -> Result<String> {
let raw_len = self.read_u16()?;
let len = (raw_len & 0x0FFF) as usize;
if len == 0 {
return Ok(String::new());
}
let mut buf = vec![0u8; len];
self.read_exact(&mut buf)?;
if raw_len & 0xF000 == 0xF000 {
for byte in &mut buf {
*byte = !*byte;
}
}
if buf.last() == Some(&0) {
buf.pop();
}
Ok(String::from_utf8_lossy(&buf).into_owned())
}
fn read_matrix44(&mut self) -> Result<[f32; 16]> {
let mut m = [0f32; 16];
for val in &mut m {
*val = self.read_f32()?;
}
Ok(m)
}
fn skip(&mut self, n: usize) -> Result<()> {
let mut buf = vec![0u8; n];
self.read_exact(&mut buf)?;
Ok(())
}
}
impl<T: Read> PlasmaRead for T {}
pub trait PlasmaWrite: Write {
fn write_u8(&mut self, v: u8) -> Result<()> {
self.write_all(&[v])?;
Ok(())
}
fn write_u16(&mut self, v: u16) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_u32(&mut self, v: u32) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_i16(&mut self, v: i16) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_i32(&mut self, v: i32) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_f32(&mut self, v: f32) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_f64(&mut self, v: f64) -> Result<()> {
self.write_all(&v.to_le_bytes())?;
Ok(())
}
fn write_safe_string(&mut self, s: &str) -> Result<()> {
let bytes = s.as_bytes();
let len = bytes.len();
let raw_len = (len as u16) | 0xF000; self.write_all(&raw_len.to_le_bytes())?;
let inverted: Vec<u8> = bytes.iter().map(|b| !b).collect();
self.write_all(&inverted)?;
Ok(())
}
fn write_matrix44(&mut self, m: &[f32; 16]) -> Result<()> {
for val in m {
self.write_all(&val.to_le_bytes())?;
}
Ok(())
}
}
impl<T: Write> PlasmaWrite for T {}
#[derive(Debug, Clone)]
pub struct PageHeader {
pub version: u32,
pub sequence_number: u32,
pub flags: u16,
pub age_name: String,
pub page_name: String,
pub major_version: u16,
pub checksum: u32,
pub data_start: u32,
pub index_start: u32,
pub class_versions: Vec<(u16, u16)>,
}
impl PageHeader {
fn read(reader: &mut impl Read) -> Result<Self> {
let version = reader.read_u32()?;
if version != 6 {
bail!("Unsupported .prp version: {} (expected 6)", version);
}
let sequence_number = reader.read_u32()?;
let flags = reader.read_u16()?;
let age_name = reader.read_safe_string()?;
let page_name = reader.read_safe_string()?;
let major_version = reader.read_u16()?;
let checksum = reader.read_u32()?;
let data_start = reader.read_u32()?;
let index_start = reader.read_u32()?;
let mut class_versions = Vec::new();
if data_start > 0 {
let num_class_versions = reader.read_u16()?;
for _ in 0..num_class_versions {
let class_type = reader.read_u16()?;
let version = reader.read_u16()?;
class_versions.push((class_type, version));
}
}
Ok(Self {
version,
sequence_number,
flags,
age_name,
page_name,
major_version,
checksum,
data_start,
index_start,
class_versions,
})
}
fn write(&self, writer: &mut impl Write) -> Result<()> {
writer.write_u32(self.version)?;
writer.write_u32(self.sequence_number)?;
writer.write_u16(self.flags)?;
writer.write_safe_string(&self.age_name)?;
writer.write_safe_string(&self.page_name)?;
writer.write_u16(self.major_version)?;
writer.write_u32(self.checksum)?;
writer.write_u32(self.data_start)?;
writer.write_u32(self.index_start)?;
if self.data_start > 0 {
writer.write_u16(self.class_versions.len() as u16)?;
for &(class_type, version) in &self.class_versions {
writer.write_u16(class_type)?;
writer.write_u16(version)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ObjectKey {
pub class_type: u16,
pub object_name: String,
pub object_id: u32,
pub start_pos: u32,
pub data_len: u32,
pub location_sequence: u32,
pub location_flags: u16,
pub load_mask: u8,
pub clone_id: u16,
pub clone_player_id: u32,
}
impl ObjectKey {
pub fn to_uoid(&self) -> crate::core::uoid::Uoid {
crate::core::uoid::Uoid {
location: crate::core::location::Location {
sequence_number: self.location_sequence,
flags: self.location_flags,
},
class_type: self.class_type,
object_name: self.object_name.clone(),
object_id: self.object_id,
load_mask: crate::core::load_mask::LoadMask::from_byte(self.load_mask),
clone_id: self.clone_id,
clone_player_id: self.clone_player_id,
}
}
fn read(reader: &mut impl Read) -> Result<Self> {
let contents = reader.read_u8()?;
let location_sequence = reader.read_u32()?;
let location_flags = reader.read_u16()?;
let load_mask = if contents & 0x02 != 0 {
reader.read_u8()?
} else {
0xFF };
let class_type = reader.read_u16()?;
let object_id = reader.read_u32()?;
let object_name = reader.read_safe_string()?;
let (clone_id, clone_player_id) = if contents & 0x01 != 0 {
let clone_id = reader.read_u16()?;
let _reserved = reader.read_u16()?;
let clone_player_id = reader.read_u32()?;
(clone_id, clone_player_id)
} else {
(0, 0)
};
let start_pos = reader.read_u32()?;
let data_len = reader.read_u32()?;
Ok(Self {
class_type,
object_name,
object_id,
start_pos,
data_len,
location_sequence,
location_flags,
load_mask,
clone_id,
clone_player_id,
})
}
pub fn write(&self, writer: &mut impl Write) -> Result<()> {
let has_load_mask = self.load_mask != 0xFF;
let has_clone = self.clone_id != 0 || self.clone_player_id != 0;
let mut contents: u8 = 0;
if has_clone { contents |= 0x01; }
if has_load_mask { contents |= 0x02; }
writer.write_u8(contents)?;
writer.write_u32(self.location_sequence)?;
writer.write_u16(self.location_flags)?;
if has_load_mask {
writer.write_u8(self.load_mask)?;
}
writer.write_u16(self.class_type)?;
writer.write_u32(self.object_id)?;
writer.write_safe_string(&self.object_name)?;
if has_clone {
writer.write_u16(self.clone_id)?;
writer.write_u16(0u16)?; writer.write_u32(self.clone_player_id)?;
}
writer.write_u32(self.start_pos)?;
writer.write_u32(self.data_len)?;
Ok(())
}
}
#[allow(dead_code)]
pub mod class_types {
pub const PL_SCENE_NODE: u16 = 0x0000;
pub const PL_SCENE_OBJECT: u16 = 0x0001;
pub const PL_MIPMAP: u16 = 0x0004;
pub const PL_CUBIC_ENVIRONMAP: u16 = 0x0005;
pub const PL_LAYER: u16 = 0x0006;
pub const HS_GMATERIAL: u16 = 0x0007;
pub const PL_DRAWABLE_SPANS: u16 = 0x004C;
pub const PL_CLUSTER_GROUP: u16 = 0x012B;
pub const PL_DYNAMIC_TEXT_MAP: u16 = 0x00AD;
}
#[derive(Debug, Clone)]
pub struct KeyIndexGroup {
pub class_type: u16,
pub deprecated_flags: u8,
}
#[derive(Debug)]
pub struct PrpPage {
pub header: PageHeader,
pub keys: Vec<ObjectKey>,
pub key_groups: Vec<KeyIndexGroup>,
data: Vec<u8>,
}
impl PrpPage {
fn parse_keys(cursor: &mut Cursor<&[u8]>, index_start: u64) -> Result<(Vec<ObjectKey>, Vec<KeyIndexGroup>)> {
cursor.seek(SeekFrom::Start(index_start))?;
let mut keys = Vec::new();
let mut key_groups = Vec::new();
let num_class_types = cursor.read_u32()?;
for _ in 0..num_class_types {
let class_type = cursor.read_u16()?;
let key_list_len = cursor.read_u32()?;
let pos_before = cursor.position();
let pos_end = pos_before + key_list_len as u64;
let deprecated_flags = cursor.read_u8()?;
let num_keys = cursor.read_u32()?;
key_groups.push(KeyIndexGroup { class_type, deprecated_flags });
for _ in 0..num_keys {
if cursor.position() >= pos_end { break; }
match ObjectKey::read(cursor) {
Ok(key) => keys.push(key),
Err(_) => break,
}
}
cursor.seek(SeekFrom::Start(pos_end))?;
}
Ok((keys, key_groups))
}
pub fn from_file(path: &Path) -> Result<Self> {
let data = std::fs::read(path)
.with_context(|| format!("Failed to read {:?}", path))?;
let mut cursor = Cursor::new(data.as_slice());
let header = PageHeader::read(&mut cursor)
.with_context(|| format!("Failed to parse header of {:?}", path))?;
let (keys, key_groups) = Self::parse_keys(&mut cursor, header.index_start as u64)?;
Ok(Self { header, keys, key_groups, data })
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let mut cursor = Cursor::new(data.as_slice());
let header = PageHeader::read(&mut cursor)
.with_context(|| "Failed to parse PRP header from bytes")?;
let (keys, key_groups) = Self::parse_keys(&mut cursor, header.index_start as u64)?;
Ok(Self { header, keys, key_groups, data })
}
pub fn object_data(&self, key: &ObjectKey) -> Option<&[u8]> {
let start = key.start_pos as usize;
let end = start + key.data_len as usize;
if end <= self.data.len() {
Some(&self.data[start..end])
} else {
None
}
}
pub fn raw_data(&self) -> &[u8] {
&self.data
}
pub fn keys_of_type(&self, class_type: u16) -> Vec<&ObjectKey> {
self.keys.iter().filter(|k| k.class_type == class_type).collect()
}
pub fn count_by_type(&self, class_type: u16) -> usize {
self.keys.iter().filter(|k| k.class_type == class_type).count()
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::with_capacity(self.data.len());
self.header.write(&mut buf)?;
let data_start = self.header.data_start as usize;
let index_start = self.header.index_start as usize;
if data_start <= self.data.len() && index_start <= self.data.len() {
while buf.len() < data_start {
buf.push(0);
}
buf.extend_from_slice(&self.data[data_start..index_start]);
}
self.write_key_index(&mut buf)?;
Ok(buf)
}
fn write_key_index(&self, writer: &mut impl Write) -> Result<()> {
let num_class_types = self.key_groups.len() as u32;
writer.write_u32(num_class_types)?;
for group in &self.key_groups {
let class_keys: Vec<&ObjectKey> = self.keys.iter()
.filter(|k| k.class_type == group.class_type)
.collect();
let mut key_payload = Vec::new();
key_payload.write_u8(group.deprecated_flags)?;
key_payload.write_u32(class_keys.len() as u32)?;
for key in &class_keys {
key.write(&mut key_payload)?;
}
writer.write_u16(group.class_type)?;
writer.write_u32(key_payload.len() as u32)?;
writer.write_all(&key_payload)?;
}
Ok(())
}
pub fn save(&self, path: &Path) -> Result<()> {
let bytes = self.to_bytes()?;
std::fs::write(path, &bytes)
.with_context(|| format!("Failed to write {:?}", path))?;
Ok(())
}
}
pub fn read_key_name(reader: &mut (impl Read + Seek)) -> Result<Option<String>> {
let non_nil = reader.read_u8()?;
if non_nil == 0 {
return Ok(None);
}
let contents = reader.read_u8()?;
reader.skip(6)?; if contents & 0x02 != 0 {
reader.skip(1)?; }
let _class_type = reader.read_u16()?;
let _object_id = reader.read_u32()?;
let name = reader.read_safe_string()?;
if contents & 0x01 != 0 {
reader.skip(8)?; }
Ok(Some(name))
}
pub fn skip_synched_object_pub(reader: &mut (impl Read + Seek)) -> Result<()> {
skip_synched_object(reader)
}
fn skip_synched_object(reader: &mut (impl Read + Seek)) -> Result<()> {
let synch_flags = reader.read_u32()?;
if synch_flags & 0x10 != 0 { let n = reader.read_u16()?;
for _ in 0..n { reader.read_safe_string()?; }
}
if synch_flags & 0x20 != 0 { let n = reader.read_u16()?;
for _ in 0..n { reader.read_safe_string()?; }
}
Ok(())
}
pub fn parse_material_layers(data: &[u8]) -> Result<Vec<String>> {
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
read_key_name(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let _load_flags = cursor.read_u32()?;
let _comp_flags = cursor.read_u32()?;
let num_layers = cursor.read_u32()?;
let _num_piggybacks = cursor.read_u32()?;
let mut layer_names = Vec::new();
for _ in 0..num_layers {
if let Some(name) = read_key_name(&mut cursor)? {
layer_names.push(name);
}
}
Ok(layer_names)
}
pub fn parse_layer_texture(data: &[u8]) -> Result<Option<String>> {
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
read_key_name(&mut cursor)?;
skip_synched_object(&mut cursor)?;
read_key_name(&mut cursor)?;
cursor.skip(20)?;
let has_matrix = cursor.read_u8()?;
if has_matrix != 0 {
cursor.skip(64)?; }
cursor.skip(16)?;
cursor.skip(16)?;
cursor.skip(16)?;
cursor.skip(16)?;
cursor.skip(4)?;
cursor.skip(4)?;
cursor.skip(4)?;
cursor.skip(4)?;
let texture_name = read_key_name(&mut cursor)?;
Ok(texture_name)
}
#[derive(Debug, Clone)]
pub struct LayerState {
pub name: String,
pub texture_name: Option<String>,
pub blend_flags: u32,
pub shade_flags: u32,
pub misc_flags: u32,
pub z_flags: u32,
pub uv_channel: u8,
pub uvw_src_full: u32,
pub opacity: f32,
pub uv_transform: Option<[[f32; 4]; 4]>,
pub preshade_color: [f32; 4],
pub runtime_color: [f32; 4],
pub ambient_color: [f32; 4],
pub specular_color: [f32; 4],
}
pub fn parse_layer_state(data: &[u8]) -> Result<LayerState> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.map(|u| u.object_name).unwrap_or_default();
let _synched = SynchedObjectData::read(&mut cursor)?;
let _underlay = read_key_uoid(&mut cursor)?;
let blend_flags = cursor.read_u32()?;
let _clamp_flags = cursor.read_u32()?;
let shade_flags = cursor.read_u32()?;
let z_flags = cursor.read_u32()?;
let misc_flags = cursor.read_u32()?;
let has_matrix = cursor.read_u8()?;
let uv_transform = if has_matrix != 0 {
let mut m = [[0.0f32; 4]; 4];
for row in &mut m {
for val in row.iter_mut() {
*val = cursor.read_f32()?;
}
}
Some(m)
} else {
None
};
let mut preshade_color = [0.0f32; 4];
let mut runtime_color = [0.0f32; 4];
let mut ambient_color = [0.0f32; 4];
let mut specular_color = [0.0f32; 4];
for c in &mut preshade_color { *c = cursor.read_f32()?; }
for c in &mut runtime_color { *c = cursor.read_f32()?; }
for c in &mut ambient_color { *c = cursor.read_f32()?; }
for c in &mut specular_color { *c = cursor.read_f32()?; }
let uvwsrc = cursor.read_u32()?;
let opacity = cursor.read_f32()?;
cursor.skip(8)?;
let tex = read_key_uoid(&mut cursor)?;
let texture_name = tex.map(|u| u.object_name);
Ok(LayerState {
name,
texture_name,
blend_flags,
shade_flags,
misc_flags,
z_flags,
uv_channel: (uvwsrc & 0xFFFF) as u8,
uvw_src_full: uvwsrc,
opacity,
uv_transform,
preshade_color,
runtime_color,
ambient_color,
specular_color,
})
}
pub fn parse_material_comp_flags(data: &[u8]) -> Result<u32> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let _self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let _load_flags = cursor.read_u32()?;
let comp_flags = cursor.read_u32()?;
Ok(comp_flags)
}
pub fn parse_layer_uv_channel(data: &[u8]) -> Result<u8> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let _self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let _underlay = read_key_uoid(&mut cursor)?;
cursor.skip(20)?; let has_matrix = cursor.read_u8()?;
if has_matrix != 0 { cursor.skip(64)?; }
cursor.skip(64)?; let uvwsrc = cursor.read_u32()?;
Ok((uvwsrc & 0xFFFF) as u8)
}
#[derive(Debug, Clone, Copy)]
pub struct UvScrollRate {
pub u: f32,
pub v: f32,
}
#[allow(dead_code)]
mod key_types {
pub const POINT3: u8 = 1;
pub const BEZ_POINT3: u8 = 2;
pub const SCALAR: u8 = 3;
pub const BEZ_SCALAR: u8 = 4;
pub const SCALE: u8 = 5;
pub const BEZ_SCALE: u8 = 6;
pub const QUAT: u8 = 7;
pub const COMPRESSED_QUAT32: u8 = 8;
pub const COMPRESSED_QUAT64: u8 = 9;
pub const MAX_KEY: u8 = 10;
pub const MATRIX33: u8 = 11;
pub const MATRIX44: u8 = 12;
}
fn key_size(key_type: u8) -> Option<usize> {
match key_type {
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,
}
}
const LEAF_CONTROLLER: u16 = 0x0230; const COMPOUND_CONTROLLER: u16 = 0x0231;
fn skip_creatable(cursor: &mut Cursor<&[u8]>) -> Result<bool> {
let class_idx = cursor.read_u16()?;
if class_idx == 0x8000 { return Ok(false); }
match class_idx {
LEAF_CONTROLLER => {
skip_leaf_controller(cursor)?;
}
COMPOUND_CONTROLLER => {
for _ in 0..3 {
skip_creatable(cursor)?;
}
}
_ => {
bail!("Unknown controller class 0x{:04X}", class_idx);
}
}
Ok(true)
}
fn skip_leaf_controller(cursor: &mut Cursor<&[u8]>) -> Result<()> {
let key_type = cursor.read_u8()?;
let num_keys = cursor.read_u32()?;
if let Some(ks) = key_size(key_type) {
cursor.skip(ks * num_keys as usize)?;
} else if key_type == key_types::MAX_KEY {
cursor.skip(62 * num_keys as usize)?;
} else {
bail!("Unknown key type {} with {} keys", key_type, num_keys);
}
Ok(())
}
fn read_transform_leaf_controller(cursor: &mut Cursor<&[u8]>) -> Result<Option<UvScrollRate>> {
let key_type = cursor.read_u8()?;
let num_keys = cursor.read_u32()?;
if key_type != key_types::MATRIX44 || num_keys < 2 {
if let Some(ks) = key_size(key_type) {
cursor.skip(ks * num_keys as usize)?;
} else if key_type == key_types::MAX_KEY {
cursor.skip(62 * num_keys as usize)?;
} else {
bail!("Unknown key type {} with {} keys", key_type, num_keys);
}
return Ok(None);
}
let frame0 = cursor.read_u16()?;
let mat0 = cursor.read_matrix44()?;
if num_keys > 2 {
let skip_bytes = 66 * (num_keys as usize - 2);
let pos = cursor.position() as usize;
let data_len = cursor.get_ref().len();
if pos + skip_bytes + 66 > data_len {
return Ok(None);
}
cursor.skip(skip_bytes)?;
}
let frame_last = cursor.read_u16()?;
let mat_last = cursor.read_matrix44()?;
let dt_frames = frame_last as f32 - frame0 as f32;
if dt_frames <= 0.0 { return Ok(None); }
let dt_secs = dt_frames / 30.0;
let du = mat_last[3] - mat0[3];
let dv = mat_last[7] - mat0[7];
let rate_u = du / dt_secs;
let rate_v = dv / dt_secs;
if (rate_u.abs() < 1e-6 && rate_v.abs() < 1e-6) || rate_u.abs() > 100.0 || rate_v.abs() > 100.0 {
return Ok(None);
}
Ok(Some(UvScrollRate {
u: rate_u,
v: rate_v,
}))
}
#[derive(Debug, Clone)]
pub struct LayerAnimData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub underlay_key: Option<crate::core::uoid::Uoid>,
pub opacity_keys: Vec<(f32, f32)>,
pub has_opacity: bool,
pub preshade_color_keys: Vec<(f32, [f32; 3])>,
pub runtime_color_keys: Vec<(f32, [f32; 3])>,
pub ambient_color_keys: Vec<(f32, [f32; 3])>,
pub specular_color_keys: Vec<(f32, [f32; 3])>,
pub length: f32,
pub sdl_var_name: Option<String>,
}
fn read_color_keys(cursor: &mut Cursor<&[u8]>) -> Result<Vec<(f32, [f32; 3])>> {
let mut keys = Vec::new();
let class_id = cursor.read_u16()?;
if class_id == 0x8000 { return Ok(keys); }
if class_id == LEAF_CONTROLLER {
let key_type = cursor.read_u8()?;
let num_keys = cursor.read_u32()?;
if key_type == key_types::POINT3 {
for _ in 0..num_keys {
let frame = cursor.read_u16()? as f32 / 30.0;
let x = cursor.read_f32()?;
let y = cursor.read_f32()?;
let z = cursor.read_f32()?;
keys.push((frame, [x, y, z]));
}
} else if key_type == key_types::BEZ_POINT3 {
for _ in 0..num_keys {
let frame = cursor.read_u16()? as f32 / 30.0;
cursor.skip(24)?; let x = cursor.read_f32()?;
let y = cursor.read_f32()?;
let z = cursor.read_f32()?;
keys.push((frame, [x, y, z]));
}
} else {
if let Some(ks) = key_size(key_type) {
cursor.skip(ks * num_keys as usize)?;
}
}
} else if class_id == COMPOUND_CONTROLLER {
for _ in 0..3 { skip_creatable(cursor)?; }
}
Ok(keys)
}
fn read_opacity_keys(cursor: &mut Cursor<&[u8]>) -> Result<(bool, Vec<(f32, f32)>)> {
let mut keys = Vec::new();
let class_id = cursor.read_u16()?;
if class_id == 0x8000 { return Ok((false, keys)); }
if class_id == LEAF_CONTROLLER {
let key_type = cursor.read_u8()?;
let num_keys = cursor.read_u32()?;
if key_type == key_types::SCALAR {
for _ in 0..num_keys {
let frame = cursor.read_u16()? as f32 / 30.0;
let value = cursor.read_f32()?;
keys.push((frame, value));
}
} else if key_type == key_types::BEZ_SCALAR {
for _ in 0..num_keys {
let frame = cursor.read_u16()? as f32 / 30.0;
let _in_tan = cursor.read_f32()?;
let _out_tan = cursor.read_f32()?;
let value = cursor.read_f32()?;
keys.push((frame, value));
}
} else {
if let Some(ks) = key_size(key_type) {
cursor.skip(ks * num_keys as usize)?;
}
}
} else if class_id == COMPOUND_CONTROLLER {
for _ in 0..3 { skip_creatable(cursor)?; }
}
Ok((true, keys))
}
pub fn parse_layer_animation_full(data: &[u8]) -> Result<LayerAnimData> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let underlay_key = read_key_uoid(&mut cursor)?;
let preshade_color_keys = read_color_keys(&mut cursor)?;
let runtime_color_keys = read_color_keys(&mut cursor)?;
let ambient_color_keys = read_color_keys(&mut cursor)?;
let specular_color_keys = read_color_keys(&mut cursor)?;
let (has_opacity, opacity_keys) = read_opacity_keys(&mut cursor)?;
skip_creatable(&mut cursor)?;
let mut length: f32 = 0.0;
if let Some(last) = preshade_color_keys.last() { length = length.max(last.0); }
if let Some(last) = runtime_color_keys.last() { length = length.max(last.0); }
if let Some(last) = ambient_color_keys.last() { length = length.max(last.0); }
if let Some(last) = specular_color_keys.last() { length = length.max(last.0); }
if let Some(last) = opacity_keys.last() { length = length.max(last.0); }
Ok(LayerAnimData {
self_key,
underlay_key,
opacity_keys,
has_opacity,
preshade_color_keys,
runtime_color_keys,
ambient_color_keys,
specular_color_keys,
length,
sdl_var_name: None,
})
}
pub fn parse_layer_animation_scroll(data: &[u8]) -> Result<Option<UvScrollRate>> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let _self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let _underlay = read_key_uoid(&mut cursor)?;
for _ in 0..5 {
skip_creatable(&mut cursor)?;
}
let xform_class = cursor.read_u16()?;
if xform_class == 0x8000 { return Ok(None); }
match xform_class {
LEAF_CONTROLLER => {
read_transform_leaf_controller(&mut cursor)
}
COMPOUND_CONTROLLER => {
Ok(None)
}
_ => {
Ok(None)
}
}
}
pub fn parse_layer_sdl_animation_full(data: &[u8]) -> Result<LayerAnimData> {
let mut la = parse_layer_animation_full(data)?;
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let _self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let _underlay = read_key_uoid(&mut cursor)?;
for _ in 0..6 { skip_creatable(&mut cursor)?; }
match cursor.read_safe_string() {
Ok(name) if !name.is_empty() => { la.sdl_var_name = Some(name); }
_ => {}
}
Ok(la)
}
pub fn parse_layer_animation_underlay(data: &[u8]) -> Result<Option<crate::core::uoid::Uoid>> {
use crate::core::uoid::read_key_uoid;
use crate::core::synched_object::SynchedObjectData;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
let _self_key = read_key_uoid(&mut cursor)?;
let _synched = SynchedObjectData::read(&mut cursor)?;
let underlay = read_key_uoid(&mut cursor)?;
Ok(underlay)
}
#[cfg(test)]
mod scroll_tests {
use super::*;
use std::path::Path;
#[test]
fn test_parse_cleft_layer_animation_scroll() {
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() {
eprintln!("Skipping test: {:?} not found", path);
return;
}
let page = PrpPage::from_file(path).unwrap();
let mut parsed = 0;
let mut ok = 0;
let mut errs = 0;
let mut with_scroll = 0;
for key in page.keys_of_type(0x0043) { if let Some(data) = page.object_data(key) {
parsed += 1;
match parse_layer_animation_scroll(data) {
Ok(Some(scroll)) => {
with_scroll += 1;
ok += 1;
eprintln!(" scroll '{}': u={:.4} v={:.4}", key.object_name, scroll.u, scroll.v);
}
Ok(None) => { ok += 1; }
Err(e) => {
errs += 1;
if errs <= 3 {
let hex: Vec<String> = data.iter().take(100).map(|b| format!("{:02x}", b)).collect();
eprintln!(" ERR '{}': {} (data: {}...)", key.object_name, e, hex.join(" "));
}
}
}
}
}
eprintln!("plLayerAnimation: {} total, {} ok, {} with scroll, {} errors",
parsed, ok, with_scroll, errs);
let path2 = Path::new("../../Plasma/staging/client/dat/Teledahn_District_Teledahn.prp");
if !path2.exists() {
eprintln!("Skipping Teledahn test");
return;
}
let page2 = PrpPage::from_file(path2).unwrap();
let mut t_parsed = 0;
let mut t_ok = 0;
let mut t_scroll = 0;
let mut t_err = 0;
for key in page2.keys_of_type(0x0043) {
if let Some(data) = page2.object_data(key) {
t_parsed += 1;
match parse_layer_animation_scroll(data) {
Ok(Some(scroll)) => {
t_scroll += 1;
t_ok += 1;
eprintln!(" Teledahn scroll '{}': u={:.4} v={:.4}", key.object_name, scroll.u, scroll.v);
}
Ok(None) => { t_ok += 1; }
Err(_) => { t_err += 1; }
}
}
}
eprintln!("Teledahn plLayerAnimation: {} total, {} ok, {} with scroll, {} errors",
t_parsed, t_ok, t_scroll, t_err);
}
#[test]
fn test_parse_layer_animation_color_keys() {
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() { eprintln!("Skipping: {:?} not found", path); return; }
let page = PrpPage::from_file(path).unwrap();
let mut parsed = 0;
let mut with_color = 0;
let mut errors = 0;
for key in page.keys_of_type(0x0043) {
if let Some(data) = page.object_data(key) {
parsed += 1;
match parse_layer_animation_full(data) {
Ok(la) => {
let c = la.preshade_color_keys.len() + la.runtime_color_keys.len()
+ la.ambient_color_keys.len() + la.specular_color_keys.len();
if c > 0 { with_color += 1; }
}
Err(_) => { errors += 1; }
}
}
}
eprintln!("Color test: {} total, {} with color, {} errors", parsed, with_color, errors);
assert!(errors <= 2);
}
#[test]
fn test_parse_layer_sdl_animation() {
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() { eprintln!("Skipping: {:?} not found", path); return; }
let page = PrpPage::from_file(path).unwrap();
let mut parsed = 0;
let mut errors = 0;
for key in page.keys_of_type(0x00F0) {
if let Some(data) = page.object_data(key) {
parsed += 1;
match parse_layer_sdl_animation_full(data) {
Ok(la) => {
eprintln!(" SDL '{}': var={:?} len={:.2}s", key.object_name, la.sdl_var_name, la.length);
}
Err(_) => { errors += 1; }
}
}
}
eprintln!("SDL anim: {} total, {} errors", parsed, errors);
if parsed > 0 { assert_eq!(errors, 0); }
}
}
pub mod bitmap_compression {
pub const UNCOMPRESSED: u8 = 0x0;
pub const DIRECTX_COMPRESSION: u8 = 0x1;
pub const JPEG_COMPRESSION: u8 = 0x2;
pub const PNG_COMPRESSION: u8 = 0x3;
}
pub mod dxt_type {
pub const ERROR: u8 = 0x0;
pub const DXT1: u8 = 0x1;
pub const DXT5: u8 = 0x5;
}
pub mod uncompressed_type {
pub const RGB8888: u8 = 0x00;
pub const RGB4444: u8 = 0x01;
pub const RGB1555: u8 = 0x02;
pub const INTEN8: u8 = 0x03;
pub const A_INTEN88: u8 = 0x04;
}
#[derive(Debug, Clone)]
pub struct MipmapData {
pub name: String,
pub width: u32,
pub height: u32,
pub compression: u8,
pub pixel_format: u8,
pub block_size: u8,
pub pixel_size: u8,
pub num_levels: u8,
pub pixel_data: Vec<u8>,
pub level_sizes: Vec<u32>,
pub row_bytes: u32,
}
impl MipmapData {
pub fn decode_to_rgba(&self) -> Option<Vec<u8>> {
if self.compression != 1 { if self.pixel_size == 32 && !self.pixel_data.is_empty() {
let pixel_count = (self.width * self.height) as usize;
let expected = pixel_count * 4;
if self.pixel_data.len() < expected { return None; }
let mut rgba = vec![0u8; expected];
for i in 0..pixel_count {
let s = i * 4;
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]; }
return Some(rgba);
}
return None;
}
let w = self.width as usize;
let h = self.height as usize;
if w == 0 || h == 0 { return None; }
let blocks_wide = (w + 3) / 4;
let blocks_high = (h + 3) / 4;
let is_dxt5 = self.pixel_format == 5;
let block_size: usize = if is_dxt5 { 16 } else { 8 };
let needed = blocks_wide * blocks_high * block_size;
if self.pixel_data.len() < needed { return None; }
let mut rgba = vec![0u8; w * h * 4];
let expand5 = |v: u8| -> u8 { (v << 3) | (v >> 2) };
let expand6 = |v: u8| -> u8 { (v << 2) | (v >> 4) };
for by in 0..blocks_high {
for bx in 0..blocks_wide {
let block_off = (by * blocks_wide + bx) * block_size;
let color_off = if is_dxt5 { block_off + 8 } else { block_off };
let cb = &self.pixel_data[color_off..color_off + 8];
let c0 = u16::from_le_bytes([cb[0], cb[1]]);
let c1 = u16::from_le_bytes([cb[2], cb[3]]);
let (r0, g0, b0) = ((c0 >> 11) as u8 & 0x1F, (c0 >> 5) as u8 & 0x3F, c0 as u8 & 0x1F);
let (r1, g1, b1) = ((c1 >> 11) as u8 & 0x1F, (c1 >> 5) as u8 & 0x3F, c1 as u8 & 0x1F);
let colors: [[u8; 4]; 4] = if c0 > c1 {
let a = [expand5(r0), expand6(g0), expand5(b0), 255];
let b = [expand5(r1), expand6(g1), expand5(b1), 255];
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];
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];
[a, b, c2, c3]
} else {
let a = [expand5(r0), expand6(g0), expand5(b0), 255];
let b = [expand5(r1), expand6(g1), expand5(b1), 255];
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];
[a, b, c2, [0, 0, 0, 0]]
};
let indices = u32::from_le_bytes([cb[4], cb[5], cb[6], cb[7]]);
let alpha_block: [u8; 16] = if is_dxt5 {
let ab = &self.pixel_data[block_off..block_off + 8];
let (a0, a1) = (ab[0], ab[1]);
let mut alphas = [0u8; 8];
alphas[0] = a0; alphas[1] = a1;
if a0 > a1 {
for i in 1..7u16 { alphas[(i+1) as usize] = (((7-i)*a0 as u16 + i*a1 as u16)/7) as u8; }
} else {
for i in 1..5u16 { alphas[(i+1) as usize] = (((5-i)*a0 as u16 + i*a1 as u16)/5) as u8; }
alphas[6] = 0; alphas[7] = 255;
}
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);
let mut result = [255u8; 16];
for i in 0..16 { result[i] = alphas[((idx_bits >> (i as u64 * 3)) & 7) as usize]; }
result
} else { [255; 16] };
for py in 0..4 {
for px in 0..4 {
let (x, y) = (bx * 4 + px, by * 4 + py);
if x >= w || y >= h { continue; }
let pi = py * 4 + px;
let ci = ((indices >> (pi * 2)) & 3) as usize;
let out = y * w * 4 + x * 4;
rgba[out] = colors[ci][0]; rgba[out+1] = colors[ci][1];
rgba[out+2] = colors[ci][2]; rgba[out+3] = alpha_block[pi];
}
}
}
}
Some(rgba)
}
pub fn from_object_data(data: &[u8], name: &str) -> Result<Self> {
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 {
bail!("Null creatable");
}
read_key_name(&mut cursor)?;
let version = cursor.read_u8()?; if version != 2 {
bail!("Unsupported bitmap version: {} (expected 2)", version);
}
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 {
bitmap_compression::UNCOMPRESSED |
bitmap_compression::JPEG_COMPRESSION |
bitmap_compression::PNG_COMPRESSION => {
let fmt = cursor.read_u8()?;
(fmt, 0u8)
}
bitmap_compression::DIRECTX_COMPRESSION => {
let bs = cursor.read_u8()?;
let ct = cursor.read_u8()?;
(ct, bs)
}
_ => {
bail!("Unknown compression type: {}", compression_type);
}
};
let _low_modified_time = cursor.read_u32()?;
let _high_modified_time = cursor.read_u32()?;
let width = cursor.read_u32()?;
let height = cursor.read_u32()?;
let row_bytes = cursor.read_u32()?;
let total_size = cursor.read_u32()?;
let num_levels = cursor.read_u8()?;
if width == 0 || height == 0 {
return Ok(Self {
name: name.to_string(), width, height,
compression: compression_type, pixel_format, block_size,
pixel_size, num_levels, pixel_data: Vec::new(),
level_sizes: Vec::new(), row_bytes,
});
}
let level_sizes = build_level_sizes(
width, height, row_bytes, num_levels,
compression_type, block_size,
);
let mut pixel_data = Vec::new();
if total_size > 0 {
let remaining = data.len().saturating_sub(cursor.position() as usize);
let read_size = (total_size as usize).min(remaining);
match compression_type {
bitmap_compression::DIRECTX_COMPRESSION => {
pixel_data = vec![0u8; read_size];
cursor.read_exact(&mut pixel_data)?;
}
bitmap_compression::UNCOMPRESSED => {
pixel_data = vec![0u8; read_size];
cursor.read_exact(&mut pixel_data)?;
}
bitmap_compression::JPEG_COMPRESSION => {
pixel_data = vec![0u8; read_size];
cursor.read_exact(&mut pixel_data)?;
log::debug!("JPEG texture '{}' ({}x{}) — decompression not implemented",
name, width, height);
}
bitmap_compression::PNG_COMPRESSION => {
pixel_data = vec![0u8; read_size];
cursor.read_exact(&mut pixel_data)?;
log::debug!("PNG texture '{}' ({}x{}) — decompression not implemented",
name, width, height);
}
_ => {
bail!("Unknown compression type: {}", compression_type);
}
}
}
Ok(Self {
name: name.to_string(),
width,
height,
compression: compression_type,
pixel_format,
block_size,
pixel_size,
num_levels,
pixel_data,
level_sizes,
row_bytes,
})
}
pub fn from_cubic_envmap_data(data: &[u8], name: &str) -> Result<Vec<Self>> {
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable"); }
read_key_name(&mut cursor)?;
Self::skip_bitmap_header(&mut cursor)?;
let mut faces = Vec::with_capacity(6);
for i in 0..6 {
let face_name = format!("{}_face{}", name, i);
let face = Self::read_mipmap_from_cursor(&mut cursor, &face_name)?;
faces.push(face);
}
Ok(faces)
}
fn skip_bitmap_header(cursor: &mut Cursor<&[u8]>) -> Result<()> {
let _version = cursor.read_u8()?;
let _pixel_size = cursor.read_u8()?;
let _space = cursor.read_u8()?;
let _flags = cursor.read_u16()?;
let compression_type = cursor.read_u8()?;
match compression_type {
bitmap_compression::UNCOMPRESSED |
bitmap_compression::JPEG_COMPRESSION |
bitmap_compression::PNG_COMPRESSION => { cursor.skip(1)?; }
bitmap_compression::DIRECTX_COMPRESSION => { cursor.skip(2)?; }
_ => { bail!("Unknown compression type: {}", compression_type); }
}
cursor.skip(8)?; Ok(())
}
fn read_mipmap_from_cursor(cursor: &mut Cursor<&[u8]>, name: &str) -> Result<Self> {
let version = cursor.read_u8()?;
if version != 2 { bail!("Unsupported bitmap version: {}", version); }
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 {
bitmap_compression::UNCOMPRESSED |
bitmap_compression::JPEG_COMPRESSION |
bitmap_compression::PNG_COMPRESSION => { (cursor.read_u8()?, 0u8) }
bitmap_compression::DIRECTX_COMPRESSION => {
let bs = cursor.read_u8()?;
let ct = cursor.read_u8()?;
(ct, bs)
}
_ => { bail!("Unknown compression type: {}", compression_type); }
};
cursor.skip(8)?;
let width = cursor.read_u32()?;
let height = cursor.read_u32()?;
let row_bytes = cursor.read_u32()?;
let total_size = cursor.read_u32()?;
let num_levels = cursor.read_u8()?;
if width == 0 || height == 0 || total_size == 0 {
return Ok(Self {
name: name.to_string(), width, height,
compression: compression_type, pixel_format, block_size,
pixel_size, num_levels, pixel_data: Vec::new(),
level_sizes: Vec::new(), row_bytes,
});
}
let level_sizes = build_level_sizes(width, height, row_bytes, num_levels, compression_type, block_size);
let remaining = cursor.get_ref().len().saturating_sub(cursor.position() as usize);
let read_size = (total_size as usize).min(remaining);
let mut pixel_data = vec![0u8; read_size];
cursor.read_exact(&mut pixel_data)?;
Ok(Self {
name: name.to_string(), width, height,
compression: compression_type, pixel_format, block_size,
pixel_size, num_levels, pixel_data, level_sizes, row_bytes,
})
}
}
fn build_level_sizes(
width: u32, height: u32, row_bytes: u32, num_levels: u8,
compression_type: u8, block_size: u8,
) -> Vec<u32> {
let mut sizes = Vec::with_capacity(num_levels as usize);
let mut w = width;
let mut h = height;
let mut rb = row_bytes;
for _ in 0..num_levels {
let level_size = if compression_type == bitmap_compression::DIRECTX_COMPRESSION {
if (w | h) & 0x03 != 0 {
h * rb
} else {
(h * w * block_size as u32) >> 4
}
} else {
h * rb
};
sizes.push(level_size);
if w > 1 { w >>= 1; rb >>= 1; }
if h > 1 { h >>= 1; }
}
sizes
}
#[derive(Debug, Clone)]
pub enum PythonParamValue {
Int(i32),
Float(f32),
Bool(bool),
String(String),
Key(Option<crate::core::uoid::Uoid>),
None,
}
#[derive(Debug, Clone)]
pub struct PythonParam {
pub id: i32,
pub value_type: i32,
pub value: PythonParamValue,
}
#[derive(Debug, Clone)]
pub struct PythonFileModData {
pub script_file: String,
pub receivers: Vec<crate::core::uoid::Uoid>,
pub parameters: Vec<PythonParam>,
pub self_key: Option<crate::core::uoid::Uoid>,
}
pub fn parse_python_file_mod(data: &[u8]) -> Result<PythonFileModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let num_bit_vectors = cursor.read_u32()?;
for _ in 0..num_bit_vectors {
let _word = cursor.read_u32()?;
}
let script_file = cursor.read_safe_string()?;
let num_receivers = cursor.read_u32()?;
let mut receivers = Vec::with_capacity(num_receivers as usize);
for _ in 0..num_receivers {
if let Some(uoid) = read_key_uoid(&mut cursor)? {
receivers.push(uoid);
}
}
let num_params = cursor.read_u32()?;
let mut parameters = Vec::with_capacity(num_params as usize);
for _ in 0..num_params {
let id = cursor.read_i32()?;
let value_type = cursor.read_i32()?;
let value = match value_type {
1 => { let v = cursor.read_i32()?;
PythonParamValue::Int(v)
}
2 => { let v = cursor.read_f32()?;
PythonParamValue::Float(v)
}
3 => { let v = cursor.read_u32()?;
PythonParamValue::Bool(v != 0)
}
4 | 13 => { let count = cursor.read_u32()? as usize;
if count > 0 {
let mut buf = vec![0u8; count - 1];
cursor.read_exact(&mut buf)?;
let _null = cursor.read_u8()?; PythonParamValue::String(String::from_utf8_lossy(&buf).into_owned())
} else {
PythonParamValue::String(String::new())
}
}
5..=12 | 14..=23 => { let uoid = read_key_uoid(&mut cursor)?;
PythonParamValue::Key(uoid)
}
24 => { PythonParamValue::None
}
_ => {
log::warn!("Unknown plPythonParameter type {}", value_type);
PythonParamValue::None
}
};
parameters.push(PythonParam { id, value_type, value });
}
Ok(PythonFileModData {
script_file,
receivers,
parameters,
self_key,
})
}
#[derive(Debug, Clone)]
pub struct InterfaceInfoModData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub logic_keys: Vec<crate::core::uoid::Uoid>,
}
pub fn parse_interface_info_modifier(data: &[u8]) -> Result<InterfaceInfoModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let n_words = cursor.read_u32()?;
for _ in 0..n_words { let _ = cursor.read_u32()?; }
let key_count = cursor.read_u32()?;
let mut logic_keys = Vec::with_capacity(key_count as usize);
for _ in 0..key_count {
if let Some(uoid) = read_key_uoid(&mut cursor)? {
logic_keys.push(uoid);
}
}
Ok(InterfaceInfoModData { self_key, logic_keys })
}
#[derive(Debug, Clone)]
pub struct OneShotModData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub anim_name: String,
pub seek_duration: f32,
pub drivable: bool,
pub reversable: bool,
pub smart_seek: bool,
pub no_seek: bool,
}
pub fn parse_one_shot_mod(data: &[u8]) -> Result<OneShotModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let num_bit_vectors = cursor.read_u32()?;
for _ in 0..num_bit_vectors {
let _word = cursor.read_u32()?;
}
let anim_name = cursor.read_safe_string()?;
let seek_duration = cursor.read_f32()?;
let drivable = cursor.read_u8()? != 0; let reversable = cursor.read_u8()? != 0;
let smart_seek = cursor.read_u8()? != 0;
let no_seek = cursor.read_u8()? != 0;
Ok(OneShotModData {
self_key,
anim_name,
seek_duration,
drivable,
reversable,
smart_seek,
no_seek,
})
}
#[derive(Debug, Clone)]
pub struct LogicModData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub notify_receivers: Vec<crate::core::uoid::Uoid>,
pub notify_id: i32,
pub cursor_type: i32,
pub condition_keys: Vec<crate::core::uoid::Uoid>,
pub flags: u32,
pub disabled: bool,
}
pub fn parse_logic_modifier(data: &[u8]) -> Result<LogicModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let n_words = cursor.read_u32()?;
for _ in 0..n_words { let _ = cursor.read_u32()?; }
let cmd_count = cursor.read_u32()?;
for _ in 0..cmd_count {
if cmd_count > 0 {
return Ok(LogicModData {
self_key,
notify_receivers: Vec::new(),
notify_id: 0,
cursor_type: 0,
condition_keys: Vec::new(),
flags: 0,
disabled: false,
});
}
}
let notify_class = cursor.read_i16()?;
let mut notify_receivers = Vec::new();
let mut notify_id = 0i32;
if notify_class >= 0 {
let _sender = read_key_uoid(&mut cursor)?;
let num_receivers = cursor.read_u32()?;
for _ in 0..num_receivers {
if let Some(uoid) = read_key_uoid(&mut cursor)? {
notify_receivers.push(uoid);
}
}
let _timestamp = cursor.read_f32()?; let _ = cursor.read_f32()?; let _bcast_flags = cursor.read_u32()?;
let _notify_type = cursor.read_i32()?;
let _notify_state = cursor.read_f32()?;
notify_id = cursor.read_i32()?;
let num_events = cursor.read_u32()?;
for _ in 0..num_events {
let event_type = cursor.read_i32()?;
match event_type {
1 => cursor.skip(1 + 1)?, 7 => cursor.skip(1 + 1)?, 8 => cursor.skip(4)?, 9 => cursor.skip(4)?, _ => {} }
}
}
let n_words2 = cursor.read_u32()?;
let flags = if n_words2 > 0 { cursor.read_u32()? } else { 0 };
for _ in 1..n_words2 { let _ = cursor.read_u32()?; }
let disabled = cursor.read_u8()? != 0;
let cond_count = cursor.read_u32()?;
let mut condition_keys = Vec::with_capacity(cond_count as usize);
for _ in 0..cond_count {
if let Some(key) = read_key_uoid(&mut cursor)? {
condition_keys.push(key);
}
}
let cursor_type = cursor.read_i32()?;
Ok(LogicModData {
self_key,
notify_receivers,
notify_id,
cursor_type,
condition_keys,
flags,
disabled,
})
}
#[derive(Debug, Clone)]
pub struct OneShotCallback {
pub marker: String,
pub receiver: Option<crate::core::uoid::Uoid>,
pub user: i16,
}
#[derive(Debug, Clone)]
pub struct ResponderCmd {
pub class_id: u16,
pub wait_on: i8,
pub anim_name: Option<String>,
pub anim_flags: u32,
pub notify_receivers: Vec<crate::core::uoid::Uoid>,
pub msg_receivers: Vec<crate::core::uoid::Uoid>,
pub sound_index: i32,
pub sound_flags: u32,
pub oneshot_callbacks: Vec<OneShotCallback>,
pub timer_id: i32,
pub timer_delay: f32,
pub enable_cmd: u32,
}
#[derive(Debug, Clone)]
pub struct ResponderState {
pub num_callbacks: u8,
pub switch_to_state: u8,
pub commands: Vec<ResponderCmd>,
}
#[derive(Debug, Clone)]
pub struct ResponderModData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub states: Vec<ResponderState>,
pub cur_state: u8,
pub enabled: bool,
pub flags: u8,
pub cur_command: i32,
pub completed_events: u64,
pub triggerer: Option<crate::core::uoid::Uoid>,
}
fn read_msg_base(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, Vec<crate::core::uoid::Uoid>)> {
use crate::core::uoid::read_key_uoid;
let sender = read_key_uoid(cursor)?;
let num_receivers = cursor.read_u32()?;
let mut receivers = Vec::with_capacity(num_receivers as usize);
for _ in 0..num_receivers {
if let Some(u) = read_key_uoid(cursor)? {
receivers.push(u);
}
}
cursor.skip(12)?;
Ok((sender, receivers))
}
fn read_msg_with_callbacks(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, Vec<crate::core::uoid::Uoid>)> {
let (sender, receivers) = read_msg_base(cursor)?;
let num_callbacks = cursor.read_u32()?;
for _ in 0..num_callbacks {
let cb_class = cursor.read_i16()?;
if cb_class >= 0 {
let _ = read_msg_base(cursor)?;
cursor.skip(12)?; }
}
Ok((sender, receivers))
}
fn parse_responder_cmd(cursor: &mut Cursor<&[u8]>) -> Result<Option<ResponderCmd>> {
let class_id = cursor.read_u16()?;
let mut cmd = ResponderCmd {
class_id,
wait_on: -1,
anim_name: None,
anim_flags: 0,
notify_receivers: Vec::new(),
msg_receivers: Vec::new(),
sound_index: -1,
sound_flags: 0,
oneshot_callbacks: Vec::new(),
timer_id: -1,
timer_delay: 0.0,
enable_cmd: 0,
};
match class_id {
0x0206 => { let (_, receivers) = read_msg_with_callbacks(cursor)?;
cmd.msg_receivers = receivers;
let n = cursor.read_u32()?;
cmd.anim_flags = if n > 0 { cursor.read_u32()? } else { 0 };
for _ in 1..n { let _ = cursor.read_u32()?; }
cursor.skip(28)?;
cmd.anim_name = Some(cursor.read_safe_string()?);
let _loop_name = cursor.read_safe_string()?;
}
0x025A => { let (_, receivers) = read_msg_with_callbacks(cursor)?;
cmd.msg_receivers = receivers;
let n = cursor.read_u32()?;
cmd.sound_flags = if n > 0 { cursor.read_u32()? } else { 0 };
for _ in 1..n { let _ = cursor.read_u32()?; }
cursor.skip(8 + 8 + 1 + 1 + 4 + 8)?;
cmd.sound_index = cursor.read_i32()?;
cursor.skip(4 + 4 + 4 + 1)?;
}
0x02ED => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers.clone();
cmd.notify_receivers = receivers;
cursor.skip(12)?;
let num_events = cursor.read_u32()?;
for _ in 0..num_events {
let event_type = cursor.read_i32()?;
match event_type {
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)?, _ => {
return Ok(Some(cmd));
}
}
}
}
0x0254 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
let n1 = cursor.read_u32()?;
cmd.enable_cmd = if n1 > 0 { cursor.read_u32()? } else { 0 };
for _ in 1..n1 { let _ = cursor.read_u32()?; }
let n2 = cursor.read_u32()?;
for _ in 0..n2 { let _ = cursor.read_u32()?; }
}
0x024F => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
cmd.timer_id = cursor.read_i32()?;
cmd.timer_delay = cursor.read_f32()?;
}
0x020A => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
let n = cursor.read_u32()?;
for _ in 0..n { let _ = cursor.read_u32()?; }
cursor.skip(8 + 1)?;
let _ = crate::core::uoid::read_key_uoid(cursor)?;
let _ = crate::core::uoid::read_key_uoid(cursor)?;
cursor.skip(44 + 1)?;
}
0x0302 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
cursor.skip(1)?; }
0x0306 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
}
0x0332 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
let n = cursor.read_u32()?;
for _ in 0..n { let _ = cursor.read_u32()?; }
cursor.skip(16)?;
cmd.anim_name = Some(cursor.read_safe_string()?);
}
0x0335 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
cursor.skip(1 + 4)?;
}
0x0307 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
let n = cursor.read_u32()?;
for _ in 0..n {
let marker = cursor.read_safe_string()?;
let receiver = crate::core::uoid::read_key_uoid(cursor)?;
let user = cursor.read_i16()?;
cmd.oneshot_callbacks.push(OneShotCallback { marker, receiver, user });
}
}
0x03BF => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
let _ = crate::core::uoid::read_key_uoid(cursor)?; }
0x0453 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
cursor.skip(1)?; }
0x0393 => { let (_, receivers) = read_msg_base(cursor)?;
cmd.msg_receivers = receivers;
cursor.skip(1 + 1)?; }
_ => {
log::debug!("Unknown responder command class 0x{:04X}", class_id);
return Ok(None);
}
}
Ok(Some(cmd))
}
pub fn parse_responder_modifier(data: &[u8]) -> Result<ResponderModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let n_flags = cursor.read_u32()?;
for _ in 0..n_flags { let _ = cursor.read_u32()?; }
let num_states = cursor.read_u8()?;
let mut states = Vec::with_capacity(num_states as usize);
let mut parse_ok = true;
for _si in 0..num_states {
if !parse_ok { break; }
let num_callbacks = cursor.read_u8()?;
let switch_to_state = cursor.read_u8()?;
let num_cmds = cursor.read_u8()?;
let mut commands = Vec::with_capacity(num_cmds as usize);
for _ in 0..num_cmds {
if !parse_ok { break; }
match parse_responder_cmd(&mut cursor) {
Ok(Some(mut cmd)) => {
let wait_on = cursor.read_u8()? as i8;
cmd.wait_on = wait_on;
commands.push(cmd);
}
Ok(None) => {
parse_ok = false;
break;
}
Err(e) => {
log::debug!("Responder cmd parse error: {}", e);
parse_ok = false;
break;
}
}
}
if parse_ok {
let map_size = cursor.read_u8()?;
for _ in 0..map_size {
let _wait = cursor.read_u8()?;
let _cmd = cursor.read_u8()?;
}
}
states.push(ResponderState {
num_callbacks,
switch_to_state,
commands,
});
}
let (cur_state, enabled, flags) = if parse_ok {
let cs = cursor.read_u8()?;
let en = cursor.read_u8()? != 0; let fl = cursor.read_u8()?;
(cs, en, fl)
} else {
(0, true, 0x01) };
Ok(ResponderModData {
self_key,
states,
cur_state,
enabled,
flags,
cur_command: -1,
completed_events: 0,
triggerer: None,
})
}
#[derive(Debug, Clone)]
pub struct VolumeDetectorData {
pub name: String,
pub collision_type: u8,
pub receivers: Vec<crate::core::uoid::Uoid>,
pub proxy_key: Option<crate::core::uoid::Uoid>,
}
pub fn parse_volume_detector(data: &[u8]) -> Result<VolumeDetectorData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let num_bit_vectors = cursor.read_u32()?;
for _ in 0..num_bit_vectors {
let _word = cursor.read_u32()?;
}
let receiver_count = cursor.read_u32()?;
let mut receivers = Vec::with_capacity(receiver_count as usize);
for _ in 0..receiver_count {
if let Some(uoid) = read_key_uoid(&mut cursor)? {
receivers.push(uoid);
}
}
let _remote_mod = read_key_uoid(&mut cursor)?;
let proxy_key = read_key_uoid(&mut cursor)?;
let collision_type = cursor.read_u8()?;
Ok(VolumeDetectorData {
name,
collision_type,
receivers,
proxy_key,
})
}
fn read_hs_matrix44(reader: &mut (impl std::io::Read + Seek)) -> Result<[f32; 16]> {
let has_data = reader.read_u8()? != 0;
if has_data {
let mut m = [0f32; 16];
for val in &mut m {
*val = reader.read_f32()?;
}
Ok(m)
} else {
Ok([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])
}
}
#[derive(Debug, Clone)]
pub struct PostEffectModData {
pub name: String,
pub hither: f32,
pub yon: f32,
pub fov_x: f32,
pub fov_y: f32,
pub w2c: [f32; 16],
pub c2w: [f32; 16],
}
pub fn parse_post_effect_mod(data: &[u8]) -> Result<PostEffectModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let num_bv = cursor.read_u32()?;
for _ in 0..num_bv { let _w = cursor.read_u32()?; }
let num_sv = cursor.read_u32()?;
let mut state_bits = 0u32;
for i in 0..num_sv {
let w = cursor.read_u32()?;
if i == 0 { state_bits = w; }
}
log::debug!(" PostEffectMod flags_bv={} state_bv={} state_bits=0x{:X} pos={}",
num_bv, num_sv, state_bits, cursor.position());
let hither = cursor.read_f32()?;
let yon = cursor.read_f32()?;
let fov_x = cursor.read_f32()?;
let fov_y = cursor.read_f32()?;
log::debug!(" hither={} yon={} fov_x={} fov_y={}", hither, yon, fov_x, fov_y);
let _node_key = read_key_uoid(&mut cursor)?;
let w2c = read_hs_matrix44(&mut cursor)?;
let c2w = read_hs_matrix44(&mut cursor)?;
log::debug!(" W2C row0=({:.3},{:.3},{:.3},{:.3})", w2c[0], w2c[1], w2c[2], w2c[3]);
log::debug!(" W2C row1=({:.3},{:.3},{:.3},{:.3})", w2c[4], w2c[5], w2c[6], w2c[7]);
log::debug!(" W2C row2=({:.3},{:.3},{:.3},{:.3})", w2c[8], w2c[9], w2c[10], w2c[11]);
log::debug!(" W2C row3=({:.3},{:.3},{:.3},{:.3})", w2c[12], w2c[13], w2c[14], w2c[15]);
Ok(PostEffectModData {
name, hither, yon, fov_x, fov_y, w2c, c2w,
})
}
#[derive(Debug, Clone)]
pub struct GuiControlData {
pub name: String,
pub control_type: String,
pub tag_id: u32,
pub visible: bool,
pub text: Option<String>,
}
fn skip_gui_proc(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
let proc_type = reader.read_u32()?; match proc_type {
3 => {} 0 => { let len = reader.read_u32()? as usize;
if len > 0 { reader.skip(len)?; }
}
1 => {} 2 => {} _ => {} }
Ok(())
}
fn parse_gui_control_base(cursor: &mut Cursor<&[u8]>) -> Result<(u32, bool)> {
use crate::core::uoid::read_key_uoid;
let num_bv = cursor.read_u32()?;
let mut flags = 0u32;
for i in 0..num_bv {
let w = cursor.read_u32()?;
if i == 0 { flags = w; }
}
let tag_id = cursor.read_u32()?;
let visible = cursor.read_u8()? != 0;
skip_gui_proc(cursor)?;
let has_dyn_text = cursor.read_u8()? != 0;
if has_dyn_text {
let _layer_key = read_key_uoid(cursor)?;
let _dyn_text_key = read_key_uoid(cursor)?;
}
let has_color = cursor.read_u8()? != 0;
if has_color {
cursor.skip(4 * 4 * 4)?; cursor.read_u32()?; cursor.read_safe_string()?; cursor.read_u8()?; cursor.read_u8()?; }
let sound_count = cursor.read_u8()? as usize;
cursor.skip(sound_count * 4)?;
if flags & (1 << 21) != 0 {
let _proxy_key = read_key_uoid(cursor)?;
}
let _skin_key = read_key_uoid(cursor)?;
Ok((tag_id, visible))
}
pub fn parse_gui_button(data: &[u8]) -> Result<GuiControlData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
match parse_gui_control_base(&mut cursor) {
Ok((tag_id, visible)) => Ok(GuiControlData {
name,
control_type: "Button".to_string(),
tag_id,
visible,
text: None,
}),
Err(_) => Ok(GuiControlData {
name,
control_type: "Button".to_string(),
tag_id: 0,
visible: true,
text: None,
}),
}
}
pub fn parse_gui_textbox(data: &[u8]) -> Result<GuiControlData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let (tag_id, visible) = match parse_gui_control_base(&mut cursor) {
Ok(v) => v,
Err(_) => return Ok(GuiControlData {
name, control_type: "TextBox".to_string(), tag_id: 0, visible: true, text: None,
}),
};
let text = if let Ok(text_len) = cursor.read_u32() {
let text_len = text_len as usize;
if text_len > 0 && text_len < 65536 {
let mut text_bytes = vec![0u8; text_len];
if cursor.read_exact(&mut text_bytes).is_ok() {
let s = String::from_utf8_lossy(&text_bytes).trim_end_matches('\0').to_string();
if s.is_empty() { None } else { Some(s) }
} else { None }
} else { None }
} else { None };
Ok(GuiControlData {
name,
control_type: "TextBox".to_string(),
tag_id,
visible,
text,
})
}
#[derive(Debug, Clone)]
pub struct ParticleSystemData {
pub name: String,
pub material_name: Option<String>,
pub x_tiles: u32,
pub y_tiles: u32,
pub max_particles: u32,
pub accel: [f32; 3],
pub pre_sim: f32,
pub drag: f32,
pub wind_mult: f32,
pub emitter_sources: Vec<[f32; 3]>,
pub particle_size: [f32; 2],
pub num_emitters: u32,
pub particle_life_range: [f32; 2],
pub particles_per_second: f32,
pub velocity_range: [f32; 2],
pub angle_range: f32,
pub scale_range: [f32; 2],
pub emitter_misc_flags: u32,
pub emitter_color: [f32; 4],
}
fn skip_creatable_controller(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
let class_idx = reader.read_u16()?;
if class_idx & 0x8000 != 0 {
return Ok(()); }
if class_idx == 0x01A9 {
let key_type = reader.read_u8()?;
let num_keys = reader.read_u32()?;
let key_size: usize = match key_type {
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),
};
reader.seek(SeekFrom::Current((num_keys as usize * key_size) as i64))?;
} else if class_idx == 0x01AA {
for _ in 0..3 {
skip_creatable_controller(reader)?;
}
} else {
bail!("Unsupported controller class 0x{:04X}", class_idx);
}
Ok(())
}
pub fn parse_particle_system(data: &[u8]) -> Result<ParticleSystemData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let mat_key = read_key_uoid(&mut cursor)?;
let material_name = mat_key.map(|k| k.object_name);
for _i in 0..5 {
skip_creatable_controller(&mut cursor)?;
}
let x_tiles = cursor.read_u32()?;
let y_tiles = cursor.read_u32()?;
let max_particles = cursor.read_u32()?;
let _max_emitters = cursor.read_u32()?;
let pre_sim = cursor.read_f32()?;
let accel_x = cursor.read_f32()?;
let accel_y = cursor.read_f32()?;
let accel_z = cursor.read_f32()?;
let drag = cursor.read_f32()?;
let wind_mult = cursor.read_f32()?;
let num_emitters = cursor.read_u32()?;
let mut emitter_sources = Vec::new();
let mut particle_size = [1.0_f32, 1.0];
let mut particle_life_range = [1.0_f32, 1.0];
let mut particles_per_second = 0.0_f32;
let mut velocity_range = [0.0_f32, 0.0];
let mut angle_range = 0.0_f32;
let mut scale_range = [1.0_f32, 1.0];
let mut emitter_misc_flags = 0u32;
let mut emitter_color = [1.0_f32, 1.0, 1.0, 1.0];
for _ei in 0..num_emitters {
let emitter_class = cursor.read_u16()?;
if emitter_class & 0x8000 != 0 { continue; }
let gen_class = cursor.read_u16()?;
if gen_class & 0x8000 == 0 {
log::debug!(" Emitter generator class: 0x{:04X}", gen_class);
if gen_class == 0x02D8 {
let _gen_life = cursor.read_f32()?;
let part_life_min = cursor.read_f32()?;
let part_life_max = cursor.read_f32()?;
particle_life_range = [part_life_min, part_life_max];
let pps = cursor.read_f32()?;
particles_per_second = pps;
let num_sources = cursor.read_u32()?;
for _ in 0..num_sources {
let px = cursor.read_f32()?;
let py = cursor.read_f32()?;
let pz = cursor.read_f32()?;
emitter_sources.push([px, py, pz]);
let _pitch = cursor.read_f32()?;
let _yaw = cursor.read_f32()?;
}
let ang = cursor.read_f32()?;
angle_range = ang;
let vel_min = cursor.read_f32()?;
let vel_max = cursor.read_f32()?;
velocity_range = [vel_min, vel_max];
let x_size = cursor.read_f32()?;
let y_size = cursor.read_f32()?;
particle_size = [x_size, y_size];
let sc_min = cursor.read_f32()?;
let sc_max = cursor.read_f32()?;
scale_range = [sc_min, sc_max];
cursor.skip(4 * 2)?; } else if gen_class == 0x0336 {
let count = cursor.read_u32()?;
let x_size = cursor.read_f32()?;
let y_size = cursor.read_f32()?;
particle_size = [x_size, y_size];
let sc_min = cursor.read_f32()?;
let sc_max = cursor.read_f32()?;
scale_range = [sc_min, sc_max];
cursor.skip(4)?; for _ in 0..count {
let px = cursor.read_f32()?;
let py = cursor.read_f32()?;
let pz = cursor.read_f32()?;
emitter_sources.push([px, py, pz]);
}
cursor.skip((count as usize) * 12)?; } else {
break;
}
}
let _span_idx = cursor.read_u32()?;
let _max_parts = cursor.read_u32()?;
let misc_flags = cursor.read_u32()?;
emitter_misc_flags = misc_flags;
let cr = cursor.read_f32()?;
let cg = cursor.read_f32()?;
let cb = cursor.read_f32()?;
let ca = cursor.read_f32()?;
emitter_color = [cr, cg, cb, ca];
}
Ok(ParticleSystemData {
name,
material_name,
x_tiles, y_tiles,
max_particles,
accel: [accel_x, accel_y, accel_z],
pre_sim,
drag,
wind_mult,
emitter_sources,
particle_size,
num_emitters,
particle_life_range,
particles_per_second,
velocity_range,
angle_range,
scale_range,
emitter_misc_flags,
emitter_color,
})
}
#[derive(Debug, Clone)]
pub struct WaveSetData {
pub name: String,
pub water_height: f32,
pub water_tint: [f32; 4], pub opacity: f32,
pub max_length: f32,
pub geo_max_len: f32,
pub geo_min_len: f32,
pub geo_amp_over_len: f32,
pub geo_chop: f32,
pub geo_angle_dev: f32,
pub wind_dir: [f32; 3],
}
pub fn parse_wave_set(data: &[u8]) -> Result<WaveSetData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let num_bv = cursor.read_u32()?;
let mut flags = 0u32;
for i in 0..num_bv {
let w = cursor.read_u32()?;
if i == 0 { flags = w; }
}
let max_length = cursor.read_f32()?;
let geo_max_len = cursor.read_f32()?;
let geo_min_len = cursor.read_f32()?;
let geo_amp_over_len = cursor.read_f32()?;
let geo_chop = cursor.read_f32()?;
let geo_angle_dev = cursor.read_f32()?;
cursor.skip(5 * 4)?;
cursor.skip(4)?;
let wind_x = cursor.read_f32()?;
let wind_y = cursor.read_f32()?;
let wind_z = cursor.read_f32()?;
cursor.skip(3 * 4)?;
let water_height = cursor.read_f32()?;
cursor.skip(3 * 4)?;
cursor.skip(3 * 4)?;
cursor.skip(3 * 4)?;
cursor.skip(3 * 4)?;
cursor.skip(4)?;
cursor.skip(4 * 4)?;
cursor.skip(4 * 4)?;
cursor.skip(4 * 4)?;
let opacity = cursor.read_f32()?;
cursor.skip(4)?;
cursor.skip(2 * 4)?;
let r = cursor.read_f32()?;
let g = cursor.read_f32()?;
let b = cursor.read_f32()?;
let a = cursor.read_f32()?;
Ok(WaveSetData {
name,
water_height,
water_tint: [r, g, b, a],
opacity,
max_length,
geo_max_len,
geo_min_len,
geo_amp_over_len,
geo_chop,
geo_angle_dev,
wind_dir: [wind_x, wind_y, wind_z],
})
}
#[derive(Debug, Clone)]
pub struct SoundData {
pub name: String,
pub volume: f32,
pub looping: bool,
pub is_3d: bool,
pub auto_start: bool,
pub sound_type: u8, pub buffer_name: Option<String>,
pub position: [f32; 3],
pub min_falloff: f32,
pub max_falloff: f32,
pub soft_region: Option<String>,
}
pub fn parse_win32_sound(data: &[u8]) -> Result<SoundData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let _playing = cursor.read_u8()?; let _time = cursor.read_f32()?; cursor.skip(4)?; let max_falloff = cursor.read_i32()? as f32;
let min_falloff = cursor.read_i32()? as f32;
let _curr_volume = cursor.read_f32()?;
let desired_vol = cursor.read_f32()?;
let _outer_vol = cursor.read_i32()?;
let _inner_cone = cursor.read_i32()?;
let _outer_cone = cursor.read_i32()?;
let _faded_volume = cursor.read_f32()?;
let properties = cursor.read_u32()?;
let sound_type = cursor.read_u8()?;
let _priority = cursor.read_u8()?;
cursor.skip(4 + 4 + 4 + 1 + 4 + 4 + 4)?;
cursor.skip(4 + 4 + 4 + 1 + 4 + 4 + 4)?;
let soft_region_key = read_key_uoid(&mut cursor)?;
let soft_region = soft_region_key.map(|k| k.object_name);
let buffer_key = read_key_uoid(&mut cursor)?;
let buffer_name = buffer_key.map(|k| k.object_name);
let looping = properties & 0x00000004 != 0; let is_3d = properties & 0x00000001 != 0; let auto_start = properties & 0x00000008 != 0;
Ok(SoundData {
name,
volume: desired_vol,
looping,
is_3d,
auto_start,
sound_type,
buffer_name,
position: [0.0, 0.0, 0.0], min_falloff,
max_falloff,
soft_region,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_parse_one_shot_mods() {
let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !prp_path.exists() { return; }
let page = PrpPage::from_file(prp_path).unwrap();
let keys: Vec<_> = page.keys_of_type(crate::core::class_index::ClassIndex::PL_ONE_SHOT_MOD)
.iter().cloned().cloned().collect();
assert!(keys.len() >= 10, "Cleft should have 14+ OneShotMod objects, got {}", keys.len());
let mut ok = 0;
for key in &keys {
if let Some(data) = page.object_data(key) {
let osm = parse_one_shot_mod(data)
.unwrap_or_else(|e| panic!("Failed to parse OneShotMod '{}': {}", key.object_name, e));
assert!(!osm.anim_name.is_empty(), "OneShotMod '{}' has empty anim_name", key.object_name);
assert!(osm.seek_duration >= 0.0, "OneShotMod '{}' has negative seek_duration", key.object_name);
ok += 1;
}
}
assert_eq!(ok, keys.len(), "All OneShotMod objects should parse successfully");
}
#[test]
fn test_responder_oneshot_callbacks_parse() {
let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !prp_path.exists() { return; }
let page = PrpPage::from_file(prp_path).unwrap();
let mut ok = 0;
let mut err = 0;
for key in page.keys_of_type(crate::core::class_index::ClassIndex::PL_RESPONDER_MODIFIER) {
if let Some(data) = page.object_data(key) {
match parse_responder_modifier(data) {
Ok(resp) => {
let n_cmds: usize = resp.states.iter().map(|s| s.commands.len()).sum();
if n_cmds > 0 || resp.enabled {
ok += 1;
}
}
Err(_) => err += 1,
}
}
}
assert!(ok >= 80, "At least 80 responders should parse Ok, got {} (err={})", ok, err);
}
#[test]
fn test_px_physical_parse() {
let prp_path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !prp_path.exists() { return; }
let page = PrpPage::from_file(prp_path).unwrap();
let keys: Vec<_> = page.keys_of_type(crate::core::class_index::ClassIndex::PL_PXPHYSICAL)
.iter().cloned().cloned().collect();
assert!(keys.len() >= 50, "Cleft should have 100+ PXPhysical objects, got {}", keys.len());
let mut ok = 0;
let mut fail = 0;
let mut trimesh_count = 0;
let mut hull_count = 0;
let mut total_verts = 0;
for key in &keys {
if let Some(data) = page.object_data(key) {
match parse_px_physical(data) {
Ok(phys) => {
match &phys.shape {
PhysShapeData::TriMesh { vertices, indices } => {
trimesh_count += 1;
total_verts += vertices.len();
assert!(indices.len() % 3 == 0, "trimesh {} indices not multiple of 3", phys.name);
}
PhysShapeData::Hull { vertices } => {
hull_count += 1;
total_verts += vertices.len();
}
_ => {}
}
ok += 1;
}
Err(e) => {
eprintln!("FAIL: {} — {}", key.object_name, e);
fail += 1;
}
}
}
}
eprintln!("PXPhysical: {} ok, {} fail, {} trimesh, {} hull, {} total verts",
ok, fail, trimesh_count, hull_count, total_verts);
assert!(ok >= 100, "At least 100 PXPhysical should parse, got {} (fail={})", ok, fail);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysBoundsType {
Box = 1,
Sphere = 2,
Hull = 3,
Proxy = 4,
Explicit = 5,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysGroup {
Static = 0,
AvatarBlocker = 1,
DynamicBlocker = 2,
Avatar = 3,
Dynamic = 4,
Detector = 5,
LOSOnly = 6,
ExcludeRegion = 7,
Max = 255,
}
#[derive(Debug, Clone)]
pub enum PhysShapeData {
Sphere { radius: f32, offset: [f32; 3] },
Box { dimensions: [f32; 3], offset: [f32; 3] },
Hull { vertices: Vec<[f32; 3]> },
TriMesh { vertices: Vec<[f32; 3]>, indices: Vec<u32> },
}
#[derive(Debug, Clone)]
pub struct PxPhysicalData {
pub name: String,
pub mass: f32,
pub friction: f32,
pub restitution: f32,
pub bounds: PhysBoundsType,
pub group: PhysGroup,
pub reports_on: u32,
pub los_dbs: u16,
pub position: [f32; 3],
pub rotation: [f32; 4], pub shape: PhysShapeData,
}
fn read_phys_group(val: u8) -> PhysGroup {
match val {
0 => PhysGroup::Static,
1 => PhysGroup::AvatarBlocker,
2 => PhysGroup::DynamicBlocker,
3 => PhysGroup::Avatar,
4 => PhysGroup::Dynamic,
5 => PhysGroup::Detector,
6 => PhysGroup::LOSOnly,
7 => PhysGroup::ExcludeRegion,
_ => PhysGroup::Max,
}
}
fn read_phys_bounds(val: u8) -> Result<PhysBoundsType> {
match val {
1 => Ok(PhysBoundsType::Box),
2 => Ok(PhysBoundsType::Sphere),
3 => Ok(PhysBoundsType::Hull),
4 => Ok(PhysBoundsType::Proxy),
5 => Ok(PhysBoundsType::Explicit),
_ => bail!("Unknown bounds type: {}", val),
}
}
fn read_point3(reader: &mut impl std::io::Read) -> Result<[f32; 3]> {
let mut buf = [0u8; 12];
reader.read_exact(&mut buf)?;
Ok([
f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
])
}
fn read_quat(reader: &mut impl std::io::Read) -> Result<[f32; 4]> {
let mut buf = [0u8; 16];
reader.read_exact(&mut buf)?;
let mut q = [
f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
];
if q[0] == 0.0 && q[1] == 0.0 && q[2] == 0.0 && q[3] == 0.0 {
q[3] = 1.0;
}
Ok(q)
}
fn skip_bit_vector(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
let count = u32::from_le_bytes(buf) as usize;
if count > 0 {
let mut skip_buf = vec![0u8; count * 4];
reader.read_exact(&mut skip_buf)?;
}
Ok(())
}
fn read_bit_vector_words(reader: &mut impl std::io::Read) -> Result<Vec<u32>> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
let count = u32::from_le_bytes(buf) as usize;
let mut words = Vec::with_capacity(count);
for _ in 0..count {
reader.read_exact(&mut buf)?;
words.push(u32::from_le_bytes(buf));
}
Ok(words)
}
fn read_uncooked_hull(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let nverts = u32::from_le_bytes(buf4) as usize;
let mut vertices = Vec::with_capacity(nverts);
for _ in 0..nverts {
vertices.push(read_point3(reader)?);
}
Ok(PhysShapeData::Hull { vertices })
}
fn read_uncooked_trimesh(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let nverts = u32::from_le_bytes(buf4) as usize;
let mut vertices = Vec::with_capacity(nverts);
for _ in 0..nverts {
vertices.push(read_point3(reader)?);
}
reader.read_exact(&mut buf4)?;
let nfaces = u32::from_le_bytes(buf4) as usize;
let mut indices = Vec::with_capacity(nfaces * 3);
for _ in 0..(nfaces * 3) {
reader.read_exact(&mut buf4)?;
indices.push(u32::from_le_bytes(buf4));
}
Ok(PhysShapeData::TriMesh { vertices, indices })
}
fn skip_max_dependent_list(reader: &mut (impl std::io::Read + Seek), size: usize) -> Result<()> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let max_val = u32::from_le_bytes(buf4);
let bytes_per_elem = if max_val > 0xFFFF { 4 } else if max_val > 0xFF { 2 } else { 1 };
reader.seek(SeekFrom::Current((size * bytes_per_elem) as i64))?;
Ok(())
}
fn read_cooked_suffix(reader: &mut (impl std::io::Read + Seek)) -> Result<()> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let hbm_size = u32::from_le_bytes(buf4) as i64;
reader.seek(SeekFrom::Current(hbm_size))?;
reader.seek(SeekFrom::Current(11 * 4))?;
let mut fbuf = [0u8; 4];
reader.read_exact(&mut fbuf)?;
let val = f32::from_le_bytes(fbuf);
if val > -1.0 {
reader.seek(SeekFrom::Current(12 * 4))?;
}
Ok(())
}
fn read_cooked_hull(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
let mut tag = [0u8; 4];
let mut buf4 = [0u8; 4];
reader.read_exact(&mut tag)?;
if &tag != b"CVXM" { bail!("Expected CVXM, got {:?}", tag); }
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)?;
let num_verts = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let num_tris = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let unk2 = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let _unk3 = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let unk4 = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let _unk5 = u32::from_le_bytes(buf4) as usize;
let mut vertices = Vec::with_capacity(num_verts);
for _ in 0..num_verts {
vertices.push(read_point3(reader)?);
}
reader.read_exact(&mut buf4)?;
let max_vert_index = u32::from_le_bytes(buf4);
let idx_size = if max_vert_index > 0xFFFF { 4 } else if max_vert_index > 0xFF { 2 } else { 1 };
reader.seek(SeekFrom::Current((num_tris * 3 * idx_size) as i64))?;
reader.seek(SeekFrom::Current(2))?; reader.seek(SeekFrom::Current((num_verts * 2) as i64))?;
reader.seek(SeekFrom::Current(12))?; reader.seek(SeekFrom::Current((_unk3 * 36) as i64))?;
reader.seek(SeekFrom::Current(unk4 as i64))?;
skip_max_dependent_list(reader, unk4)?;
reader.seek(SeekFrom::Current(8))?; reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
skip_max_dependent_list(reader, unk2)?;
skip_max_dependent_list(reader, unk2)?;
skip_max_dependent_list(reader, unk2)?;
reader.seek(SeekFrom::Current((unk2 * 2) as i64))?;
reader.read_exact(&mut tag)?; reader.read_exact(&mut tag)?; reader.read_exact(&mut buf4)?;
reader.read_exact(&mut buf4)?;
let vale_unk1 = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let vale_unk2 = u32::from_le_bytes(buf4) as usize;
skip_max_dependent_list(reader, vale_unk1)?;
reader.seek(SeekFrom::Current(vale_unk2 as i64))?;
read_cooked_suffix(reader)?;
if num_verts > 0x20 {
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)?; reader.read_exact(&mut buf4)?;
let gaus_unk2 = u32::from_le_bytes(buf4) as usize;
reader.seek(SeekFrom::Current((gaus_unk2 * 2) as i64))?;
}
Ok(PhysShapeData::Hull { vertices })
}
fn read_cooked_trimesh(reader: &mut (impl std::io::Read + Seek)) -> Result<PhysShapeData> {
let mut tag = [0u8; 4];
let mut buf4 = [0u8; 4];
reader.read_exact(&mut tag)?;
if &tag != b"MESH" { bail!("Expected MESH, got {:?}", tag); }
reader.read_exact(&mut buf4)?;
reader.read_exact(&mut buf4)?;
let flags = u32::from_le_bytes(buf4);
reader.seek(SeekFrom::Current(4))?; reader.seek(SeekFrom::Current(4))?; reader.seek(SeekFrom::Current(4))?;
reader.read_exact(&mut buf4)?;
let num_verts = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let num_tris = u32::from_le_bytes(buf4) as usize;
let mut vertices = Vec::with_capacity(num_verts);
for _ in 0..num_verts {
vertices.push(read_point3(reader)?);
}
let mut indices = Vec::with_capacity(num_tris * 3);
for _ in 0..(num_tris * 3) {
let idx = if flags & 0x08 != 0 {
let mut b = [0u8; 1];
reader.read_exact(&mut b)?;
b[0] as u32
} else if flags & 0x10 != 0 {
let mut b = [0u8; 2];
reader.read_exact(&mut b)?;
u16::from_le_bytes(b) as u32
} else {
reader.read_exact(&mut buf4)?;
u32::from_le_bytes(buf4)
};
indices.push(idx);
}
if flags & 0x01 != 0 {
reader.seek(SeekFrom::Current((num_tris * 2) as i64))?;
}
if flags & 0x02 != 0 {
reader.read_exact(&mut buf4)?;
let max_val = u32::from_le_bytes(buf4);
let elem_size = if max_val > 0xFFFF { 4 } else if max_val > 0xFF { 2 } else { 1 };
reader.seek(SeekFrom::Current((num_tris * elem_size) as i64))?;
}
reader.read_exact(&mut buf4)?;
let num_convex_parts = u32::from_le_bytes(buf4) as usize;
reader.read_exact(&mut buf4)?;
let num_flat_parts = u32::from_le_bytes(buf4) as usize;
if num_convex_parts > 0 {
reader.seek(SeekFrom::Current((num_tris * 2) as i64))?;
}
if num_flat_parts > 0 {
let elem_size = if num_flat_parts > 0xFF { 2 } else { 1 };
reader.seek(SeekFrom::Current((num_tris * elem_size) as i64))?;
}
read_cooked_suffix(reader)?;
reader.read_exact(&mut buf4)?;
let extra = u32::from_le_bytes(buf4);
if extra != 0 {
reader.seek(SeekFrom::Current(num_tris as i64))?;
}
Ok(PhysShapeData::TriMesh { vertices, indices })
}
fn read_mesh_shape(reader: &mut (impl std::io::Read + Seek), is_hull: bool) -> Result<PhysShapeData> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if &magic == b"HSP\x01" {
if is_hull {
read_uncooked_hull(reader)
} else {
read_uncooked_trimesh(reader)
}
} else if &magic == b"NXS\x01" {
if is_hull {
read_cooked_hull(reader)
} else {
read_cooked_trimesh(reader)
}
} else {
bail!("Unknown mesh magic: {:02x}{:02x}{:02x}{:02x}", magic[0], magic[1], magic[2], magic[3]);
}
}
pub fn parse_px_physical(data: &[u8]) -> Result<PxPhysicalData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(&mut cursor)?;
let mass = cursor.read_f32()?;
let friction = cursor.read_f32()?;
let restitution = cursor.read_f32()?;
let bounds_raw = cursor.read_u8()?;
let group_raw = cursor.read_u8()?;
let reports_on = cursor.read_u32()?;
let los_dbs = cursor.read_u16()?;
let mut group = read_phys_group(group_raw);
if los_dbs == 0x0080 { group = PhysGroup::Max;
}
let bounds = read_phys_bounds(bounds_raw)?;
let _object_key = read_key_uoid(&mut cursor)?;
let _scene_node = read_key_uoid(&mut cursor)?;
let _world_key = read_key_uoid(&mut cursor)?;
let _sound_group = read_key_uoid(&mut cursor)?;
let position = read_point3(&mut cursor)?;
let rotation = read_quat(&mut cursor)?;
skip_bit_vector(&mut cursor)?;
let shape = match bounds {
PhysBoundsType::Sphere => {
let radius = cursor.read_f32()?;
let offset = read_point3(&mut cursor)?;
PhysShapeData::Sphere { radius, offset }
}
PhysBoundsType::Box => {
let dimensions = read_point3(&mut cursor)?;
let offset = read_point3(&mut cursor)?;
PhysShapeData::Box { dimensions, offset }
}
PhysBoundsType::Hull => {
read_mesh_shape(&mut cursor, true)?
}
PhysBoundsType::Proxy | PhysBoundsType::Explicit => {
read_mesh_shape(&mut cursor, false)?
}
};
Ok(PxPhysicalData {
name,
mass,
friction,
restitution,
bounds,
group,
reports_on,
los_dbs,
position,
rotation,
shape,
})
}
#[derive(Debug, Clone)]
pub struct VisRegionData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub region_key: Option<crate::core::uoid::Uoid>,
pub disable_normal: bool,
pub is_not: bool,
pub replace_normal: bool,
pub disabled: bool,
}
impl Default for VisRegionData {
fn default() -> Self {
Self {
self_key: None,
region_key: None,
disable_normal: false,
is_not: false,
replace_normal: true,
disabled: false,
}
}
}
pub fn parse_vis_region(data: &[u8]) -> Result<VisRegionData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable in plVisRegion"); }
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let _owner_key = read_key_uoid(&mut cursor)?;
let prop_words = read_bit_vector_words(&mut cursor)?;
let get_bit = |bit: usize| -> bool {
let word_idx = bit / 32;
let bit_idx = bit % 32;
word_idx < prop_words.len() && (prop_words[word_idx] & (1 << bit_idx)) != 0
};
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)?;
let _vis_mgr_key = read_key_uoid(&mut cursor)?;
Ok(VisRegionData {
self_key,
region_key,
disable_normal,
is_not,
replace_normal,
disabled,
})
}
#[derive(Debug, Clone)]
pub enum VolumeIsectData {
Convex {
planes: Vec<ConvexPlane>,
},
Sphere {
center: [f32; 3],
world_center: [f32; 3],
radius: f32,
mins: [f32; 3],
maxs: [f32; 3],
},
Cylinder {
top: [f32; 3],
bot: [f32; 3],
radius: f32,
world_bot: [f32; 3],
world_norm: [f32; 3],
length: f32,
min: f32,
max: f32,
},
Parallel {
planes: Vec<ParallelPlane>,
},
Cone {
capped: bool,
rad_angle: f32,
length: f32,
world_tip: [f32; 3],
world_norm: [f32; 3],
norms: Vec<[f32; 3]>,
dists: Vec<f32>,
},
}
#[derive(Debug, Clone)]
pub struct ConvexPlane {
pub norm: [f32; 3],
pub pos: [f32; 3],
pub dist: f32,
pub world_norm: [f32; 3],
pub world_dist: f32,
}
#[derive(Debug, Clone)]
pub struct ParallelPlane {
pub norm: [f32; 3],
pub min: f32,
pub max: f32,
pub pos_one: [f32; 3],
pub pos_two: [f32; 3],
}
fn read_volume_isect(reader: &mut (impl std::io::Read + Seek)) -> Result<Option<VolumeIsectData>> {
use crate::core::class_index::ClassIndex;
let class_idx = reader.read_u16()?;
if class_idx == 0x8000 {
return Ok(None); }
match class_idx {
ClassIndex::PL_CONVEX_ISECT => {
let n = reader.read_u16()? as usize;
let mut planes = Vec::with_capacity(n);
for _ in 0..n {
let norm = read_point3(reader)?;
let pos = read_point3(reader)?;
let dist = reader.read_f32()?;
let world_norm = read_point3(reader)?;
let world_dist = reader.read_f32()?;
planes.push(ConvexPlane { norm, pos, dist, world_norm, world_dist });
}
Ok(Some(VolumeIsectData::Convex { planes }))
}
ClassIndex::PL_SPHERE_ISECT => {
let center = read_point3(reader)?;
let world_center = read_point3(reader)?;
let radius = reader.read_f32()?;
let mins = read_point3(reader)?;
let maxs = read_point3(reader)?;
Ok(Some(VolumeIsectData::Sphere { center, world_center, radius, mins, maxs }))
}
ClassIndex::PL_CYLINDER_ISECT => {
let top = read_point3(reader)?;
let bot = read_point3(reader)?;
let radius = reader.read_f32()?;
let world_bot = read_point3(reader)?;
let world_norm = read_point3(reader)?;
let length = reader.read_f32()?;
let min = reader.read_f32()?;
let max = reader.read_f32()?;
Ok(Some(VolumeIsectData::Cylinder { top, bot, radius, world_bot, world_norm, length, min, max }))
}
ClassIndex::PL_PARALLEL_ISECT => {
let n = reader.read_u16()? as usize;
let mut planes = Vec::with_capacity(n);
for _ in 0..n {
let norm = read_point3(reader)?;
let min = reader.read_f32()?;
let max = reader.read_f32()?;
let pos_one = read_point3(reader)?;
let pos_two = read_point3(reader)?;
planes.push(ParallelPlane { norm, min, max, pos_one, pos_two });
}
Ok(Some(VolumeIsectData::Parallel { planes }))
}
ClassIndex::PL_CONE_ISECT => {
let capped = reader.read_u32()? != 0; let rad_angle = reader.read_f32()?;
let length = reader.read_f32()?;
let world_tip = read_point3(reader)?;
let world_norm = read_point3(reader)?;
let has_w2ndc = reader.read_u8()?;
if has_w2ndc != 0 { reader.skip(64)?; }
let has_l2ndc = reader.read_u8()?;
if has_l2ndc != 0 { reader.skip(64)?; }
let n = if capped { 5 } else { 4 };
let mut norms = Vec::with_capacity(n);
let mut dists = Vec::with_capacity(n);
for _ in 0..n {
norms.push(read_point3(reader)?);
dists.push(reader.read_f32()?);
}
Ok(Some(VolumeIsectData::Cone { capped, rad_angle, length, world_tip, world_norm, norms, dists }))
}
_ => {
bail!("Unknown plVolumeIsect subtype: 0x{:04X}", class_idx);
}
}
}
#[derive(Debug, Clone)]
pub enum SoftVolume {
Simple {
key: Option<crate::core::uoid::Uoid>,
inside_strength: f32,
outside_strength: f32,
soft_dist: f32,
bounds_min: [f32; 3],
bounds_max: [f32; 3],
disabled: bool,
},
Union {
key: Option<crate::core::uoid::Uoid>,
inside_strength: f32,
outside_strength: f32,
sub_keys: Vec<crate::core::uoid::Uoid>,
},
Intersect {
key: Option<crate::core::uoid::Uoid>,
inside_strength: f32,
outside_strength: f32,
sub_keys: Vec<crate::core::uoid::Uoid>,
},
Invert {
key: Option<crate::core::uoid::Uoid>,
inside_strength: f32,
outside_strength: f32,
sub_key: Option<crate::core::uoid::Uoid>,
},
}
impl SoftVolume {
pub fn key(&self) -> &Option<crate::core::uoid::Uoid> {
match self {
SoftVolume::Simple { key, .. } => key,
SoftVolume::Union { key, .. } => key,
SoftVolume::Intersect { key, .. } => key,
SoftVolume::Invert { key, .. } => key,
}
}
}
fn read_obj_interface_header(cursor: &mut Cursor<&[u8]>) -> Result<(Option<crate::core::uoid::Uoid>, bool)> {
use crate::core::uoid::read_key_uoid;
let class_idx = cursor.read_i16()?;
if class_idx < 0 { bail!("Null creatable in soft volume"); }
let self_key = read_key_uoid(cursor)?;
skip_synched_object(cursor)?;
let _owner_key = read_key_uoid(cursor)?;
let prop_words = read_bit_vector_words(cursor)?;
let disabled = !prop_words.is_empty() && (prop_words[0] & 1) != 0;
Ok((self_key, disabled))
}
fn read_soft_volume_base(cursor: &mut Cursor<&[u8]>) -> Result<(u32, f32, f32)> {
let listen_state = cursor.read_u32()?;
let inside_strength = cursor.read_f32()?;
let outside_strength = cursor.read_f32()?;
Ok((listen_state, inside_strength, outside_strength))
}
pub fn parse_soft_volume_simple(data: &[u8]) -> Result<(SoftVolume, Option<VolumeIsectData>)> {
let mut cursor = Cursor::new(data);
let (self_key, disabled) = read_obj_interface_header(&mut cursor)?;
let (_listen_state, inside_strength, outside_strength) = read_soft_volume_base(&mut cursor)?;
let soft_dist = cursor.read_f32()?;
let volume = read_volume_isect(&mut cursor)?;
let (bounds_min, bounds_max) = match &volume {
Some(VolumeIsectData::Convex { planes }) => compute_convex_bounds(planes),
Some(VolumeIsectData::Sphere { world_center, radius, .. }) => {
let r = *radius;
(
[world_center[0] - r, world_center[1] - r, world_center[2] - r],
[world_center[0] + r, world_center[1] + r, world_center[2] + r],
)
}
Some(VolumeIsectData::Cylinder { world_bot, world_norm, length, radius, .. }) => {
let r = *radius;
let top = [
world_bot[0] + world_norm[0] * length,
world_bot[1] + world_norm[1] * length,
world_bot[2] + world_norm[2] * length,
];
(
[
world_bot[0].min(top[0]) - r,
world_bot[1].min(top[1]) - r,
world_bot[2].min(top[2]) - r,
],
[
world_bot[0].max(top[0]) + r,
world_bot[1].max(top[1]) + r,
world_bot[2].max(top[2]) + r,
],
)
}
_ => ([f32::MIN, f32::MIN, f32::MIN], [f32::MAX, f32::MAX, f32::MAX]),
};
let sv = SoftVolume::Simple {
key: self_key,
inside_strength,
outside_strength,
soft_dist,
bounds_min,
bounds_max,
disabled,
};
Ok((sv, volume))
}
fn compute_convex_bounds(planes: &[ConvexPlane]) -> ([f32; 3], [f32; 3]) {
if planes.is_empty() {
return ([0.0; 3], [0.0; 3]);
}
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for p in planes {
for i in 0..3 {
min[i] = min[i].min(p.pos[i]);
max[i] = max[i].max(p.pos[i]);
}
}
(min, max)
}
fn parse_soft_volume_complex_base(data: &[u8]) -> Result<(
Option<crate::core::uoid::Uoid>,
f32, f32,
Vec<crate::core::uoid::Uoid>,
)> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let (self_key, _disabled) = read_obj_interface_header(&mut cursor)?;
let (_listen_state, inside_strength, outside_strength) = read_soft_volume_base(&mut cursor)?;
let n = cursor.read_u32()? as usize;
let mut sub_keys = Vec::with_capacity(n);
for _ in 0..n {
if let Some(uoid) = read_key_uoid(&mut cursor)? {
sub_keys.push(uoid);
}
}
Ok((self_key, inside_strength, outside_strength, sub_keys))
}
pub fn parse_soft_volume_union(data: &[u8]) -> Result<SoftVolume> {
let (self_key, inside_strength, outside_strength, sub_keys) =
parse_soft_volume_complex_base(data)?;
Ok(SoftVolume::Union { key: self_key, inside_strength, outside_strength, sub_keys })
}
pub fn parse_soft_volume_intersect(data: &[u8]) -> Result<SoftVolume> {
let (self_key, inside_strength, outside_strength, sub_keys) =
parse_soft_volume_complex_base(data)?;
Ok(SoftVolume::Intersect { key: self_key, inside_strength, outside_strength, sub_keys })
}
pub fn parse_soft_volume_invert(data: &[u8]) -> Result<SoftVolume> {
let (self_key, inside_strength, outside_strength, sub_keys) =
parse_soft_volume_complex_base(data)?;
let sub_key = sub_keys.into_iter().next();
Ok(SoftVolume::Invert { key: self_key, inside_strength, outside_strength, sub_key })
}
pub fn read_bit_vector(data: &[u8], offset: &mut usize) -> Vec<u32> {
if *offset + 4 > data.len() { return Vec::new(); }
let count = u32::from_le_bytes([data[*offset], data[*offset+1], data[*offset+2], data[*offset+3]]) as usize;
*offset += 4;
let mut words = Vec::with_capacity(count);
for _ in 0..count {
if *offset + 4 > data.len() { break; }
words.push(u32::from_le_bytes([data[*offset], data[*offset+1], data[*offset+2], data[*offset+3]]));
*offset += 4;
}
words
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecalManagerType {
Foot,
Ripple,
Puddle,
Bullet,
Wake,
Torpedo,
RippleVS,
TorpedoVS,
}
#[derive(Debug, Clone)]
pub struct DecalManagerData {
pub name: String,
pub manager_type: DecalManagerType,
pub mat_pre_shade: Option<String>,
pub mat_rt_shade: Option<String>,
pub target_names: Vec<String>,
pub max_num_verts: u32,
pub max_num_idx: u32,
pub wait_on_enable: u32,
pub intensity: f32,
pub wet_length: f32,
pub ramp_end: f32,
pub decay_start: f32,
pub life_span: f32,
pub grid_size_u: f32,
pub grid_size_v: f32,
pub scale: [f32; 3],
pub party_time: f32,
pub notify_names: Vec<String>,
pub init_uvw: Option<[f32; 3]>,
pub final_uvw: Option<[f32; 3]>,
pub wake_default_dir: Option<[f32; 3]>,
pub wake_anim_wgt: Option<f32>,
pub wake_vel_wgt: Option<f32>,
}
fn parse_dyna_decal_mgr_base(cursor: &mut Cursor<&[u8]>) -> Result<DecalManagerData> {
use crate::core::uoid::read_key_uoid;
let self_key = read_key_uoid(cursor)?;
let name = self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
skip_synched_object(cursor)?;
let mat_pre = read_key_name(cursor)?;
let mat_rt = read_key_name(cursor)?;
let num_targets = cursor.read_u32()?;
let mut target_names = Vec::new();
for _ in 0..num_targets { if let Some(n) = read_key_name(cursor)? { target_names.push(n); } }
let num_party = cursor.read_u32()?;
for _ in 0..num_party { let _ = read_key_name(cursor)?; }
let max_num_verts = cursor.read_u32()?;
let max_num_idx = cursor.read_u32()?;
let wait_on_enable = cursor.read_u32()?;
let intensity = cursor.read_f32()?;
let wet_length = cursor.read_f32()?;
let ramp_end = cursor.read_f32()?;
let decay_start = cursor.read_f32()?;
let life_span = cursor.read_f32()?;
let grid_size_u = cursor.read_f32()?;
let grid_size_v = cursor.read_f32()?;
let sx = cursor.read_f32()?; let sy = cursor.read_f32()?; let sz = cursor.read_f32()?;
let party_time = cursor.read_f32()?;
let num_notifies = cursor.read_u32()?;
let mut notify_names = Vec::new();
for _ in 0..num_notifies { if let Some(n) = read_key_name(cursor)? { notify_names.push(n); } }
Ok(DecalManagerData {
name, manager_type: DecalManagerType::Foot,
mat_pre_shade: mat_pre, mat_rt_shade: mat_rt, target_names,
max_num_verts, max_num_idx, wait_on_enable,
intensity, wet_length, ramp_end, decay_start, life_span,
grid_size_u, grid_size_v, scale: [sx, sy, sz], party_time, notify_names,
init_uvw: None, final_uvw: None,
wake_default_dir: None, wake_anim_wgt: None, wake_vel_wgt: None,
})
}
fn read_ripple_uvw(c: &mut Cursor<&[u8]>, m: &mut DecalManagerData) -> Result<()> {
let ix = c.read_f32()?; let iy = c.read_f32()?; let iz = c.read_f32()?;
m.init_uvw = Some([ix, iy, iz]);
let fx = c.read_f32()?; let fy = c.read_f32()?; let fz = c.read_f32()?;
m.final_uvw = Some([fx, fy, fz]);
Ok(())
}
pub fn parse_dyna_foot_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Foot; Ok(m)
}
pub fn parse_dyna_ripple_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Ripple;
read_ripple_uvw(&mut c, &mut m)?; Ok(m)
}
pub fn parse_dyna_bullet_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Bullet; Ok(m)
}
pub fn parse_dyna_puddle_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Puddle;
read_ripple_uvw(&mut c, &mut m)?; Ok(m)
}
pub fn parse_dyna_wake_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Wake;
read_ripple_uvw(&mut c, &mut m)?;
let dx = c.read_f32()?; let dy = c.read_f32()?; let dz = c.read_f32()?;
m.wake_default_dir = Some([dx, dy, dz]);
let ac = c.read_u16()?; if ac != 0x8000 { return Ok(m); }
m.wake_anim_wgt = Some(c.read_f32()?); m.wake_vel_wgt = Some(c.read_f32()?); Ok(m)
}
pub fn parse_dyna_torpedo_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::Torpedo;
read_ripple_uvw(&mut c, &mut m)?; Ok(m)
}
pub fn parse_dyna_ripple_vs_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::RippleVS;
read_ripple_uvw(&mut c, &mut m)?; let _ = read_key_name(&mut c)?; Ok(m)
}
pub fn parse_dyna_torpedo_vs_mgr(data: &[u8]) -> Result<DecalManagerData> {
let mut c = Cursor::new(data); let _ = c.read_i16()?;
let mut m = parse_dyna_decal_mgr_base(&mut c)?; m.manager_type = DecalManagerType::TorpedoVS;
read_ripple_uvw(&mut c, &mut m)?; let _ = read_key_name(&mut c)?; Ok(m)
}
#[derive(Debug, Clone)]
pub struct EaxListenerModData {
pub self_key: Option<crate::core::uoid::Uoid>,
pub soft_region_key: Option<String>,
pub environment: u32,
pub environment_size: f32,
pub environment_diffusion: f32,
pub room: i32,
pub room_hf: i32,
pub room_lf: i32,
pub decay_time: f32,
pub decay_hf_ratio: f32,
pub decay_lf_ratio: f32,
pub reflections: i32,
pub reflections_delay: f32,
pub reverb: i32,
pub reverb_delay: f32,
pub echo_time: f32,
pub echo_depth: f32,
pub modulation_time: f32,
pub modulation_depth: f32,
pub air_absorption_hf: f32,
pub hf_reference: f32,
pub lf_reference: f32,
pub room_rolloff_factor: f32,
pub flags: u32,
}
pub fn parse_eax_listener_mod(data: &[u8]) -> Result<EaxListenerModData> {
use crate::core::uoid::read_key_uoid;
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_i16()?;
let self_key = read_key_uoid(&mut cursor)?;
skip_synched_object(&mut cursor)?;
let num_bit_vectors = cursor.read_u32()?;
for _ in 0..num_bit_vectors {
let _word = cursor.read_u32()?;
}
let soft_region_uoid = read_key_uoid(&mut cursor)?;
let soft_region_key = soft_region_uoid.map(|u| u.object_name.clone());
let environment = cursor.read_u32()?;
let environment_size = cursor.read_f32()?;
let environment_diffusion = cursor.read_f32()?;
let room = cursor.read_i32()?;
let room_hf = cursor.read_i32()?;
let room_lf = cursor.read_i32()?;
let decay_time = cursor.read_f32()?;
let decay_hf_ratio = cursor.read_f32()?;
let decay_lf_ratio = cursor.read_f32()?;
let reflections = cursor.read_i32()?;
let reflections_delay = cursor.read_f32()?;
let reverb = cursor.read_i32()?;
let reverb_delay = cursor.read_f32()?;
let echo_time = cursor.read_f32()?;
let echo_depth = cursor.read_f32()?;
let modulation_time = cursor.read_f32()?;
let modulation_depth = cursor.read_f32()?;
let air_absorption_hf = cursor.read_f32()?;
let hf_reference = cursor.read_f32()?;
let lf_reference = cursor.read_f32()?;
let room_rolloff_factor = cursor.read_f32()?;
let flags = cursor.read_u32()?;
Ok(EaxListenerModData {
self_key,
soft_region_key,
environment,
environment_size,
environment_diffusion,
room,
room_hf,
room_lf,
decay_time,
decay_hf_ratio,
decay_lf_ratio,
reflections,
reflections_delay,
reverb,
reverb_delay,
echo_time,
echo_depth,
modulation_time,
modulation_depth,
air_absorption_hf,
hf_reference,
lf_reference,
room_rolloff_factor,
flags,
})
}
#[cfg(test)]
mod round_trip_tests {
use super::*;
use std::path::Path;
fn round_trip_file(path: &Path) -> Result<()> {
let original = std::fs::read(path)?;
let page = PrpPage::from_file(path)?;
let written = page.to_bytes()?;
if original != written {
let min_len = original.len().min(written.len());
for i in 0..min_len {
if original[i] != written[i] {
bail!(
"{}: first byte diff at offset 0x{:X} (orig={:#04X}, written={:#04X}), \
original={} bytes, written={} bytes",
path.display(), i, original[i], written[i],
original.len(), written.len()
);
}
}
if original.len() != written.len() {
bail!(
"{}: length mismatch: original={} bytes, written={} bytes",
path.display(), original.len(), written.len()
);
}
}
Ok(())
}
#[test]
fn test_round_trip_cleft() {
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() {
eprintln!("Skipping: {:?} not found", path);
return;
}
round_trip_file(path).unwrap();
eprintln!("Round-trip OK: Cleft_District_Cleft.prp");
}
#[test]
fn test_round_trip_all_ages() {
let dat_dir = Path::new("../../Plasma/staging/client/dat");
if !dat_dir.exists() {
eprintln!("Skipping: {:?} not found", dat_dir);
return;
}
let mut total = 0;
let mut passed = 0;
let mut failed = 0;
let mut failures: Vec<String> = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(dat_dir).unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "prp"))
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in &entries {
total += 1;
match round_trip_file(&entry.path()) {
Ok(()) => passed += 1,
Err(e) => {
failed += 1;
let msg = format!("{}", e);
if failures.len() < 10 {
failures.push(msg.clone());
}
eprintln!("FAIL: {}", msg);
}
}
}
eprintln!("\nRound-trip results: {}/{} passed, {} failed", passed, total, failed);
if failed > 0 {
panic!("{} PRP files failed round-trip. First failures:\n{}",
failed, failures.join("\n"));
}
}
}