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 #[wow_data(expr = WDBC)]
23 WDBC,
24 #[wow_data(expr = WDB2)]
26 WDB2,
27 #[wow_data(expr = WDB3)]
29 WDB3,
30 #[wow_data(expr = WDB4)]
32 WDB4,
33 #[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}