wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
dbd_loader.cpp
Go to the documentation of this file.
1
13
15
17
18#if WOWLIB_DB_SCHEMA_RUNTIME
19
20#include <algorithm>
21#include <array>
22#include <cctype>
23#include <charconv>
24#include <cstdint>
25#include <cstring>
26#include <filesystem>
27#include <fstream>
28#include <map>
29#include <optional>
30#include <set>
31#include <sstream>
32#include <string>
33#include <string_view>
34#include <vector>
35
36namespace wowlib::db {
37 namespace {
38 using Build = std::array<std::uint32_t, 4>;
39
42 constexpr std::array<ClientVersion, 11> Targets{
54 };
55
56 Build buildOf(ClientVersion v) {
57 return {v.major, v.minor, v.patch, v.build};
58 }
59
62 std::optional<int> locstringLangs(Build build) {
63 if (build < Build{2, 1, 0, 6692}) return 8;
64 if (build < Build{4, 0, 0, 0}) return 16;
65 return std::nullopt;
66 }
67
69 bool reservedName(std::string_view name) {
70 static const std::set<std::string_view> Reserved{
71 "alignas",
72 "alignof",
73 "and",
74 "asm",
75 "auto",
76 "bool",
77 "break",
78 "case",
79 "catch",
80 "char",
81 "class",
82 "concept",
83 "const",
84 "consteval",
85 "constexpr",
86 "constinit",
87 "continue",
88 "decltype",
89 "default",
90 "delete",
91 "do",
92 "double",
93 "else",
94 "enum",
95 "explicit",
96 "export",
97 "extern",
98 "false",
99 "float",
100 "for",
101 "friend",
102 "goto",
103 "if",
104 "inline",
105 "int",
106 "long",
107 "mutable",
108 "namespace",
109 "new",
110 "noexcept",
111 "not",
112 "nullptr",
113 "operator",
114 "or",
115 "private",
116 "protected",
117 "public",
118 "register",
119 "requires",
120 "return",
121 "short",
122 "signed",
123 "sizeof",
124 "static",
125 "static_assert",
126 "struct",
127 "switch",
128 "template",
129 "this",
130 "throw",
131 "true",
132 "try",
133 "typedef",
134 "typeid",
135 "typename",
136 "union",
137 "unsigned",
138 "using",
139 "virtual",
140 "void",
141 "volatile",
142 "while",
143 "xor",
144 "version",
145 "table_name"
146 };
147 return Reserved.contains(name);
148 }
149
155 std::string snake(std::string_view name) {
156 const auto upper = [](char c) { return c >= 'A' && c <= 'Z'; };
157 const auto lower = [](char c) { return c >= 'a' && c <= 'z'; };
158 const auto digit = [](char c) { return c >= '0' && c <= '9'; };
159
160 // Pass 1: an underscore before every [A-Z][a-z]+ run that has ANY
161 // predecessor (the regex needs one char of lookbehind).
162 std::string pass1;
163 for (std::size_t i = 0; i < name.size(); ++i) {
164 if (i > 0 && upper(name[i]) && i + 1 < name.size() && lower(name[i + 1])) pass1 += '_';
165 pass1 += name[i];
166 }
167 // Pass 2: an underscore between [a-z0-9] and [A-Z].
168 std::string pass2;
169 for (std::size_t i = 0; i < pass1.size(); ++i) {
170 if (i > 0 && upper(pass1[i]) && (lower(pass1[i - 1]) || digit(pass1[i - 1]))) pass2 += '_';
171 pass2 += pass1[i];
172 }
173 // Lowercase + collapse "__".
174 std::string out;
175 for (char c : pass2) {
176 const char l = upper(c) ? static_cast<char>(c - 'A' + 'a') : c;
177 if (l == '_' && !out.empty() && out.back() == '_') continue;
178 out += l;
179 }
180 return out;
181 }
182
185 std::string memberName(std::string_view dbdName) {
186 std::string_view base = dbdName;
187 if (base.ends_with("_lang")) base.remove_suffix(5);
188 std::string name = snake(base);
189 if (reservedName(name)) name += '_';
190 if (!name.empty() && name.front() >= '0' && name.front() <= '9') name.insert(name.begin(), '_');
191 return name;
192 }
193
194 // --- the .dbd grammar (dbd.py) ------------------------------------------
195
196 struct ColumnDecl {
197 std::string type;
198 };
199
200 struct Entry {
201 std::string name;
202 bool isId = false;
203 bool isRelation = false;
204 bool noninline = false;
205 std::optional<int> bits;
206 bool isUnsigned = false;
207 std::optional<int> arrayLen;
208 };
209
210 struct VersionBlock {
211 std::vector<std::pair<Build, Build>> builds;
212 bool hasLayouts = false;
213 std::vector<Entry> entries;
214
215 bool matches(Build build) const {
216 return std::ranges::any_of(builds, [&](const auto& range) {
217 return range.first <= build && build <= range.second;
218 });
219 }
220 };
221
222 struct Definition {
223 std::map<std::string, ColumnDecl, std::less<>> columns;
224 std::vector<VersionBlock> blocks;
225
226 const VersionBlock* blockFor(Build build) const {
227 for (const VersionBlock& block : blocks)
228 if (block.matches(build)) return &block;
229 return nullptr;
230 }
231 };
232
234 std::string_view stripLine(std::string_view line) {
235 if (const auto at = line.find("//"); at != std::string_view::npos) line = line.substr(0, at);
236 while (!line.empty() && (line.front() == ' ' || line.front() == '\t' || line.front() == '\r')) line.
237 remove_prefix(1);
238 while (!line.empty() && (line.back() == ' ' || line.back() == '\t' || line.back() == '\r')) line.remove_suffix(1);
239 return line;
240 }
241
242 bool identChar(char c) {
243 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
244 }
245
246 std::optional<Build> parseBuild(std::string_view text) {
247 Build out{};
248 std::size_t part = 0;
249 const char* p = text.data();
250 const char* end = p + text.size();
251 while (part < 4) {
252 auto [next, ec] = std::from_chars(p, end, out[part]);
253 if (ec != std::errc{}) return std::nullopt;
254 p = next;
255 ++part;
256 if (part < 4) {
257 if (p == end || *p != '.') return std::nullopt;
258 ++p;
259 }
260 }
261 return p == end ? std::optional{out} : std::nullopt;
262 }
263
265 bool parseColumnLine(std::string_view body, Definition& def) {
266 static constexpr std::array<std::string_view, 4> Types{"int", "float", "string", "locstring"};
267 std::string_view type;
268 for (const std::string_view t : Types)
269 if (body.starts_with(t) && (body.size() == t.size() || body[t.size()] == '<' || body[t.size()] == ' '))
270 if (t.size() > type.size()) type = t;
271 if (type.empty()) return false;
272 body.remove_prefix(type.size());
273 if (body.starts_with('<')) {
274 const auto close = body.find('>');
275 if (close == std::string_view::npos) return false;
276 body.remove_prefix(close + 1);
277 }
278 while (body.starts_with(' ')) body.remove_prefix(1);
279 std::string_view name = body;
280 if (name.ends_with('?')) name.remove_suffix(1);
281 if (name.empty() || !std::ranges::all_of(name, identChar)) return false;
282 def.columns.emplace(std::string{name}, ColumnDecl{std::string{type}});
283 return true;
284 }
285
287 std::optional<Entry> parseEntry(std::string_view body) {
288 Entry entry{};
289 if (body.starts_with('$')) {
290 const auto close = body.find('$', 1);
291 if (close == std::string_view::npos) return std::nullopt;
292 std::string_view anns = body.substr(1, close - 1);
293 body.remove_prefix(close + 1);
294 while (!anns.empty()) {
295 const auto comma = anns.find(',');
296 const std::string_view ann = anns.substr(0, comma);
297 if (ann == "id") entry.isId = true;
298 else if (ann == "relation") entry.isRelation = true;
299 else if (ann == "noninline") entry.noninline = true;
300 if (comma == std::string_view::npos) break;
301 anns.remove_prefix(comma + 1);
302 }
303 }
304 std::size_t at = 0;
305 while (at < body.size() && identChar(body[at])) ++at;
306 if (at == 0) return std::nullopt;
307 entry.name = std::string{body.substr(0, at)};
308 body.remove_prefix(at);
309 if (body.starts_with('<')) {
310 body.remove_prefix(1);
311 if (body.starts_with('u')) {
312 entry.isUnsigned = true;
313 body.remove_prefix(1);
314 }
315 int bits = 0;
316 auto [next, ec] = std::from_chars(body.data(), body.data() + body.size(), bits);
317 if (ec != std::errc{} || next == body.data() || *next != '>') return std::nullopt;
318 entry.bits = bits;
319 body.remove_prefix(static_cast<std::size_t>(next - body.data()) + 1);
320 }
321 if (body.starts_with('[')) {
322 body.remove_prefix(1);
323 int len = 0;
324 auto [next, ec] = std::from_chars(body.data(), body.data() + body.size(), len);
325 if (ec != std::errc{} || next == body.data() || *next != ']') return std::nullopt;
326 entry.arrayLen = len;
327 body.remove_prefix(static_cast<std::size_t>(next - body.data()) + 1);
328 }
329 return body.empty() ? std::optional{entry} : std::nullopt;
330 }
331
334 std::optional<Definition> parseDbd(const std::string& text) {
335 std::vector<std::vector<std::string_view>> paragraphs;
336 std::vector<std::string_view> current;
337 std::string_view rest = text;
338 // Skip a UTF-8 BOM (dbdgen reads utf-8-sig).
339 if (rest.starts_with("\xEF\xBB\xBF")) rest.remove_prefix(3);
340 while (!rest.empty()) {
341 const auto nl = rest.find('\n');
342 std::string_view raw = rest.substr(0, nl);
343 rest = nl == std::string_view::npos ? std::string_view{} : rest.substr(nl + 1);
344 std::string_view line = raw;
345 while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.remove_suffix(1);
346 while (!line.empty() && line.front() == ' ') line.remove_prefix(1);
347 if (line.empty()) {
348 if (!current.empty()) {
349 paragraphs.push_back(std::move(current));
350 current.clear();
351 }
352 }
353 else {
354 current.push_back(line);
355 }
356 }
357 if (!current.empty()) paragraphs.push_back(std::move(current));
358 if (paragraphs.empty() || paragraphs.front().front() != "COLUMNS") return std::nullopt;
359
360 Definition def{};
361 for (std::size_t i = 1; i < paragraphs.front().size(); ++i) {
362 const std::string_view body = stripLine(paragraphs.front()[i]);
363 if (body.empty()) continue;
364 if (!parseColumnLine(body, def)) return std::nullopt;
365 }
366 for (std::size_t p = 1; p < paragraphs.size(); ++p) {
367 VersionBlock block{};
368 for (const std::string_view raw : paragraphs[p]) {
369 const std::string_view body = stripLine(raw);
370 if (body.empty()) continue;
371 if (body.starts_with("LAYOUT ")) {
372 block.hasLayouts = true;
373 }
374 else if (body.starts_with("BUILD ")) {
375 std::string_view list = body.substr(6);
376 while (!list.empty()) {
377 const auto comma = list.find(',');
378 std::string_view part = stripLine(list.substr(0, comma));
379 if (const auto dash = part.find('-'); dash != std::string_view::npos) {
380 const auto lo = parseBuild(stripLine(part.substr(0, dash)));
381 const auto hi = parseBuild(stripLine(part.substr(dash + 1)));
382 if (!lo || !hi) return std::nullopt;
383 block.builds.emplace_back(*lo, *hi);
384 }
385 else {
386 const auto build = parseBuild(part);
387 if (!build) return std::nullopt;
388 block.builds.emplace_back(*build, *build);
389 }
390 if (comma == std::string_view::npos) break;
391 list.remove_prefix(comma + 1);
392 }
393 }
394 else if (body.starts_with("COMMENT")) {
395 // Doc text only — the schema does not carry it.
396 }
397 else {
398 auto entry = parseEntry(body);
399 if (!entry) return std::nullopt;
400 block.entries.push_back(std::move(*entry));
401 }
402 }
403 if (block.builds.empty() && !block.hasLayouts) return std::nullopt;
404 def.blocks.push_back(std::move(block));
405 }
406 return def;
407 }
408
409 // --- member resolution (emit.py's build_members/_member_of) -------------
410
412 struct Member {
413 std::string name;
414 ColumnType type = ColumnType::Int;
415 std::uint8_t bits = 32;
416 bool isSigned = false;
417 std::uint16_t arrayLen = 1;
418 std::uint8_t localeCount = 0;
419 bool isId = false;
420 bool isRelation = false;
421 bool noninline = false;
422 std::string dbdName;
423
424 bool sameShape(const Member& other) const {
425 return name == other.name && type == other.type && bits == other.bits && isSigned == other.isSigned &&
426 arrayLen == other.arrayLen && localeCount == other.localeCount && isId == other.isId && isRelation ==
427 other.isRelation && noninline == other.noninline;
428 }
429 };
430
433 std::optional<std::vector<Member>> membersFor(const Definition& def, const VersionBlock& block, Build build) {
434 const std::optional<int> langs = locstringLangs(build);
435
436 // The `_lang` strip may collide with a sibling column; colliding
437 // locstrings keep their suffix (build_members' first pass).
438 std::vector<std::string> names;
439 names.reserve(block.entries.size());
440 for (const Entry& entry : block.entries) names.push_back(memberName(entry.name));
441 for (std::size_t i = 0; i < block.entries.size(); ++i) {
442 if (std::ranges::count(names, names[i]) > 1 && block.entries[i].name.ends_with("_lang")) {
443 std::string kept = snake(block.entries[i].name);
444 if (reservedName(kept)) kept += '_';
445 names[i] = std::move(kept);
446 }
447 }
448
449 // Uniqueness up front (names is stable; storing views into the
450 // growing members vector would dangle on reallocation).
451 if (std::set<std::string_view>(names.begin(), names.end()).size() != names.size()) return std::nullopt;
452 // member name collision
453 std::vector<Member> members;
454 for (std::size_t i = 0; i < block.entries.size(); ++i) {
455 const Entry& entry = block.entries[i];
456 const auto decl = def.columns.find(entry.name);
457 if (decl == def.columns.end()) return std::nullopt; // no COLUMNS declaration
458 const std::string& type = decl->second.type;
459
460 Member m{};
461 m.name = names[i];
462 m.dbdName = entry.name;
463 m.arrayLen = static_cast<std::uint16_t>(entry.arrayLen.value_or(1));
464 m.isId = entry.isId;
465 m.isRelation = entry.isRelation;
466 m.noninline = entry.noninline;
467
468 if (entry.bits) {
469 if (type != "int") return std::nullopt; // sized entry on a non-int column
470 if (*entry.bits != 8 && *entry.bits != 16 && *entry.bits != 32 && *entry.bits != 64) return std::nullopt;
471 // unsupported width
472 m.type = ColumnType::Int;
473 m.bits = static_cast<std::uint8_t>(*entry.bits);
474 m.isSigned = !entry.isUnsigned;
475 }
476 else if (type == "float") {
477 m.type = ColumnType::Float;
478 m.bits = 32;
479 }
480 else if (type == "string") {
481 m.type = ColumnType::String;
482 m.bits = 32;
483 }
484 else if (type == "locstring") {
485 if (entry.arrayLen) return std::nullopt; // locstring arrays unsupported
486 if (langs) {
487 m.type = ColumnType::LocString;
488 m.bits = 32;
489 m.localeCount = static_cast<std::uint8_t>(*langs);
490 }
491 else {
492 m.type = ColumnType::String;
493 m.bits = 32;
494 }
495 }
496 else if (type == "int" && entry.noninline) {
497 // ids/relations delivered by satellite blocks: u32.
498 m.type = ColumnType::Int;
499 m.bits = 32;
500 m.isSigned = false;
501 }
502 else {
503 return std::nullopt; // int entry without a <size>
504 }
505
506 members.push_back(std::move(m));
507 }
508 return members;
509 }
510
511 // --- WDBS assembly (emit.py's emit_schema_blob) -------------------------
512
513 struct Range {
514 std::uint16_t eraMask = 0;
515 std::vector<Member> members;
516 };
517
518 class BlobWriter {
519 public:
520 void addTable(const std::string& name, const std::string& disk, const std::vector<Range>& ranges) {
521 _appendU32(_tables, _intern(name));
522 _appendU32(_tables, _intern(disk));
523 _appendU32(_tables, _rangeCount);
524 _appendU32(_tables, static_cast<std::uint32_t>(ranges.size()));
525 for (const Range& range : ranges) {
526 _appendU32(_ranges, _columnCount);
527 _appendU16(_ranges, static_cast<std::uint16_t>(range.members.size()));
528 _appendU16(_ranges, range.eraMask);
529 ++_rangeCount;
530 for (const Member& m : range.members) {
531 _appendU32(_columns, _intern(m.name));
532 _columns.push_back(static_cast<unsigned char>(m.type));
533 _columns.push_back(m.bits);
534 _columns.push_back(
535 static_cast<unsigned char>((m.isSigned ? 1 : 0) | (m.isId ? 2 : 0) | (m.isRelation ? 4 : 0) | (
536 m.noninline ? 8 : 0)));
537 _columns.push_back(m.localeCount);
538 _appendU16(_columns, m.arrayLen);
539 _appendU16(_columns, 0);
540 _appendU32(_columns, _intern(m.dbdName));
541 ++_columnCount;
542 }
543 }
544 ++_tableCount;
545 }
546
547 std::vector<unsigned char> finish() const {
548 std::vector<unsigned char> out;
549 _appendU32(out, blob::Magic);
550 _appendU32(out, blob::FormatVersion);
551 _appendU32(out, _tableCount);
552 _appendU32(out, _rangeCount);
553 _appendU32(out, _columnCount);
554 _appendU32(out, static_cast<std::uint32_t>(_pool.size()));
555 out.push_back(static_cast<unsigned char>(Targets.size()));
556 out.insert(out.end(), 3, 0);
557 for (const ClientVersion v : Targets) {
558 _appendU16(out, v.major);
559 _appendU16(out, v.minor);
560 _appendU16(out, v.patch);
561 _appendU16(out, 0);
562 _appendU32(out, v.build);
563 }
564 out.insert(out.end(), _tables.begin(), _tables.end());
565 out.insert(out.end(), _ranges.begin(), _ranges.end());
566 out.insert(out.end(), _columns.begin(), _columns.end());
567 out.insert(out.end(), _pool.begin(), _pool.end());
568 return out;
569 }
570
571 private:
572 static void _appendU16(std::vector<unsigned char>& out, std::uint16_t v) {
573 out.push_back(static_cast<unsigned char>(v & 0xFF));
574 out.push_back(static_cast<unsigned char>(v >> 8));
575 }
576
577 static void _appendU32(std::vector<unsigned char>& out, std::uint32_t v) {
578 for (int shift = 0; shift < 32; shift += 8) out.push_back(static_cast<unsigned char>((v >> shift) & 0xFF));
579 }
580
581 std::uint32_t _intern(const std::string& text) {
582 const auto [at, fresh] = _interned.try_emplace(text, static_cast<std::uint32_t>(_pool.size()));
583 if (fresh) {
584 _pool.insert(_pool.end(), text.begin(), text.end());
585 _pool.push_back(0);
586 }
587 return at->second;
588 }
589
590 std::vector<unsigned char> _pool{0};
591 std::map<std::string, std::uint32_t> _interned{{"", 0}};
592 std::vector<unsigned char> _tables;
593 std::vector<unsigned char> _ranges;
594 std::vector<unsigned char> _columns;
595 std::uint32_t _tableCount = 0;
596 std::uint32_t _rangeCount = 0;
597 std::uint32_t _columnCount = 0;
598 };
599 }
600
601 Result<SchemaCatalog> SchemaCatalog::fromDbdDir(const std::filesystem::path& definitions) {
602 std::error_code ec;
603 std::filesystem::directory_iterator it{definitions, ec};
604 if (ec)
605 return makeError(ErrorCode::IoError, "cannot read WoWDBDefs definitions directory: " + definitions.string());
606
607 // dbdgen walks sorted(*.dbd); the blob writer wants name-sorted tables
608 // anyway, so collect and sort by IDENTIFIER name.
609 std::vector<std::pair<std::string, std::filesystem::path>> files;
610 for (const auto& entry : it) {
611 if (!entry.is_regular_file() || entry.path().extension() != ".dbd") continue;
612 std::string table = entry.path().stem().string();
613 if (table == "Item-sparse") // dbdgen's IDENT_RENAMES
614 table = "ItemSparseLegacy";
615 const bool identifier = !table.empty() && !(table.front() >= '0' && table.front() <= '9') && std::ranges::all_of(
616 table, identChar);
617 if (!identifier) continue;
618 files.emplace_back(std::move(table), entry.path());
619 }
620 std::ranges::sort(files);
621
622 BlobWriter writer;
623 for (const auto& [table, path] : files) {
624 std::ifstream in{path, std::ios::binary};
625 if (!in) continue;
626 std::ostringstream buffer;
627 buffer << in.rdbuf();
628 const auto def = parseDbd(buffer.str());
629 if (!def) continue; // dbdgen warns and skips; the loader skips silently
630
631 // Per-target member lists, then collapse() consecutive identical ones.
632 std::vector<Range> ranges;
633 for (std::size_t era = 0; era < Targets.size(); ++era) {
634 const Build build = buildOf(Targets[era]);
635 const VersionBlock* block = def->blockFor(build);
636 if (!block) continue;
637 auto members = membersFor(*def, *block, build);
638 if (!members) {
639 ranges.clear(); // any era error drops the table, like dbdgen
640 break;
641 }
642 const auto same = [&](const Range& range) {
643 return range.members.size() == members->size() && std::ranges::equal(
644 range.members, *members, [](const Member& a, const Member& b) {
645 return a.sameShape(b);
646 });
647 };
648 if (!ranges.empty() && same(ranges.back()))
649 ranges.back().eraMask |= static_cast<std::uint16_t>(1u << era);
650 else
651 ranges.push_back(Range{static_cast<std::uint16_t>(1u << era), std::move(*members)});
652 }
653 if (ranges.empty()) continue;
654 const std::string disk = table == "ItemSparseLegacy" ? "Item-sparse" : table;
655 writer.addTable(table, disk, ranges);
656 }
657
658 auto bytes = writer.finish();
659 const blob::View view{bytes};
660 if (!view.valid() || view.tableCount() == 0)
662 "no WoWDBDefs definition in " + definitions.string() + " produced a usable schema");
663 return fromBlob(std::move(bytes));
664 }
665}
666
667#endif // WOWLIB_DB_SCHEMA_RUNTIME
ColumnType
The logical value class of a column.
Definition schema.hpp:34
@ Int
An integer column; Column::bits / Column::isSigned give the shape.
Definition schema.hpp:35
@ Float
A 32-bit IEEE float column.
Definition schema.hpp:36
@ String
A string-block reference column (u32 offset on disk).
Definition schema.hpp:37
@ LocString
A pre-Cataclysm localized string column (see LocString).
Definition schema.hpp:38
constexpr ClientVersion Shadowlands
constexpr ClientVersion Tww
constexpr ClientVersion Bfa
constexpr ClientVersion Mop
constexpr ClientVersion Wod
constexpr ClientVersion Tbc
constexpr ClientVersion Vanilla
constexpr ClientVersion Legion
constexpr ClientVersion Dragonflight
constexpr ClientVersion Cata
constexpr ClientVersion Wotlk
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ SchemaBlobInvalid
A WDBS schema blob (or WoWDBDefs source) is malformed or truncated.
Definition error.hpp:48
@ IoError
Generic filesystem I/O failure (project directory).
Definition error.hpp:30
The WDBS schema-blob format: a constexpr-parseable view over the compact binary dbdgen emits (--schem...
The runtime schema source of the generic ClientDB engine: table name + client version in,...