Skip to main content

wow_m2/chunks/
event.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::io::{Read, Write};
3
4use crate::common::M2Array;
5use crate::error::Result;
6use crate::version::M2Version;
7
8/// Event types
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum M2EventType {
11    /// Play sound
12    Sound = 0,
13    /// Stop sound
14    SoundStop = 1,
15    /// Create a spell animation at a bone
16    SpellCastOMG = 2,
17    /// Hide a model
18    Hide = 3,
19    /// Unknown
20    Unknown4 = 4,
21    /// Footstep sound
22    StandOrLand = 5,
23    /// Used for displaying speech bubbles
24    SpeechEmote = 6,
25    /// Unknown
26    FootstepFront = 7,
27    /// Unknown
28    FootstepBack = 8,
29    /// Play a sound from a sound table
30    PlaySoundKitFromTable = 9,
31}
32
33impl M2EventType {
34    /// Parse from integer value
35    pub fn from_u32(value: u32) -> Option<Self> {
36        match value {
37            0 => Some(Self::Sound),
38            1 => Some(Self::SoundStop),
39            2 => Some(Self::SpellCastOMG),
40            3 => Some(Self::Hide),
41            4 => Some(Self::Unknown4),
42            5 => Some(Self::StandOrLand),
43            6 => Some(Self::SpeechEmote),
44            7 => Some(Self::FootstepFront),
45            8 => Some(Self::FootstepBack),
46            9 => Some(Self::PlaySoundKitFromTable),
47            _ => None,
48        }
49    }
50}
51
52/// Represents an event in an M2 model
53///
54/// Event structure (44 bytes):
55/// - identifier\[4\]: Event name string (e.g., "$CAH", "$CST", "$HIT")
56/// - data: u32: Sound/spell database ID
57/// - bone: i16: Bone index to attach event to
58/// - unknown: u16: Unknown field (possibly padding or submesh ID)
59/// - position: C3Vector (12 bytes): Position relative to bone
60/// - interp_type: u16: Interpolation type for animation
61/// - global_sequence: i16: Global sequence ID or -1
62/// - ranges: M2Array (8 bytes): Animation ranges
63/// - times: M2Array (8 bytes): Timestamp arrays
64#[derive(Debug, Clone)]
65pub struct M2Event {
66    /// Event identifier (4-char string like "$CAH", "$CST")
67    pub identifier: [u8; 4],
68    /// Event data (sound/spell database ID)
69    pub data: u32,
70    /// Bone to attach the event to
71    pub bone_index: i16,
72    /// Unknown field (possibly submesh ID or padding)
73    pub unknown: u16,
74    /// Position relative to bone
75    pub position: [f32; 3],
76    /// Interpolation type
77    pub interp_type: u16,
78    /// Global sequence ID or -1
79    pub global_sequence: i16,
80    /// Animation ranges (for per-animation timing)
81    pub ranges: M2Array<u32>,
82    /// Event timestamps
83    pub times: M2Array<u32>,
84}
85
86impl M2Event {
87    /// Parse an event from a reader based on the M2 version
88    ///
89    /// Event structure is 44 bytes for all versions.
90    pub fn parse<R: Read>(reader: &mut R, _version: u32) -> Result<Self> {
91        let mut identifier = [0u8; 4];
92        reader.read_exact(&mut identifier)?;
93
94        let data = reader.read_u32_le()?;
95        let bone_index = reader.read_i16_le()?;
96        let unknown = reader.read_u16_le()?;
97
98        let mut position = [0.0; 3];
99        for item in &mut position {
100            *item = reader.read_f32_le()?;
101        }
102
103        let interp_type = reader.read_u16_le()?;
104        let global_sequence = reader.read_i16_le()?;
105
106        let ranges = M2Array::parse(reader)?;
107        let times = M2Array::parse(reader)?;
108
109        Ok(Self {
110            identifier,
111            data,
112            bone_index,
113            unknown,
114            position,
115            interp_type,
116            global_sequence,
117            ranges,
118            times,
119        })
120    }
121
122    /// Write an event to a writer based on the M2 version
123    pub fn write<W: Write>(&self, writer: &mut W, _version: u32) -> Result<()> {
124        writer.write_all(&self.identifier)?;
125        writer.write_u32_le(self.data)?;
126        writer.write_i16_le(self.bone_index)?;
127        writer.write_u16_le(self.unknown)?;
128
129        for &pos in &self.position {
130            writer.write_f32_le(pos)?;
131        }
132
133        writer.write_u16_le(self.interp_type)?;
134        writer.write_i16_le(self.global_sequence)?;
135
136        self.ranges.write(writer)?;
137        self.times.write(writer)?;
138
139        Ok(())
140    }
141
142    /// Convert this event to a different version (no version differences for events)
143    pub fn convert(&self, _target_version: M2Version) -> Self {
144        self.clone()
145    }
146
147    /// Create a new event with default values
148    pub fn new(identifier: [u8; 4], bone_index: i16) -> Self {
149        Self {
150            identifier,
151            data: 0,
152            bone_index,
153            unknown: 0,
154            position: [0.0, 0.0, 0.0],
155            interp_type: 0,
156            global_sequence: -1,
157            ranges: M2Array::new(0, 0),
158            times: M2Array::new(0, 0),
159        }
160    }
161
162    /// Get the event identifier as a string
163    pub fn identifier_str(&self) -> String {
164        String::from_utf8_lossy(&self.identifier).to_string()
165    }
166
167    /// Returns the size of an event in bytes (always 44)
168    pub const fn size() -> usize {
169        44
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::io::Cursor;
177
178    #[test]
179    fn test_event_parse_write() {
180        let event = M2Event::new(*b"$CST", 25);
181
182        // Test write
183        let mut data = Vec::new();
184        event
185            .write(&mut data, M2Version::Vanilla.to_header_version())
186            .unwrap();
187
188        // Event should be 44 bytes
189        assert_eq!(data.len(), 44);
190
191        // Test parse
192        let mut cursor = Cursor::new(data);
193        let parsed = M2Event::parse(&mut cursor, M2Version::Vanilla.to_header_version()).unwrap();
194
195        assert_eq!(parsed.identifier, *b"$CST");
196        assert_eq!(parsed.bone_index, 25);
197        assert_eq!(parsed.global_sequence, -1);
198        assert_eq!(parsed.identifier_str(), "$CST");
199    }
200
201    #[test]
202    fn test_event_size() {
203        assert_eq!(M2Event::size(), 44);
204    }
205
206    #[test]
207    fn test_event_types() {
208        assert_eq!(M2EventType::from_u32(0), Some(M2EventType::Sound));
209        assert_eq!(M2EventType::from_u32(5), Some(M2EventType::StandOrLand));
210        assert_eq!(
211            M2EventType::from_u32(9),
212            Some(M2EventType::PlaySoundKitFromTable)
213        );
214        assert_eq!(M2EventType::from_u32(20), None);
215    }
216}