355 lines
16 KiB
C++
355 lines
16 KiB
C++
// 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 <cctype>
|
|
#include <charconv>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
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<uint8_t> g_spellDbc;
|
|
std::vector<uint8_t> g_skillDbc;
|
|
std::vector<uint8_t> 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<unsigned char>(value.front())))
|
|
value.remove_prefix(1);
|
|
while (!value.empty() && std::isspace(static_cast<unsigned char>(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<unsigned char>(left[i] == '/' ? '\\' : left[i]);
|
|
const unsigned char b = static_cast<unsigned char>(right[i] == '/' ? '\\' : right[i]);
|
|
if (std::tolower(a) != std::tolower(b)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseOverrides(const std::vector<uint8_t>& bytes, std::vector<Override>& out)
|
|
{
|
|
const std::string_view text(reinterpret_cast<const char*>(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<std::string_view> 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<uint8_t>& 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<size_t>(count) * recordSize;
|
|
return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size();
|
|
}
|
|
|
|
const uint8_t* FindSpell(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t id)
|
|
{
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSpellRecordSize;
|
|
if (ReadU32(row) == id) return row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const uint8_t* FindSkillRow(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t spellId)
|
|
{
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSkillRecordSize;
|
|
if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool BuildSpellDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
|
std::vector<uint8_t>& out)
|
|
{
|
|
uint32_t count = 0, stringSize = 0;
|
|
size_t recordsEnd = 0;
|
|
if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd))
|
|
return false;
|
|
|
|
std::vector<uint8_t> records;
|
|
records.reserve(overrides.size() * kSpellRecordSize);
|
|
std::vector<uint8_t> 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<uint32_t>(strings.size());
|
|
strings.insert(strings.end(), item.name.begin(), item.name.end());
|
|
strings.push_back(0);
|
|
const std::string description =
|
|
std::string(reinterpret_cast<const char*>(kDescriptionPrefixUtf8)) + item.name + ".";
|
|
const uint32_t descriptionOffset = stringSize + static_cast<uint32_t>(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<uint32_t>(overrides.size()));
|
|
WriteU32(out.data() + 16, stringSize + static_cast<uint32_t>(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<uint8_t>& base, const std::vector<Override>& overrides,
|
|
std::vector<uint8_t>& out)
|
|
{
|
|
uint32_t count = 0, stringSize = 0;
|
|
size_t recordsEnd = 0;
|
|
if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd))
|
|
return false;
|
|
|
|
std::vector<uint8_t> 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<uint32_t>(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<uint8_t>& base, const std::vector<Override>& overrides,
|
|
std::vector<uint8_t>& out)
|
|
{
|
|
uint32_t count = 0, stringSize = 0;
|
|
size_t recordsEnd = 0;
|
|
if (!ValidateWdbc(base, kSpellIconFields, kSpellIconRecordSize, count, stringSize, recordsEnd))
|
|
return false;
|
|
|
|
std::vector<uint8_t> records;
|
|
std::vector<uint8_t> 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<size_t>(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<uint32_t>(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<uint32_t>(records.size() / kSpellIconRecordSize));
|
|
WriteU32(out.data() + 16, stringSize + static_cast<uint32_t>(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<uint8_t> overrideBytes, spellBase, skillBase, spellIconBase;
|
|
if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes))
|
|
return;
|
|
|
|
std::vector<Override> 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<uint8_t>& 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;
|
|
}
|
|
}
|