18#if WOWLIB_DB_SCHEMA_RUNTIME
38 using Build = std::array<std::uint32_t, 4>;
42 constexpr std::array<ClientVersion, 11> Targets{
56 Build buildOf(ClientVersion v) {
57 return {v.major, v.minor, v.patch, v.build};
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;
69 bool reservedName(std::string_view name) {
70 static const std::set<std::string_view> Reserved{
147 return Reserved.contains(name);
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'; };
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 +=
'_';
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 +=
'_';
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;
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(),
'_');
203 bool isRelation =
false;
204 bool noninline =
false;
205 std::optional<int> bits;
206 bool isUnsigned =
false;
207 std::optional<int> arrayLen;
210 struct VersionBlock {
211 std::vector<std::pair<Build, Build>> builds;
212 bool hasLayouts =
false;
213 std::vector<Entry> entries;
215 bool matches(Build build)
const {
216 return std::ranges::any_of(builds, [&](
const auto& range) {
217 return range.first <= build && build <= range.second;
223 std::map<std::string, ColumnDecl, std::less<>> columns;
224 std::vector<VersionBlock> blocks;
226 const VersionBlock* blockFor(Build build)
const {
227 for (
const VersionBlock& block : blocks)
228 if (block.matches(build))
return █
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.
238 while (!line.empty() && (line.back() ==
' ' || line.back() ==
'\t' || line.back() ==
'\r')) line.remove_suffix(1);
242 bool identChar(
char c) {
243 return (c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z') || (c >=
'0' && c <=
'9') || c ==
'_';
246 std::optional<Build> parseBuild(std::string_view text) {
248 std::size_t part = 0;
249 const char* p = text.data();
250 const char* end = p + text.size();
252 auto [next, ec] = std::from_chars(p, end, out[part]);
253 if (ec != std::errc{})
return std::nullopt;
257 if (p == end || *p !=
'.')
return std::nullopt;
261 return p == end ? std::optional{out} : std::nullopt;
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);
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}});
287 std::optional<Entry> parseEntry(std::string_view body) {
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);
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);
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;
319 body.remove_prefix(
static_cast<std::size_t
>(next - body.data()) + 1);
321 if (body.starts_with(
'[')) {
322 body.remove_prefix(1);
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);
329 return body.empty() ? std::optional{entry} : std::nullopt;
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;
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);
348 if (!current.empty()) {
349 paragraphs.push_back(std::move(current));
354 current.push_back(line);
357 if (!current.empty()) paragraphs.push_back(std::move(current));
358 if (paragraphs.empty() || paragraphs.front().front() !=
"COLUMNS")
return std::nullopt;
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;
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;
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);
386 const auto build = parseBuild(part);
387 if (!build)
return std::nullopt;
388 block.builds.emplace_back(*build, *build);
390 if (comma == std::string_view::npos)
break;
391 list.remove_prefix(comma + 1);
394 else if (body.starts_with(
"COMMENT")) {
398 auto entry = parseEntry(body);
399 if (!entry)
return std::nullopt;
400 block.entries.push_back(std::move(*entry));
403 if (block.builds.empty() && !block.hasLayouts)
return std::nullopt;
404 def.blocks.push_back(std::move(block));
415 std::uint8_t bits = 32;
416 bool isSigned =
false;
417 std::uint16_t arrayLen = 1;
418 std::uint8_t localeCount = 0;
420 bool isRelation =
false;
421 bool noninline =
false;
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;
433 std::optional<std::vector<Member>> membersFor(
const Definition& def,
const VersionBlock& block, Build build) {
434 const std::optional<int> langs = locstringLangs(build);
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);
451 if (std::set<std::string_view>(names.begin(), names.end()).size() != names.size())
return std::nullopt;
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;
458 const std::string& type = decl->second.type;
462 m.dbdName = entry.name;
463 m.arrayLen =
static_cast<std::uint16_t
>(entry.arrayLen.value_or(1));
465 m.isRelation = entry.isRelation;
466 m.noninline = entry.noninline;
469 if (type !=
"int")
return std::nullopt;
470 if (*entry.bits != 8 && *entry.bits != 16 && *entry.bits != 32 && *entry.bits != 64)
return std::nullopt;
473 m.bits =
static_cast<std::uint8_t
>(*entry.bits);
474 m.isSigned = !entry.isUnsigned;
476 else if (type ==
"float") {
480 else if (type ==
"string") {
484 else if (type ==
"locstring") {
485 if (entry.arrayLen)
return std::nullopt;
489 m.localeCount =
static_cast<std::uint8_t
>(*langs);
496 else if (type ==
"int" && entry.noninline) {
506 members.push_back(std::move(m));
514 std::uint16_t eraMask = 0;
515 std::vector<Member> members;
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);
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);
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));
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);
562 _appendU32(out, v.build);
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());
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));
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));
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()));
584 _pool.insert(_pool.end(), text.begin(), text.end());
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;
603 std::filesystem::directory_iterator it{definitions, ec};
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")
614 table =
"ItemSparseLegacy";
615 const bool identifier = !table.empty() && !(table.front() >=
'0' && table.front() <=
'9') && std::ranges::all_of(
617 if (!identifier)
continue;
618 files.emplace_back(std::move(table), entry.path());
620 std::ranges::sort(files);
623 for (
const auto& [table, path] : files) {
624 std::ifstream in{path, std::ios::binary};
626 std::ostringstream buffer;
627 buffer << in.rdbuf();
628 const auto def = parseDbd(buffer.str());
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);
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);
648 if (!ranges.empty() && same(ranges.back()))
649 ranges.back().eraMask |=
static_cast<std::uint16_t
>(1u << era);
651 ranges.push_back(Range{
static_cast<std::uint16_t
>(1u << era), std::move(*members)});
653 if (ranges.empty())
continue;
654 const std::string disk = table ==
"ItemSparseLegacy" ?
"Item-sparse" : table;
655 writer.addTable(table, disk, ranges);
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));
ColumnType
The logical value class of a column.
@ Int
An integer column; Column::bits / Column::isSigned give the shape.
@ Float
A 32-bit IEEE float column.
@ String
A string-block reference column (u32 offset on disk).
@ LocString
A pre-Cataclysm localized string column (see LocString).
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-...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ SchemaBlobInvalid
A WDBS schema blob (or WoWDBDefs source) is malformed or truncated.
@ IoError
Generic filesystem I/O failure (project directory).
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,...