wow_alchemy_cdbc/
wdb.rs

1use std::collections::HashMap;
2use std::io::SeekFrom;
3
4use custom_debug::Debug;
5use wow_alchemy_data::error::Result as WDResult;
6use wow_alchemy_data::{prelude::*, types::MagicStr};
7use wow_alchemy_data_derive::{WowEnumFrom, WowHeaderR, WowHeaderW};
8use wow_alchemy_utils::debug;
9
10pub const WDBC: MagicStr = *b"WDBC";
11pub const WDB2: MagicStr = *b"WDB2";
12pub const WDB3: MagicStr = *b"WDB3";
13pub const WDB4: MagicStr = *b"WDB4";
14pub const WDB5: MagicStr = *b"WDB5";
15
16#[derive(
17    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, WowEnumFrom, WowHeaderR, WowHeaderW,
18)]
19#[wow_data(from_type=MagicStr)]
20pub enum DbcVersion {
21    /// Original WDBC format, up to Legion or possibly WoD
22    #[wow_data(expr = WDBC)]
23    WDBC,
24    /// V2, briefly used in Legion or maybe in WoD
25    #[wow_data(expr = WDB2)]
26    WDB2,
27    /// V3, briefly used in Legion
28    #[wow_data(expr = WDB3)]
29    WDB3,
30    /// V4, briefly used in Legion
31    #[wow_data(expr = WDB4)]
32    WDB4,
33    /// V5, used in Legion+
34    #[wow_data(expr = WDB5)]
35    #[default]
36    WDB5,
37}
38
39impl DataVersion for DbcVersion {}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, WowHeaderR, WowHeaderW)]
42#[wow_data(version = DbcVersion)]
43pub enum Wdb2Fields {
44    None,
45
46    #[wow_data(read_if = version >= DbcVersion::WDB2)]
47    Fields {
48        table_hash: u32,
49        build: u32,
50        timestamp_last_written: u32,
51        min_id: u32,
52        max_id: u32,
53        locale: u32,
54        copy_table_size: u32,
55    },
56}
57
58impl Default for Wdb2Fields {
59    fn default() -> Self {
60        Self::Fields {
61            table_hash: 0,
62            build: 0,
63            timestamp_last_written: 0,
64            min_id: 0,
65            max_id: 0,
66            locale: 0,
67            copy_table_size: 0,
68        }
69    }
70}
71
72#[derive(Debug, Clone, WowHeaderR, WowHeaderW)]
73#[wow_data(version = DbcVersion)]
74pub enum VGTE4<T: Default + WowHeaderR + WowHeaderW> {
75    None,
76
77    #[wow_data(read_if = version >= DbcVersion::WDB4)]
78    Some(T),
79}
80
81impl<T: Default + WowHeaderR + WowHeaderW> Default for VGTE4<T> {
82    fn default() -> Self {
83        Self::Some(T::default())
84    }
85}
86
87#[derive(Debug, Clone, WowHeaderR, WowHeaderW)]
88#[wow_data(version = DbcVersion)]
89pub enum VGTE5<T: Default + WowHeaderR + WowHeaderW> {
90    None,
91
92    #[wow_data(read_if = version >= DbcVersion::WDB4)]
93    Some(T),
94}
95
96impl<T: Default + WowHeaderR + WowHeaderW> Default for VGTE5<T> {
97    fn default() -> Self {
98        Self::Some(T::default())
99    }
100}
101
102#[derive(Debug, Clone, Default, WowHeaderR, WowHeaderW)]
103#[wow_data(version = DbcVersion)]
104pub struct WdbHeader {
105    pub record_count: u32,
106    pub field_count: u32,
107    pub record_size: u32,
108    pub string_block_size: u32,
109    #[wow_data(versioned)]
110    pub wdb2: Wdb2Fields,
111    #[wow_data(versioned)]
112    pub flags: VGTE4<u32>,
113    #[wow_data(versioned)]
114    pub id_index: VGTE5<u32>,
115}
116
117impl WdbHeader {
118    pub fn string_offset(&self) -> u64 {
119        ((4 + self.wow_size()) as u32 + (self.record_size * self.record_count)) as u64
120    }
121}
122
123#[derive(Debug)]
124pub struct WdbFile {
125    pub version: DbcVersion,
126    pub header: WdbHeader,
127    pub header_size: usize,
128    #[debug(with = debug::trimmed_collection_fmt)]
129    pub strings: Vec<String>,
130    #[debug(skip)]
131    pub string_pos: HashMap<usize, usize>,
132}
133
134impl WdbFile {
135    pub fn wow_read<R: Read + Seek>(reader: &mut R) -> WDResult<Self> {
136        reader.seek(SeekFrom::Start(0))?;
137        let version: DbcVersion = MagicStr::wow_read(reader)?.try_into()?;
138
139        let header: WdbHeader = reader.wow_read_versioned(version)?;
140
141        let header_size = version.wow_size() + header.wow_size();
142
143        let string_block_offset = header_size as u32 + (header.record_count * header.record_size);
144
145        reader.seek(SeekFrom::Start(string_block_offset as u64))?;
146
147        let mut string_block = vec![0u8; header.string_block_size as usize];
148        reader.read_exact(&mut string_block)?;
149
150        let strings: Vec<String> = string_block
151            .split(|i| *i == 0)
152            .map(|v| String::from_utf8_lossy(v).into())
153            .collect();
154
155        let mut string_pos = HashMap::new();
156        let mut current_pos = 0;
157        for (idx, item) in strings.iter().enumerate() {
158            string_pos.insert(current_pos, idx);
159            current_pos += item.len() + 1;
160        }
161
162        Ok(Self {
163            version,
164            header,
165            header_size,
166            strings,
167            string_pos,
168        })
169    }
170
171    pub fn get_string(&self, index: usize) -> Option<&String> {
172        self.strings.get(index)
173    }
174
175    pub fn get_string_by_offset(&self, offset: usize) -> Option<&String> {
176        if let Some(item) = self.string_pos.get(&offset) {
177            self.get_string(*item)
178        } else {
179            None
180        }
181    }
182
183    pub fn records_start_offset(&self) -> u64 {
184        self.header_size as u64
185    }
186}