Skip to main content

wow_alchemy_cdbc/dbd/
mod.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use crate::{Error, Result};
6
7pub mod download;
8pub mod file_map;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct GameBuild {
12    pub major: u32,
13    pub minor: u32,
14    pub patch: u32,
15    pub build: u32,
16}
17
18impl TryFrom<&str> for GameBuild {
19    type Error = Error;
20
21    fn try_from(value: &str) -> Result<Self> {
22        let parts = value.split(&".");
23        let mut vals: [u32; 4] = [0, 0, 0, 0];
24        let mut count = 0;
25
26        for part in parts {
27            let Ok(val) = part.parse() else {
28                return Err(Error::GameBuild(format!(
29                    "can't convert string {} to game build",
30                    value
31                )));
32            };
33            vals[count] = val;
34
35            count += 1;
36            if count > 4 {
37                return Err(Error::GameBuild(format!(
38                    "can't convert string {} to game build",
39                    value
40                )));
41            }
42        }
43
44        Ok(Self {
45            major: vals[0],
46            minor: vals[1],
47            patch: vals[2],
48            build: vals[3],
49        })
50    }
51}
52
53#[derive(Debug, Clone)]
54pub struct DbdColumn {
55    pub name: String,
56    pub base_type: String,
57    pub foreign_key: Option<ForeignKey>,
58    pub comment: Option<String>,
59    pub is_optional: bool,
60}
61
62#[derive(Debug, Clone)]
63pub struct ForeignKey {
64    pub table: String,
65    pub field: String,
66}
67
68#[derive(Debug, Clone)]
69pub struct DbdField {
70    pub name: String,
71    pub type_size: TypeSize,
72    pub is_array: bool,
73    pub array_size: Option<usize>,
74    pub is_key: bool,
75    pub is_relation: bool,
76    pub is_noninline: bool,
77}
78
79#[derive(Debug, Clone, PartialEq)]
80pub enum TypeSize {
81    Unspecified,
82    Int8,
83    UInt8,
84    Int16,
85    UInt16,
86    Int32,
87    UInt32,
88    Int64,
89    UInt64,
90}
91
92impl TypeSize {
93    pub fn parse_type_size(s: &str) -> Self {
94        match s {
95            "8" => TypeSize::Int8,
96            "u8" => TypeSize::UInt8,
97            "16" => TypeSize::Int16,
98            "u16" => TypeSize::UInt16,
99            "32" => TypeSize::Int32,
100            "u32" => TypeSize::UInt32,
101            "64" => TypeSize::Int64,
102            "u64" => TypeSize::UInt64,
103            _ => TypeSize::Unspecified,
104        }
105    }
106
107    pub fn to_type_name(&self, base_type: &str) -> &'static str {
108        match self {
109            TypeSize::Int8 => "Int8",
110            TypeSize::UInt8 => "UInt8",
111            TypeSize::Int16 => "Int16",
112            TypeSize::UInt16 => "UInt16",
113            TypeSize::Int32 => "Int32",
114            TypeSize::UInt32 => "UInt32",
115            TypeSize::Int64 => "Int64",
116            TypeSize::UInt64 => "UInt64",
117            TypeSize::Unspecified => match base_type {
118                "float" => "Float32",
119                "string" | "locstring" => "String",
120                _ => "UInt32",
121            },
122        }
123    }
124}
125
126#[derive(Debug, Clone)]
127pub struct DbdBuild {
128    pub versions: Vec<GameBuildSpec>,
129    pub fields: Vec<DbdField>,
130}
131
132#[derive(Debug, Clone)]
133pub struct DbdFile {
134    pub columns: HashMap<String, DbdColumn>,
135    pub build: DbdBuild,
136}
137
138pub fn parse_dbd_file(game_build: &GameBuild, path: &Path) -> Result<DbdFile> {
139    let content = fs::read_to_string(path)?;
140    parse_dbd_content(game_build, &content)
141}
142
143#[derive(Debug, Clone)]
144pub enum GameBuildSpec {
145    Single(GameBuild),
146    Range((GameBuild, GameBuild)),
147}
148
149pub fn parse_dbd_content(game_build: &GameBuild, content: &str) -> Result<DbdFile> {
150    let mut columns = HashMap::new();
151
152    let mut current_section = None;
153    let mut build_state = 0;
154    let mut current_build_versions = Vec::new();
155    let mut current_build_fields = Vec::new();
156
157    for line in content.lines() {
158        let line = line.trim();
159
160        if line.is_empty() {
161            continue;
162        }
163
164        if line == "COLUMNS" {
165            current_section = Some("COLUMNS");
166            continue;
167        } else if let Some(stripped) = line.strip_prefix("BUILD ") {
168            if build_state == 2 {
169                break;
170            }
171            current_section = Some("BUILD");
172            let versions: Vec<String> = stripped.split(", ").map(|s| s.to_string()).collect();
173            for version in versions {
174                match version.split_once("-") {
175                    Some((a, b)) => {
176                        let current_build_a: GameBuild = a.try_into()?;
177                        let current_build_b: GameBuild = b.try_into()?;
178                        if *game_build >= current_build_a && *game_build <= current_build_b {
179                            build_state = 1;
180                        }
181                        current_build_versions
182                            .push(GameBuildSpec::Range((current_build_a, current_build_b)));
183                    }
184                    None => {
185                        let current_build: GameBuild = version.as_str().try_into()?;
186                        if current_build == *game_build {
187                            build_state = 1;
188                        }
189                        current_build_versions.push(GameBuildSpec::Single(current_build));
190                    }
191                }
192            }
193            continue;
194        } else if line.strip_prefix("LAYOUT ").is_some() {
195            if build_state == 2 {
196                break;
197            }
198            continue;
199        } else if line.strip_prefix("COMMENT ").is_some() {
200            continue;
201        }
202
203        match current_section {
204            Some("COLUMNS") => {
205                if let Some(column) = parse_column_line(line) {
206                    columns.insert(column.name.trim_end_matches("?").into(), column);
207                }
208            }
209            Some("BUILD") if build_state >= 1 => {
210                build_state = 2;
211                let field = parse_field_line(line);
212                current_build_fields.push(field);
213            }
214            _ => {
215                current_build_versions = Vec::new();
216            }
217        }
218    }
219
220    if current_build_fields.is_empty() {
221        println!("{content}");
222        return Err(Error::NoFieldsForBuild);
223    }
224
225    Ok(DbdFile {
226        columns,
227        build: DbdBuild {
228            versions: current_build_versions,
229            fields: current_build_fields,
230        },
231    })
232}
233
234fn parse_column_line(line: &str) -> Option<DbdColumn> {
235    let parts: Vec<&str> = line.splitn(3, ' ').collect();
236    if parts.len() < 2 {
237        return None;
238    }
239
240    let type_and_rest = parts[0];
241    let rest = parts[1..].join(" ");
242
243    // Extract base type and check for foreign key in the type specification
244    let (base_type, type_foreign_key) = if let Some(angle_start) = type_and_rest.find('<') {
245        let base = &type_and_rest[..angle_start];
246        if let Some(angle_end) = type_and_rest.find('>') {
247            let fk_str = &type_and_rest[angle_start + 1..angle_end];
248            let foreign_key = fk_str.find("::").map(|sep_pos| ForeignKey {
249                table: fk_str[..sep_pos].to_string(),
250                field: fk_str[sep_pos + 2..].to_string(),
251            });
252            (base, foreign_key)
253        } else {
254            (type_and_rest, None)
255        }
256    } else {
257        (type_and_rest, None)
258    };
259
260    let is_optional = rest.trim_end().ends_with('?');
261    let rest = if is_optional {
262        rest.trim_end().trim_end_matches('?')
263    } else {
264        rest.trim_end()
265    };
266
267    let (name, remaining) = {
268        let comment_pos = rest.find("//");
269        if let Some(pos) = comment_pos {
270            (rest[..pos].trim().to_string(), &rest[pos..])
271        } else {
272            (rest.trim().to_string(), "")
273        }
274    };
275
276    let comment = if remaining.trim().starts_with("//") {
277        Some(remaining.trim()[2..].trim().to_string())
278    } else {
279        None
280    };
281
282    Some(DbdColumn {
283        name,
284        base_type: base_type.to_string(),
285        foreign_key: type_foreign_key,
286        comment,
287        is_optional,
288    })
289}
290
291fn parse_field_line(line: &str) -> DbdField {
292    let mut name: String;
293    let mut type_size = TypeSize::Unspecified;
294    let mut is_array = false;
295    let mut array_size = None;
296    let mut is_key = false;
297    let mut is_relation = false;
298    let mut is_noninline = false;
299
300    // Check for special markers
301    let line = if let Some(stripped) = line.strip_prefix("$id$") {
302        is_key = true;
303        stripped
304    } else if let Some(stripped) = line.strip_prefix("$noninline,id$") {
305        is_key = true;
306        is_noninline = true;
307        stripped
308    } else if let Some(stripped) = line.strip_prefix("$relation$") {
309        is_relation = true;
310        stripped
311    } else {
312        line
313    };
314
315    // Handle array notation first (can be combined with type size)
316    let (base_part, array_info) = if let Some(bracket_start) = line.find('[') {
317        if let Some(bracket_end) = line.find(']') {
318            let array_str = &line[bracket_start + 1..bracket_end];
319            is_array = true;
320            array_size = array_str.parse().ok();
321
322            // Check if there's a type spec before the array
323            let before_bracket = &line[..bracket_start];
324            let after_bracket = &line[bracket_end + 1..];
325            (
326                before_bracket.to_string() + after_bracket,
327                Some((is_array, array_size)),
328            )
329        } else {
330            (line.to_string(), None)
331        }
332    } else {
333        (line.to_string(), None)
334    };
335
336    // Apply array info if found
337    if let Some((arr, size)) = array_info {
338        is_array = arr;
339        array_size = size;
340    }
341
342    // Parse type size notation
343    if let Some(angle_start) = base_part.find('<') {
344        name = base_part[..angle_start].to_string();
345        if let Some(angle_end) = base_part.find('>') {
346            let size_str = &base_part[angle_start + 1..angle_end];
347            type_size = TypeSize::parse_type_size(size_str);
348        }
349    } else {
350        name = base_part.trim().to_string();
351    }
352
353    if let Some(idx) = name.find(" ") {
354        name.truncate(idx);
355    }
356
357    DbdField {
358        name,
359        type_size,
360        is_array,
361        array_size,
362        is_key,
363        is_relation,
364        is_noninline,
365    }
366}