wow_m2/chunks/
texture.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::io::{Read, Seek, Write};
3
4use crate::common::M2ArrayString;
5use crate::error::Result;
6use crate::version::M2Version;
7
8/// Texture type enum as defined in the M2 format
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum M2TextureType {
11    /// Regular texture
12    Hardcoded = 0,
13    /// Body + clothes
14    Body = 1,
15    /// Item, capes
16    Item = 2,
17    /// Weapon, armor (armorless)
18    WeaponArmorBasic = 3,
19    /// Weapon blade
20    WeaponBlade = 4,
21    /// Weapon handle
22    WeaponHandle = 5,
23    /// Environment
24    Environment = 6,
25    /// Hair, beard
26    Hair = 7,
27    /// Skin extra (accessories)
28    SkinExtra = 8,
29    /// Inventory art
30    UiSkin = 9,
31    /// Tauren mane
32    TaurenMane = 10,
33    /// Monster skin 1
34    Monster1 = 11,
35    /// Monster skin 2
36    Monster2 = 12,
37    /// Monster skin 3
38    Monster3 = 13,
39    /// Item icon
40    ItemIcon = 14,
41    /// Unknown
42    Unknown = 255,
43}
44
45impl M2TextureType {
46    /// Parse from integer value
47    pub fn from_u32(value: u32) -> Option<Self> {
48        match value {
49            0 => Some(Self::Hardcoded),
50            1 => Some(Self::Body),
51            2 => Some(Self::Item),
52            3 => Some(Self::WeaponArmorBasic),
53            4 => Some(Self::WeaponBlade),
54            5 => Some(Self::WeaponHandle),
55            6 => Some(Self::Environment),
56            7 => Some(Self::Hair),
57            8 => Some(Self::SkinExtra),
58            9 => Some(Self::UiSkin),
59            10 => Some(Self::TaurenMane),
60            11 => Some(Self::Monster1),
61            12 => Some(Self::Monster2),
62            13 => Some(Self::Monster3),
63            14 => Some(Self::ItemIcon),
64            _ => None,
65        }
66    }
67}
68
69bitflags::bitflags! {
70    /// Texture flags as defined in the M2 format
71    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
72    pub struct M2TextureFlags: u32 {
73        /// Texture is wrapped horizontally
74        const WRAP_X = 0x01;
75        /// Texture is wrapped vertically
76        const WRAP_Y = 0x02;
77        /// Texture will not be replaced by other textures
78        /// (character customization texture replacement)
79        const NOT_REPLACEABLE = 0x04;
80    }
81}
82
83/// Represents a texture in an M2 model
84#[derive(Debug, Clone)]
85pub struct M2Texture {
86    /// Type of the texture
87    pub texture_type: M2TextureType,
88    /// Flags for this texture
89    pub flags: M2TextureFlags,
90    /// Filename of the texture
91    pub filename: M2ArrayString,
92}
93
94impl M2Texture {
95    /// Parse a texture from a reader based on the M2 version
96    pub fn parse<R: Read + Seek>(reader: &mut R, _version: u32) -> Result<Self> {
97        let texture_type_raw = reader.read_u32_le()?;
98        let texture_type =
99            M2TextureType::from_u32(texture_type_raw).unwrap_or(M2TextureType::Unknown);
100
101        let flags = M2TextureFlags::from_bits_retain(reader.read_u32_le()?);
102        let filename = M2ArrayString::parse(reader)?;
103
104        Ok(Self {
105            texture_type,
106            flags,
107            filename,
108        })
109    }
110
111    /// Write a texture to a writer
112    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
113        writer.write_u32_le(self.texture_type as u32)?;
114        writer.write_u32_le(self.flags.bits())?;
115        self.filename.write(writer)?;
116
117        Ok(())
118    }
119
120    /// Convert this texture to a different version (no version differences for textures)
121    pub fn convert(&self, _target_version: M2Version) -> Self {
122        self.clone()
123    }
124
125    /// Create a new texture with the given type and filename
126    pub fn new(texture_type: M2TextureType, filename: M2ArrayString) -> Self {
127        Self {
128            texture_type,
129            flags: M2TextureFlags::empty(),
130            filename,
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use crate::common::{FixedString, M2Array};
138
139    use super::*;
140    use std::io::{Cursor, SeekFrom};
141
142    #[test]
143    fn test_texture_parse() {
144        let mut data = Vec::new();
145
146        let dummy = [0, 0, 0];
147        data.extend_from_slice(&dummy);
148
149        let filename_str = "test\0";
150        data.extend_from_slice(filename_str.as_bytes());
151
152        // Texture type (Body)
153        data.extend_from_slice(&1u32.to_le_bytes());
154
155        // Flags (WRAP_X | WRAP_Y)
156        data.extend_from_slice(&3u32.to_le_bytes());
157
158        data.extend_from_slice(&(filename_str.len() as u32).to_le_bytes());
159        data.extend_from_slice(&(dummy.len() as u32).to_le_bytes());
160
161        let mut cursor = Cursor::new(data);
162        cursor
163            .seek(SeekFrom::Start((filename_str.len() + dummy.len()) as u64))
164            .unwrap();
165        let texture =
166            M2Texture::parse(&mut cursor, M2Version::Vanilla.to_header_version()).unwrap();
167
168        assert_eq!(texture.texture_type, M2TextureType::Body);
169        assert_eq!(
170            texture.flags,
171            M2TextureFlags::WRAP_X | M2TextureFlags::WRAP_Y
172        );
173        assert_eq!(texture.filename.array.count, 5);
174        assert_eq!(texture.filename.array.offset, 3);
175    }
176
177    #[test]
178    fn test_texture_write() {
179        let texture = M2Texture {
180            texture_type: M2TextureType::Body,
181            flags: M2TextureFlags::WRAP_X | M2TextureFlags::WRAP_Y,
182            filename: M2ArrayString {
183                string: FixedString { data: Vec::new() },
184                array: M2Array::new(10, 0x100),
185            },
186        };
187
188        let mut data = Vec::new();
189        texture.write(&mut data).unwrap();
190
191        assert_eq!(
192            data,
193            [
194                // Texture type (Body)
195                1, 0, 0, 0, // Flags (WRAP_X | WRAP_Y)
196                3, 0, 0, 0, // Filename
197                10, 0, 0, 0, // count = 10
198                0, 1, 0, 0, // offset = 0x100
199            ]
200        );
201    }
202
203    #[test]
204    fn test_texture_conversion() {
205        let texture = M2Texture {
206            texture_type: M2TextureType::Body,
207            flags: M2TextureFlags::WRAP_X | M2TextureFlags::WRAP_Y,
208            filename: M2ArrayString {
209                string: FixedString { data: Vec::new() },
210                array: M2Array::new(10, 0x100),
211            },
212        };
213
214        // Convert to Cataclysm (should be identical since there are no version differences)
215        let converted = texture.convert(M2Version::Cataclysm);
216
217        assert_eq!(converted.texture_type, texture.texture_type);
218        assert_eq!(converted.flags, texture.flags);
219        assert_eq!(converted.filename.array.count, texture.filename.array.count);
220        assert_eq!(
221            converted.filename.array.offset,
222            texture.filename.array.offset
223        );
224    }
225}