Skip to main content

wow_m2/chunks/
physics.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::io::{Read, Write};
3
4use crate::common::C3Vector;
5use crate::error::Result;
6use crate::version::M2Version;
7
8/// Physics simulation shape types
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum M2PhysicsShapeType {
11    /// No shape (physics disabled)
12    None = 0,
13    /// Sphere collision
14    Sphere = 1,
15    /// Capsule (cylinder with rounded ends)
16    Capsule = 2,
17    /// Plane (infinite flat surface)
18    Plane = 3,
19    /// Box (cuboid)
20    Box = 4,
21}
22
23impl M2PhysicsShapeType {
24    /// Parse from integer value
25    pub fn from_u8(value: u8) -> Option<Self> {
26        match value {
27            0 => Some(Self::None),
28            1 => Some(Self::Sphere),
29            2 => Some(Self::Capsule),
30            3 => Some(Self::Plane),
31            4 => Some(Self::Box),
32            _ => None,
33        }
34    }
35}
36
37bitflags::bitflags! {
38    /// Physics flags as defined in the M2 format
39    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
40    pub struct M2PhysicsFlags: u16 {
41        /// Use general collision
42        const GENERAL_COLLISION = 0x1;
43        /// Is this element a collision trigger
44        const COLLISION_TRIGGER = 0x2;
45        /// Unknown (added in WoD)
46        const UNKNOWN_WOD_1 = 0x4;
47        /// Unknown (added in WoD)
48        const UNKNOWN_WOD_2 = 0x8;
49        /// Unknown (added in WoD)
50        const UNKNOWN_WOD_3 = 0x10;
51        /// Causes vertices to inherit position from physics
52        const ANIMATED_BY_PHYSICS = 0x20;
53        /// Unknown
54        const UNKNOWN_0x40 = 0x40;
55        /// Has precise collision geometry
56        const PRECISE_COLLISION = 0x80;
57    }
58}
59
60/// Represents a physics joint between physics bodies
61#[derive(Debug, Clone)]
62pub struct M2PhysicsJoint {
63    /// First physics body
64    pub body1: u32,
65    /// Second physics body
66    pub body2: u32,
67    /// Joint types match Havok/PhysX standard enum
68    pub joint_type: u32,
69    /// Position of the joint (pivot point)
70    pub position: C3Vector,
71    /// Orientation of the joint (radians)
72    pub orientation: C3Vector,
73    /// Lower limits for rotation/movement
74    pub lower_limits: C3Vector,
75    /// Upper limits for rotation/movement
76    pub upper_limits: C3Vector,
77    /// Spring coefficients for the 3 axes
78    pub spring_coefficients: C3Vector,
79    /// Dampening coefficients for the 3 axes
80    pub damping_coefficients: C3Vector,
81}
82
83impl M2PhysicsJoint {
84    /// Parse a physics joint from a reader
85    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
86        let body1 = reader.read_u32_le()?;
87        let body2 = reader.read_u32_le()?;
88        let joint_type = reader.read_u32_le()?;
89
90        let position = C3Vector::parse(reader)?;
91        let orientation = C3Vector::parse(reader)?;
92        let lower_limits = C3Vector::parse(reader)?;
93        let upper_limits = C3Vector::parse(reader)?;
94        let spring_coefficients = C3Vector::parse(reader)?;
95        let damping_coefficients = C3Vector::parse(reader)?;
96
97        Ok(Self {
98            body1,
99            body2,
100            joint_type,
101            position,
102            orientation,
103            lower_limits,
104            upper_limits,
105            spring_coefficients,
106            damping_coefficients,
107        })
108    }
109
110    /// Write a physics joint to a writer
111    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
112        writer.write_u32_le(self.body1)?;
113        writer.write_u32_le(self.body2)?;
114        writer.write_u32_le(self.joint_type)?;
115
116        self.position.write(writer)?;
117        self.orientation.write(writer)?;
118        self.lower_limits.write(writer)?;
119        self.upper_limits.write(writer)?;
120        self.spring_coefficients.write(writer)?;
121        self.damping_coefficients.write(writer)?;
122
123        Ok(())
124    }
125}
126
127/// Represents a physics collision element
128#[derive(Debug, Clone)]
129pub struct M2PhysicsShape {
130    /// Shape type
131    pub shape_type: M2PhysicsShapeType,
132    /// Index of the bone this shape is attached to
133    pub bone_index: u16,
134    /// Physics flags
135    pub flags: M2PhysicsFlags,
136    /// Position relative to the bone
137    pub position: C3Vector,
138    /// Orientation (radians)
139    pub orientation: C3Vector,
140    /// Size parameters (interpretation depends on shape type)
141    pub dimensions: [f32; 5],
142}
143
144impl M2PhysicsShape {
145    /// Parse a physics shape from a reader
146    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
147        let shape_type_raw = reader.read_u8()?;
148        let shape_type =
149            M2PhysicsShapeType::from_u8(shape_type_raw).unwrap_or(M2PhysicsShapeType::None);
150
151        reader.read_u8()?; // Skip 1 byte of padding
152
153        let bone_index = reader.read_u16_le()?;
154        let flags = M2PhysicsFlags::from_bits_retain(reader.read_u16_le()?);
155
156        reader.read_u16_le()?; // Skip another 2 bytes of padding
157
158        let position = C3Vector::parse(reader)?;
159        let orientation = C3Vector::parse(reader)?;
160
161        let mut dimensions = [0.0; 5];
162        for item in &mut dimensions {
163            *item = reader.read_f32_le()?;
164        }
165
166        Ok(Self {
167            shape_type,
168            bone_index,
169            flags,
170            position,
171            orientation,
172            dimensions,
173        })
174    }
175
176    /// Write a physics shape to a writer
177    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
178        writer.write_u8(self.shape_type as u8)?;
179        writer.write_u8(0)?; // Write padding
180
181        writer.write_u16_le(self.bone_index)?;
182        writer.write_u16_le(self.flags.bits())?;
183        writer.write_u16_le(0)?; // Write more padding
184
185        self.position.write(writer)?;
186        self.orientation.write(writer)?;
187
188        for &dim in &self.dimensions {
189            writer.write_f32_le(dim)?;
190        }
191
192        Ok(())
193    }
194
195    /// Get the size of this physics shape in bytes
196    pub fn size_in_bytes() -> usize {
197        2 + // shape_type + padding
198        2 + // bone_index
199        2 + // flags
200        2 + // more padding
201        3 * 4 + // position
202        3 * 4 + // orientation
203        5 * 4 // dimensions
204    }
205}
206
207/// Represents the physics data section of an M2 model
208/// Introduced in Mists of Pandaria (5.x)
209#[derive(Debug, Clone)]
210pub struct M2PhysicsData {
211    /// Physics collision shapes
212    pub shapes: Vec<M2PhysicsShape>,
213    /// Physics bodies
214    pub bodies: Vec<u32>,
215    /// Physics joints
216    pub joints: Vec<M2PhysicsJoint>,
217}
218
219impl M2PhysicsData {
220    /// Parse physics data from a reader based on the M2 version
221    pub fn parse<R: Read + std::io::Seek>(reader: &mut R, version: u32) -> Result<Self> {
222        // Physics data was introduced in MoP (5.x)
223        if let Some(m2_version) = M2Version::from_header_version(version)
224            && m2_version < M2Version::MoP
225        {
226            return Ok(Self {
227                shapes: Vec::new(),
228                bodies: Vec::new(),
229                joints: Vec::new(),
230            });
231        }
232
233        let shapes_count = reader.read_u32_le()?;
234        let shapes_offset = reader.read_u32_le()?;
235
236        let bodies_count = reader.read_u32_le()?;
237        let bodies_offset = reader.read_u32_le()?;
238
239        let joints_count = reader.read_u32_le()?;
240        let joints_offset = reader.read_u32_le()?;
241
242        // Parse physics shapes
243        let mut shapes = Vec::with_capacity(shapes_count as usize);
244        if shapes_count > 0 {
245            reader.seek(std::io::SeekFrom::Start(shapes_offset as u64))?;
246            for _ in 0..shapes_count {
247                shapes.push(M2PhysicsShape::parse(reader)?);
248            }
249        }
250
251        // Parse physics bodies
252        let mut bodies = Vec::with_capacity(bodies_count as usize);
253        if bodies_count > 0 {
254            reader.seek(std::io::SeekFrom::Start(bodies_offset as u64))?;
255            for _ in 0..bodies_count {
256                bodies.push(reader.read_u32_le()?);
257            }
258        }
259
260        // Parse physics joints
261        let mut joints = Vec::with_capacity(joints_count as usize);
262        if joints_count > 0 {
263            reader.seek(std::io::SeekFrom::Start(joints_offset as u64))?;
264            for _ in 0..joints_count {
265                joints.push(M2PhysicsJoint::parse(reader)?);
266            }
267        }
268
269        Ok(Self {
270            shapes,
271            bodies,
272            joints,
273        })
274    }
275
276    /// Write physics data to a writer based on the M2 version
277    pub fn write<W: Write + std::io::Seek>(&self, writer: &mut W, version: u32) -> Result<()> {
278        // Physics data was introduced in MoP (5.x)
279        if let Some(m2_version) = M2Version::from_header_version(version)
280            && m2_version < M2Version::MoP
281        {
282            return Ok(());
283        }
284
285        let header_pos = writer.stream_position()?;
286
287        // Write placeholders for counts and offsets
288        writer.write_u32_le(self.shapes.len() as u32)?;
289        writer.write_u32_le(0)?; // shapes_offset placeholder
290
291        writer.write_u32_le(self.bodies.len() as u32)?;
292        writer.write_u32_le(0)?; // bodies_offset placeholder
293
294        writer.write_u32_le(self.joints.len() as u32)?;
295        writer.write_u32_le(0)?; // joints_offset placeholder
296
297        // Write shapes
298        if !self.shapes.is_empty() {
299            let shapes_offset = writer.stream_position()?;
300
301            // Update shapes offset in header
302            writer.seek(std::io::SeekFrom::Start(header_pos + 4))?;
303            writer.write_u32_le(shapes_offset as u32)?;
304            writer.seek(std::io::SeekFrom::Start(shapes_offset))?;
305
306            for shape in &self.shapes {
307                shape.write(writer)?;
308            }
309        }
310
311        // Write bodies
312        if !self.bodies.is_empty() {
313            let bodies_offset = writer.stream_position()?;
314
315            // Update bodies offset in header
316            writer.seek(std::io::SeekFrom::Start(header_pos + 12))?;
317            writer.write_u32_le(bodies_offset as u32)?;
318            writer.seek(std::io::SeekFrom::Start(bodies_offset))?;
319
320            for &body in &self.bodies {
321                writer.write_u32_le(body)?;
322            }
323        }
324
325        // Write joints
326        if !self.joints.is_empty() {
327            let joints_offset = writer.stream_position()?;
328
329            // Update joints offset in header
330            writer.seek(std::io::SeekFrom::Start(header_pos + 20))?;
331            writer.write_u32_le(joints_offset as u32)?;
332            writer.seek(std::io::SeekFrom::Start(joints_offset))?;
333
334            for joint in &self.joints {
335                joint.write(writer)?;
336            }
337        }
338
339        Ok(())
340    }
341
342    /// Convert this physics data to a different version
343    pub fn convert(&self, source_version: M2Version, target_version: M2Version) -> Self {
344        if target_version < M2Version::MoP {
345            // Remove physics data for pre-MoP versions
346            Self {
347                shapes: Vec::new(),
348                bodies: Vec::new(),
349                joints: Vec::new(),
350            }
351        } else if source_version < M2Version::MoP {
352            // If we're upgrading to MoP+ from a version without physics,
353            // we just return an empty physics data structure
354            self.clone()
355        } else {
356            // No changes needed for MoP+ to MoP+ conversions
357            self.clone()
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use std::io::Cursor;
366
367    #[test]
368    fn test_physics_shape_parse_write() {
369        let shape = M2PhysicsShape {
370            shape_type: M2PhysicsShapeType::Sphere,
371            bone_index: 1,
372            flags: M2PhysicsFlags::GENERAL_COLLISION,
373            position: C3Vector {
374                x: 1.0,
375                y: 2.0,
376                z: 3.0,
377            },
378            orientation: C3Vector {
379                x: 0.0,
380                y: 0.0,
381                z: 0.0,
382            },
383            dimensions: [1.0, 0.0, 0.0, 0.0, 0.0], // Sphere radius is first dimension
384        };
385
386        let mut data = Vec::new();
387        shape.write(&mut data).unwrap();
388
389        let mut cursor = Cursor::new(data);
390        let parsed_shape = M2PhysicsShape::parse(&mut cursor).unwrap();
391
392        assert_eq!(parsed_shape.shape_type, M2PhysicsShapeType::Sphere);
393        assert_eq!(parsed_shape.bone_index, 1);
394        assert_eq!(parsed_shape.flags, M2PhysicsFlags::GENERAL_COLLISION);
395        assert_eq!(parsed_shape.position.x, 1.0);
396        assert_eq!(parsed_shape.position.y, 2.0);
397        assert_eq!(parsed_shape.position.z, 3.0);
398        assert_eq!(parsed_shape.dimensions[0], 1.0); // Sphere radius
399    }
400
401    #[test]
402    fn test_physics_joint_parse_write() {
403        let joint = M2PhysicsJoint {
404            body1: 0,
405            body2: 1,
406            joint_type: 2, // 2 = hinge joint
407            position: C3Vector {
408                x: 1.0,
409                y: 2.0,
410                z: 3.0,
411            },
412            orientation: C3Vector {
413                x: 0.0,
414                y: 0.0,
415                z: 0.0,
416            },
417            lower_limits: C3Vector {
418                x: -1.0,
419                y: -1.0,
420                z: -1.0,
421            },
422            upper_limits: C3Vector {
423                x: 1.0,
424                y: 1.0,
425                z: 1.0,
426            },
427            spring_coefficients: C3Vector {
428                x: 0.0,
429                y: 0.0,
430                z: 0.0,
431            },
432            damping_coefficients: C3Vector {
433                x: 0.5,
434                y: 0.5,
435                z: 0.5,
436            },
437        };
438
439        let mut data = Vec::new();
440        joint.write(&mut data).unwrap();
441
442        let mut cursor = Cursor::new(data);
443        let parsed_joint = M2PhysicsJoint::parse(&mut cursor).unwrap();
444
445        assert_eq!(parsed_joint.body1, 0);
446        assert_eq!(parsed_joint.body2, 1);
447        assert_eq!(parsed_joint.joint_type, 2);
448        assert_eq!(parsed_joint.position.x, 1.0);
449        assert_eq!(parsed_joint.position.y, 2.0);
450        assert_eq!(parsed_joint.position.z, 3.0);
451        assert_eq!(parsed_joint.lower_limits.x, -1.0);
452        assert_eq!(parsed_joint.upper_limits.x, 1.0);
453        assert_eq!(parsed_joint.damping_coefficients.x, 0.5);
454    }
455}