wow_cdbc/
versions.rs

1//! Support for different DBC file versions
2
3use crate::{DbcHeader, Error, Result};
4use std::io::{Read, Seek, SeekFrom};
5
6/// DBC file format version
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DbcVersion {
9    /// Original WDBC format (World of Warcraft Classic)
10    WDBC,
11    /// World of Warcraft: The Burning Crusade
12    WDB2,
13    /// World of Warcraft: Wrath of the Lich King
14    WDB3,
15    /// World of Warcraft: Cataclysm
16    WDB4,
17    /// World of Warcraft: Mists of Pandaria
18    WDB5,
19}
20
21impl DbcVersion {
22    /// Detect the DBC version from a reader
23    pub fn detect<R: Read + Seek>(reader: &mut R) -> Result<Self> {
24        reader.seek(SeekFrom::Start(0))?;
25
26        let mut magic = [0u8; 4];
27        reader.read_exact(&mut magic)?;
28
29        match &magic {
30            b"WDBC" => Ok(DbcVersion::WDBC),
31            b"WDB2" => Ok(DbcVersion::WDB2),
32            b"WDB3" => Ok(DbcVersion::WDB3),
33            b"WDB4" => Ok(DbcVersion::WDB4),
34            b"WDB5" => Ok(DbcVersion::WDB5),
35            _ => Err(Error::InvalidHeader(format!(
36                "Unknown DBC version: {:?}",
37                std::str::from_utf8(&magic).unwrap_or("Invalid UTF-8")
38            ))),
39        }
40    }
41
42    /// Get the magic signature for this DBC version
43    pub fn magic(&self) -> [u8; 4] {
44        match self {
45            DbcVersion::WDBC => *b"WDBC",
46            DbcVersion::WDB2 => *b"WDB2",
47            DbcVersion::WDB3 => *b"WDB3",
48            DbcVersion::WDB4 => *b"WDB4",
49            DbcVersion::WDB5 => *b"WDB5",
50        }
51    }
52}
53
54/// WDB2 header (Cataclysm 4.0+)
55///
56/// The WDB2 format was introduced in Cataclysm and has two variants:
57/// - Basic header (build <= 12880): 28 bytes
58/// - Extended header (build > 12880): 48 bytes + optional index arrays
59///
60/// Reference: <https://wowdev.wiki/DB2>
61/// Reference: TrinityCore DB2StorageLoader.cpp
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Wdb2Header {
64    /// The magic signature, should be "WDB2"
65    pub magic: [u8; 4],
66    /// Number of records in the file
67    pub record_count: u32,
68    /// Number of fields in each record
69    pub field_count: u32,
70    /// Size of each record in bytes
71    pub record_size: u32,
72    /// Size of the string block in bytes
73    pub string_block_size: u32,
74    /// Table hash
75    pub table_hash: u32,
76    /// Build number
77    pub build: u32,
78    /// Timestamp (only present in extended header)
79    pub timestamp: u32,
80    // Extended header fields (build > 12880)
81    /// Minimum ID in the file
82    pub min_index: i32,
83    /// Maximum ID in the file
84    pub max_index: i32,
85    /// Locale flags
86    pub locale: i32,
87    /// Copy table size (unused in Cataclysm, always 0)
88    pub copy_table_size: u32,
89    /// Whether this header uses the extended format
90    pub has_extended_header: bool,
91    /// Size of the index array section (to be skipped)
92    pub index_array_size: u64,
93}
94
95impl Wdb2Header {
96    /// The size of a basic WDB2 header in bytes (build <= 12880)
97    pub const BASIC_SIZE: usize = 28;
98
99    /// The size of an extended WDB2 header in bytes (build > 12880)
100    pub const EXTENDED_SIZE: usize = 48;
101
102    /// Build number threshold for extended header format
103    pub const EXTENDED_BUILD_THRESHOLD: u32 = 12880;
104
105    /// Parse a WDB2 header from a reader
106    ///
107    /// This handles both the basic (build <= 12880) and extended (build > 12880) formats.
108    /// For builds > 12880, also skips the index arrays if max_index != 0.
109    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
110        // Ensure we're at the beginning of the file
111        reader.seek(SeekFrom::Start(0))?;
112
113        // Read the magic signature
114        let mut magic = [0u8; 4];
115        reader.read_exact(&mut magic)?;
116
117        // Validate the magic signature
118        if magic != *b"WDB2" {
119            return Err(Error::InvalidHeader(format!(
120                "Invalid magic signature: {:?}, expected: {:?}",
121                magic, b"WDB2"
122            )));
123        }
124
125        // Read the basic header fields
126        let mut buf = [0u8; 4];
127
128        reader.read_exact(&mut buf)?;
129        let record_count = u32::from_le_bytes(buf);
130
131        reader.read_exact(&mut buf)?;
132        let field_count = u32::from_le_bytes(buf);
133
134        reader.read_exact(&mut buf)?;
135        let record_size = u32::from_le_bytes(buf);
136
137        reader.read_exact(&mut buf)?;
138        let string_block_size = u32::from_le_bytes(buf);
139
140        reader.read_exact(&mut buf)?;
141        let table_hash = u32::from_le_bytes(buf);
142
143        reader.read_exact(&mut buf)?;
144        let build = u32::from_le_bytes(buf);
145
146        // Read timestamp (present in all WDB2 files)
147        reader.read_exact(&mut buf)?;
148        let timestamp = u32::from_le_bytes(buf);
149
150        // Check if this is an extended header (build > 12880)
151        let has_extended_header = build > Self::EXTENDED_BUILD_THRESHOLD;
152
153        let (min_index, max_index, locale, copy_table_size, index_array_size) =
154            if has_extended_header {
155                // Read extended header fields
156                reader.read_exact(&mut buf)?;
157                let min_index = i32::from_le_bytes(buf);
158
159                reader.read_exact(&mut buf)?;
160                let max_index = i32::from_le_bytes(buf);
161
162                reader.read_exact(&mut buf)?;
163                let locale = i32::from_le_bytes(buf);
164
165                reader.read_exact(&mut buf)?;
166                let copy_table_size = u32::from_le_bytes(buf);
167
168                // Calculate index array size to skip
169                let index_array_size = if max_index > 0 {
170                    let diff = (max_index - min_index + 1) as u64;
171                    // Index array: diff * 4 bytes (u32 per entry)
172                    // String length array: diff * 2 bytes (u16 per entry)
173                    diff * 4 + diff * 2
174                } else {
175                    0
176                };
177
178                // Skip the index arrays
179                if index_array_size > 0 {
180                    reader.seek(SeekFrom::Current(index_array_size as i64))?;
181                }
182
183                (
184                    min_index,
185                    max_index,
186                    locale,
187                    copy_table_size,
188                    index_array_size,
189                )
190            } else {
191                (0, 0, 0, 0, 0)
192            };
193
194        Ok(Self {
195            magic,
196            record_count,
197            field_count,
198            record_size,
199            string_block_size,
200            table_hash,
201            build,
202            timestamp,
203            min_index,
204            max_index,
205            locale,
206            copy_table_size,
207            has_extended_header,
208            index_array_size,
209        })
210    }
211
212    /// Convert to a standard DBC header
213    pub fn to_dbc_header(&self) -> DbcHeader {
214        DbcHeader {
215            magic: *b"WDBC", // Convert to WDBC format
216            record_count: self.record_count,
217            field_count: self.field_count,
218            record_size: self.record_size,
219            string_block_size: self.string_block_size,
220        }
221    }
222
223    /// Calculates the size of the header including any index arrays
224    pub fn header_size(&self) -> u64 {
225        if self.has_extended_header {
226            Self::EXTENDED_SIZE as u64 + self.index_array_size
227        } else {
228            Self::BASIC_SIZE as u64
229        }
230    }
231
232    /// Calculates the offset to the record data
233    pub fn record_data_offset(&self) -> u64 {
234        self.header_size()
235    }
236
237    /// Calculates the offset to the string block
238    pub fn string_block_offset(&self) -> u64 {
239        self.header_size() + (self.record_count as u64 * self.record_size as u64)
240    }
241
242    /// Calculates the total size of the WDB2 file
243    pub fn total_size(&self) -> u64 {
244        self.string_block_offset() + self.string_block_size as u64
245    }
246}
247
248/// WDB5 header (Mists of Pandaria)
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct Wdb5Header {
251    /// The magic signature, should be "WDB5"
252    pub magic: [u8; 4],
253    /// Number of records in the file
254    pub record_count: u32,
255    /// Number of fields in each record
256    pub field_count: u32,
257    /// Size of each record in bytes
258    pub record_size: u32,
259    /// Size of the string block in bytes
260    pub string_block_size: u32,
261    /// Table hash
262    pub table_hash: u32,
263    /// Layout hash
264    pub layout_hash: u32,
265    /// Min ID
266    pub min_id: u32,
267    /// Max ID
268    pub max_id: u32,
269    /// Locale
270    pub locale: u32,
271    /// Flags
272    pub flags: u16,
273    /// ID index
274    pub id_index: u16,
275}
276
277impl Wdb5Header {
278    /// The size of a WDB5 header in bytes
279    pub const SIZE: usize = 48;
280
281    /// Parse a WDB5 header from a reader
282    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
283        // Ensure we're at the beginning of the file
284        reader.seek(SeekFrom::Start(0))?;
285
286        // Read the magic signature
287        let mut magic = [0u8; 4];
288        reader.read_exact(&mut magic)?;
289
290        // Validate the magic signature
291        if magic != *b"WDB5" {
292            return Err(Error::InvalidHeader(format!(
293                "Invalid magic signature: {:?}, expected: {:?}",
294                magic, b"WDB5"
295            )));
296        }
297
298        // Read the rest of the header
299        let mut buf4 = [0u8; 4];
300        let mut buf2 = [0u8; 2];
301
302        reader.read_exact(&mut buf4)?;
303        let record_count = u32::from_le_bytes(buf4);
304
305        reader.read_exact(&mut buf4)?;
306        let field_count = u32::from_le_bytes(buf4);
307
308        reader.read_exact(&mut buf4)?;
309        let record_size = u32::from_le_bytes(buf4);
310
311        reader.read_exact(&mut buf4)?;
312        let string_block_size = u32::from_le_bytes(buf4);
313
314        reader.read_exact(&mut buf4)?;
315        let table_hash = u32::from_le_bytes(buf4);
316
317        reader.read_exact(&mut buf4)?;
318        let layout_hash = u32::from_le_bytes(buf4);
319
320        reader.read_exact(&mut buf4)?;
321        let min_id = u32::from_le_bytes(buf4);
322
323        reader.read_exact(&mut buf4)?;
324        let max_id = u32::from_le_bytes(buf4);
325
326        reader.read_exact(&mut buf4)?;
327        let locale = u32::from_le_bytes(buf4);
328
329        reader.read_exact(&mut buf2)?;
330        let flags = u16::from_le_bytes(buf2);
331
332        reader.read_exact(&mut buf2)?;
333        let id_index = u16::from_le_bytes(buf2);
334
335        Ok(Self {
336            magic,
337            record_count,
338            field_count,
339            record_size,
340            string_block_size,
341            table_hash,
342            layout_hash,
343            min_id,
344            max_id,
345            locale,
346            flags,
347            id_index,
348        })
349    }
350
351    /// Convert to a standard DBC header
352    pub fn to_dbc_header(&self) -> DbcHeader {
353        DbcHeader {
354            magic: *b"WDBC", // Convert to WDBC format
355            record_count: self.record_count,
356            field_count: self.field_count,
357            record_size: self.record_size,
358            string_block_size: self.string_block_size,
359        }
360    }
361
362    /// Calculates the offset to the string block
363    pub fn string_block_offset(&self) -> u64 {
364        Self::SIZE as u64 + (self.record_count as u64 * self.record_size as u64)
365    }
366
367    /// Calculates the total size of the WDB5 file
368    pub fn total_size(&self) -> u64 {
369        self.string_block_offset() + self.string_block_size as u64
370    }
371}