// SPDX-License-Identifier: GPL-3.0-or-later // Appends compact MoonWell spell overrides to the stock 3.3.5 DBCs at open time. #include "Host.hpp" #include "core/Logger.hpp" #include "mpq/MpqStore.hpp" #include #include #include #include #include #include #include #include #include namespace moonwell::host { namespace { constexpr std::string_view kOverridesPath = "WXLSpellOverrides.tsv"; constexpr std::string_view kSpellPath = "DBFilesClient\\Spell.dbc"; constexpr std::string_view kSkillPath = "DBFilesClient\\SkillLineAbility.dbc"; constexpr std::string_view kSpellIconPath = "DBFilesClient\\SpellIcon.dbc"; constexpr uint32_t kSpellFields = 234; constexpr uint32_t kSpellRecordSize = kSpellFields * 4; constexpr uint32_t kSkillFields = 14; constexpr uint32_t kSkillRecordSize = kSkillFields * 4; constexpr uint32_t kSpellIconFields = 2; constexpr uint32_t kSpellIconRecordSize = kSpellIconFields * 4; constexpr char8_t kDescriptionPrefixUtf8[] = u8"\u041F\u0440\u0438\u0437\u044B\u0432\u0430\u0435\u0442 \u0438\u043B\u0438 \u043E\u0442\u043F\u0443\u0441\u043A\u0430\u0435\u0442 \u0441\u043F\u0443\u0442\u043D\u0438\u043A\u0430: "; struct Override { uint32_t spellId = 0; uint32_t templateSpellId = 0; uint32_t creatureEntry = 0; uint32_t skillLineAbilityId = 0; uint32_t raceMask = 0; uint32_t spellIconId = 0; std::string iconPath; std::string name; }; std::once_flag g_loadOnce; bool g_ready = false; std::vector g_spellDbc; std::vector g_skillDbc; std::vector g_spellIconDbc; uint32_t ReadU32(const uint8_t* data) { uint32_t value = 0; std::memcpy(&value, data, sizeof(value)); return value; } void WriteU32(uint8_t* data, uint32_t value) { std::memcpy(data, &value, sizeof(value)); } std::string_view Trim(std::string_view value) { while (!value.empty() && std::isspace(static_cast(value.front()))) value.remove_prefix(1); while (!value.empty() && std::isspace(static_cast(value.back()))) value.remove_suffix(1); return value; } bool ParseU32(std::string_view text, uint32_t& value) { text = Trim(text); const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value); return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); } bool SamePath(std::string_view left, std::string_view right) { if (left.size() != right.size()) return false; for (size_t i = 0; i < left.size(); ++i) { const unsigned char a = static_cast(left[i] == '/' ? '\\' : left[i]); const unsigned char b = static_cast(right[i] == '/' ? '\\' : right[i]); if (std::tolower(a) != std::tolower(b)) return false; } return true; } bool ParseOverrides(const std::vector& bytes, std::vector& out) { const std::string_view text(reinterpret_cast(bytes.data()), bytes.size()); size_t lineStart = 0; while (lineStart < text.size()) { size_t lineEnd = text.find('\n', lineStart); if (lineEnd == std::string_view::npos) lineEnd = text.size(); std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart)); lineStart = lineEnd + 1; if (line.empty() || line.front() == '#') continue; std::vector fields; size_t fieldStart = 0; while (true) { const size_t tab = line.find('\t', fieldStart); if (tab == std::string_view::npos) { fields.push_back(Trim(line.substr(fieldStart))); break; } fields.push_back(Trim(line.substr(fieldStart, tab - fieldStart))); fieldStart = tab + 1; } Override item; if (fields.size() == 5) { if (!ParseU32(fields[0], item.spellId) || !ParseU32(fields[1], item.templateSpellId) || !ParseU32(fields[2], item.creatureEntry) || !ParseU32(fields[3], item.skillLineAbilityId) || fields[4].empty()) return false; item.name.assign(fields[4]); } else if (fields.size() == 8) { if (!ParseU32(fields[0], item.spellId) || !ParseU32(fields[1], item.templateSpellId) || !ParseU32(fields[2], item.creatureEntry) || !ParseU32(fields[3], item.skillLineAbilityId) || !ParseU32(fields[4], item.raceMask) || !ParseU32(fields[5], item.spellIconId) || fields[7].empty()) return false; if (item.spellIconId != 0) { if (fields[6].empty() || fields[6] == "-") return false; item.iconPath.assign(fields[6]); } item.name.assign(fields[7]); } else { return false; } out.push_back(std::move(item)); } return !out.empty(); } bool ValidateWdbc(const std::vector& bytes, uint32_t fields, uint32_t recordSize, uint32_t& count, uint32_t& stringSize, size_t& recordsEnd) { if (bytes.size() < 20 || std::memcmp(bytes.data(), "WDBC", 4) != 0) return false; count = ReadU32(bytes.data() + 4); if (ReadU32(bytes.data() + 8) != fields || ReadU32(bytes.data() + 12) != recordSize) return false; stringSize = ReadU32(bytes.data() + 16); recordsEnd = 20ull + static_cast(count) * recordSize; return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size(); } const uint8_t* FindSpell(const std::vector& bytes, uint32_t count, uint32_t id) { for (uint32_t i = 0; i < count; ++i) { const uint8_t* row = bytes.data() + 20 + static_cast(i) * kSpellRecordSize; if (ReadU32(row) == id) return row; } return nullptr; } const uint8_t* FindSkillRow(const std::vector& bytes, uint32_t count, uint32_t spellId) { for (uint32_t i = 0; i < count; ++i) { const uint8_t* row = bytes.data() + 20 + static_cast(i) * kSkillRecordSize; if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row; } return nullptr; } bool BuildSpellDbc(const std::vector& base, const std::vector& overrides, std::vector& out) { uint32_t count = 0, stringSize = 0; size_t recordsEnd = 0; if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd)) return false; std::vector records; records.reserve(overrides.size() * kSpellRecordSize); std::vector strings; for (const Override& item : overrides) { const uint8_t* source = FindSpell(base, count, item.templateSpellId); if (!source) return false; const size_t recordStart = records.size(); records.insert(records.end(), source, source + kSpellRecordSize); uint8_t* row = records.data() + recordStart; WriteU32(row, item.spellId); WriteU32(row + 110 * 4, item.creatureEntry); if (item.spellIconId != 0) WriteU32(row + 133 * 4, item.spellIconId); const uint32_t nameOffset = stringSize + static_cast(strings.size()); strings.insert(strings.end(), item.name.begin(), item.name.end()); strings.push_back(0); const std::string description = std::string(reinterpret_cast(kDescriptionPrefixUtf8)) + item.name + "."; const uint32_t descriptionOffset = stringSize + static_cast(strings.size()); strings.insert(strings.end(), description.begin(), description.end()); strings.push_back(0); for (uint32_t field = 136; field <= 151; ++field) WriteU32(row + field * 4, nameOffset); for (uint32_t field = 170; field <= 185; ++field) WriteU32(row + field * 4, descriptionOffset); for (uint32_t field = 187; field <= 202; ++field) WriteU32(row + field * 4, 0); } out.reserve(base.size() + records.size() + strings.size()); out.insert(out.end(), base.begin(), base.begin() + 20); WriteU32(out.data() + 4, count + static_cast(overrides.size())); WriteU32(out.data() + 16, stringSize + static_cast(strings.size())); out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd); out.insert(out.end(), records.begin(), records.end()); out.insert(out.end(), base.begin() + recordsEnd, base.end()); out.insert(out.end(), strings.begin(), strings.end()); return true; } bool BuildSkillDbc(const std::vector& base, const std::vector& overrides, std::vector& out) { uint32_t count = 0, stringSize = 0; size_t recordsEnd = 0; if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd)) return false; std::vector records; records.reserve(overrides.size() * kSkillRecordSize); for (const Override& item : overrides) { const uint8_t* source = FindSkillRow(base, count, item.templateSpellId); if (!source) return false; const size_t recordStart = records.size(); records.insert(records.end(), source, source + kSkillRecordSize); uint8_t* row = records.data() + recordStart; WriteU32(row, item.skillLineAbilityId); WriteU32(row + 8, item.spellId); WriteU32(row + 12, item.raceMask); } out.reserve(base.size() + records.size()); out.insert(out.end(), base.begin(), base.begin() + 20); WriteU32(out.data() + 4, count + static_cast(overrides.size())); out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd); out.insert(out.end(), records.begin(), records.end()); out.insert(out.end(), base.begin() + recordsEnd, base.end()); return true; } bool BuildSpellIconDbc(const std::vector& base, const std::vector& overrides, std::vector& out) { uint32_t count = 0, stringSize = 0; size_t recordsEnd = 0; if (!ValidateWdbc(base, kSpellIconFields, kSpellIconRecordSize, count, stringSize, recordsEnd)) return false; std::vector records; std::vector strings; for (const Override& item : overrides) { if (item.spellIconId == 0) continue; for (uint32_t i = 0; i < count; ++i) { const uint8_t* row = base.data() + 20 + static_cast(i) * kSpellIconRecordSize; if (ReadU32(row) == item.spellIconId) return false; } for (size_t i = 0; i < records.size(); i += kSpellIconRecordSize) if (ReadU32(records.data() + i) == item.spellIconId) return false; const uint32_t pathOffset = stringSize + static_cast(strings.size()); const size_t recordStart = records.size(); records.resize(recordStart + kSpellIconRecordSize); WriteU32(records.data() + recordStart, item.spellIconId); WriteU32(records.data() + recordStart + 4, pathOffset); strings.insert(strings.end(), item.iconPath.begin(), item.iconPath.end()); strings.push_back(0); } if (records.empty()) return true; out.reserve(base.size() + records.size() + strings.size()); out.insert(out.end(), base.begin(), base.begin() + 20); WriteU32(out.data() + 4, count + static_cast(records.size() / kSpellIconRecordSize)); WriteU32(out.data() + 16, stringSize + static_cast(strings.size())); out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd); out.insert(out.end(), records.begin(), records.end()); out.insert(out.end(), base.begin() + recordsEnd, base.end()); out.insert(out.end(), strings.begin(), strings.end()); return true; } void Load() { const std::string root = wxl::host::ClientRoot(); wxl::host::mpq::MpqStore store; std::vector overrideBytes, spellBase, skillBase, spellIconBase; if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes)) return; std::vector overrides; if (!ParseOverrides(overrideBytes, overrides) || !store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) || !store.ReadAll(kSpellIconPath, spellIconBase) || !BuildSpellDbc(spellBase, overrides, g_spellDbc) || !BuildSkillDbc(skillBase, overrides, g_skillDbc) || !BuildSpellIconDbc(spellIconBase, overrides, g_spellIconDbc)) { WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs"); return; } g_ready = true; WLOG_INFO("moonwell-spells: appended %zu companion spell(s)", overrides.size()); } bool Provide(std::string_view name, std::vector& out) { if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath) && !SamePath(name, kSpellIconPath)) return false; std::call_once(g_loadOnce, &Load); if (!g_ready) return false; if (SamePath(name, kSpellPath)) out = g_spellDbc; else if (SamePath(name, kSkillPath)) out = g_skillDbc; else { if (g_spellIconDbc.empty()) return false; out = g_spellIconDbc; } return true; } struct Registrar { Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); } }; Registrar g_registrar; } }