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 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 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 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 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 if let Some((arr, size)) = array_info {
338 is_array = arr;
339 array_size = size;
340 }
341
342 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}