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