Compare commits
11 Commits
01f476ca1b
...
new-pets
| Author | SHA1 | Date | |
|---|---|---|---|
| c905d0558e | |||
| 76ca0cc851 | |||
| 398245d27d | |||
| 887ab7879b | |||
| e3b8d2649e | |||
| e916cdea19 | |||
| befdaa456d | |||
| 7291fff664 | |||
| 790506fc7a | |||
| 87329f2633 | |||
| 8375b95f7b |
@@ -1,15 +1,20 @@
|
||||
[submodule "vendor/warcraftxl"]
|
||||
path = vendor/warcraftxl
|
||||
url = https://github.com/WarcraftXL/wxl-core.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-assets"]
|
||||
path = vendor/modules/wxl-modern-assets
|
||||
url = https://github.com/WarcraftXL/wxl-modern-assets.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-render"]
|
||||
path = vendor/modules/wxl-modern-render
|
||||
url = https://github.com/WarcraftXL/wxl-modern-render.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-unit-outline"]
|
||||
path = vendor/modules/wxl-unit-outline
|
||||
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-adt"]
|
||||
path = vendor/modules/wxl-modern-adt
|
||||
url = https://github.com/WarcraftXL/wxl-modern-adt.git
|
||||
branch = main
|
||||
|
||||
@@ -41,6 +41,8 @@ if(TARGET WarcraftXL)
|
||||
endif()
|
||||
|
||||
if(TARGET WarcraftXLHost)
|
||||
wxl_collect_sources(WXL_MOONWELL_HOST
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/host/*.cpp")
|
||||
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||
@@ -53,6 +55,7 @@ if(TARGET WarcraftXLHost)
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||
|
||||
target_sources(WarcraftXLHost PRIVATE
|
||||
${WXL_MOONWELL_HOST}
|
||||
${WXL_MODERN_ASSETS_HOST}
|
||||
${WXL_MODERN_ADT_HOST})
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Автоматический импорт mount и pet
|
||||
|
||||
Список современных моделей находится в `custom-creatures.json`. В нём есть две независимые
|
||||
секции:
|
||||
|
||||
```json
|
||||
{
|
||||
"mounts": [
|
||||
"assets/creatures/my_mount"
|
||||
],
|
||||
"pets": [
|
||||
"assets/creatures/my_pet"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Папка должна быть результатом **Raw M2 export** из wow.export и содержать:
|
||||
|
||||
- один основной `.m2`;
|
||||
- `*.manifest.json`;
|
||||
- все упомянутые в manifest текстуры, skin, skeleton, bone и animation-файлы.
|
||||
|
||||
Разрешены абсолютные пути, например `C:/Users/sindo/wow.export/creature/catslime`, но для
|
||||
воспроизводимой сборки папку лучше скопировать внутрь репозитория и указать относительный путь.
|
||||
|
||||
## Что делает сборщик
|
||||
|
||||
При обычном запуске `tool.exe`, `run.ps1` или `deploy.ps1` сборщик:
|
||||
|
||||
1. читает manifest каждой модели и размещает retail M2 со всеми зависимостями в loose-каталоге
|
||||
`Data/Patch-WXL.MPQ`, создавая рядом `.mdx`-алиас для загрузчика WoW 3.3.5, чтобы WarcraftXL всегда
|
||||
преобразовывал MD21 до загрузки клиентом;
|
||||
2. генерирует `WXLFileData.csv` для WarcraftXL;
|
||||
3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`;
|
||||
4. сохраняет стабильные ID в `custom-creatures.lock.json`;
|
||||
5. для `pets` создаёт companion summon spell и строку skill line `778`;
|
||||
6. кладёт серверные DBC, SQL и таблицу ID в `build/server-dbc`.
|
||||
|
||||
Полный `Spell.dbc` в патч не копируется. WarcraftXL читает компактный
|
||||
`WXLSpellOverrides.tsv` и при открытии DBC добавляет новые spell-записи к штатному файлу клиента.
|
||||
|
||||
Перестановка моделей в списке не меняет уже выданные ID. Lock-файл необходимо хранить в Git.
|
||||
|
||||
## Дополнительные параметры
|
||||
|
||||
Если значений по умолчанию недостаточно, вместо строки используется объект:
|
||||
|
||||
```json
|
||||
{
|
||||
"folder": "assets/creatures/my_mount",
|
||||
"key": "my_mount",
|
||||
"name": "My Mount",
|
||||
"name_ru": "Мой транспорт",
|
||||
"race_mask": 1101,
|
||||
"icon": "my_icon.jpg",
|
||||
"archive": "Creature/MyMount",
|
||||
"model_file": "my_mount.m2",
|
||||
"template_display_id": 2404,
|
||||
"display_scale": 1.0,
|
||||
"model_scale": 1.0,
|
||||
"collision_width": 0.6111,
|
||||
"collision_height": 2.031,
|
||||
"mount_height": 1.8657,
|
||||
"flags": 0
|
||||
}
|
||||
```
|
||||
|
||||
Для `mounts` по умолчанию клонируется верховая лошадь с display ID `2404`, для `pets` —
|
||||
компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC;
|
||||
параметры объекта позволяют их переопределить.
|
||||
|
||||
Для `pets` поле `race_mask` ограничивает строку коллекции набором рас (`1101` — Альянс,
|
||||
`690` — Орда, `0` или отсутствие поля — все расы). Поле `icon` указывает JPG, PNG или TGA рядом
|
||||
с моделью либо путь от корня проекта. Сборщик конвертирует изображение в BLP2, выдаёт стабильный
|
||||
`spellIconID`, добавляет запись в `SpellIcon.dbc` через MoonWell provider и назначает её summon-spell.
|
||||
|
||||
## Конвертация иконок
|
||||
|
||||
Встроенная команда CLI преобразует JPG, PNG или TGA в совместимый с клиентом BLP2:
|
||||
|
||||
```powershell
|
||||
tool.exe icon "путь/к/icon.jpg" ["путь/к/icon.blp"]
|
||||
```
|
||||
|
||||
Если выходной путь не указан, `.blp` создаётся рядом с исходным файлом. Конвертер автоматически
|
||||
приводит изображение к ближайшему квадратному размеру степени двойки (не более 512×512), кодирует
|
||||
его в DXT1 и создаёт mipmap-уровни. Например, иконка 56×56 будет преобразована в 64×64.
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
Для каждого элемента `pets` сборщик автоматически выдаёт `creatureEntry`, `summonSpellID` и
|
||||
`skillLineAbilityID`. Значения находятся в `build/server-dbc/custom-creatures.csv`.
|
||||
|
||||
1. Скопировать `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` из `build/server-dbc` в
|
||||
серверный каталог `data/dbc`.
|
||||
2. Применить `build/server-dbc/custom-creatures.sql` к `acore_world`.
|
||||
3. Перезапустить worldserver: DBC и таблицы `spell_dbc` загружаются только при старте.
|
||||
4. Для проверки изучить выданный spell командой `.learn <summonSpellID>`.
|
||||
|
||||
SQL создаёт `creature_template`, `creature_model_info`, `creature_template_model`, companion spell и строку skill line.
|
||||
Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует
|
||||
другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"base_dbc_dir": "assets/dbc/3.3.5a",
|
||||
"model_data_id_start": 50000,
|
||||
"display_info_id_start": 50000,
|
||||
"creature_entry_id_start": 5000000,
|
||||
"summon_spell_id_start": 110000,
|
||||
"skill_line_ability_id_start": 30000,
|
||||
"spell_icon_id_start": 50000,
|
||||
"defaults": {
|
||||
"mount_template_display_id": 2404,
|
||||
"pet_template_display_id": 5448
|
||||
},
|
||||
"mounts": [],
|
||||
"pets": [
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/Catslime",
|
||||
"name": "Cat Slime",
|
||||
"name_ru": "Котослизень",
|
||||
"display_scale": 0.3
|
||||
},
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/MoonkinHatchling_Alliance",
|
||||
"key": "moonkin_hatchling_alliance",
|
||||
"race_mask": 1101,
|
||||
"icon": "inv_misc_petmoonkinne.jpg",
|
||||
"name": "Moonkin Hatchling",
|
||||
"name_ru": "Лунный совушек",
|
||||
"display_scale": 0.3
|
||||
},
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/MoonkinHatchling_Horde",
|
||||
"key": "moonkin_hatchling_horde",
|
||||
"race_mask": 690,
|
||||
"icon": "inv_misc_petmoonkinta.jpg",
|
||||
"name": "Moonkin Hatchling",
|
||||
"name_ru": "Лунный совушек",
|
||||
"display_scale": 0.3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"models": {
|
||||
"catslime": {
|
||||
"model_data_id": 50000,
|
||||
"display_info_id": 50000,
|
||||
"creature_entry": 5000000,
|
||||
"summon_spell_id": 110000,
|
||||
"skill_line_ability_id": 30000
|
||||
},
|
||||
"moonkin_hatchling_alliance": {
|
||||
"model_data_id": 50001,
|
||||
"display_info_id": 50001,
|
||||
"creature_entry": 5000001,
|
||||
"summon_spell_id": 110001,
|
||||
"skill_line_ability_id": 30001,
|
||||
"spell_icon_id": 50000
|
||||
},
|
||||
"moonkin_hatchling_horde": {
|
||||
"model_data_id": 50002,
|
||||
"display_info_id": 50002,
|
||||
"creature_entry": 5000002,
|
||||
"summon_spell_id": 110002,
|
||||
"skill_line_ability_id": 30002,
|
||||
"spell_icon_id": 50001
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -76,11 +76,12 @@ New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
if (-not $SkipDataBuild) {
|
||||
Write-Host '[2/5] Building MPQ patches...'
|
||||
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
|
||||
if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' }
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if ($cargo) {
|
||||
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
|
||||
} elseif (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||
throw 'Cargo was not found and the MPQ builder is not compiled.'
|
||||
}
|
||||
|
||||
& $tool (Join-Path $repoRoot 'src') $PackagePath
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// FileDataID resolver for selected retail assets shipped in MoonWell patches.
|
||||
|
||||
#include "Host.hpp"
|
||||
#include "core/Logger.hpp"
|
||||
#include "mpq/MpqStore.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::host
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
std::unordered_map<uint32_t, std::string> g_paths;
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
|
||||
value.front() == '\r' || value.front() == '\n'))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
|
||||
value.back() == '\r' || value.back() == '\n'))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
const std::string root = wxl::host::ClientRoot();
|
||||
wxl::host::mpq::MpqStore store;
|
||||
std::vector<uint8_t> bytes;
|
||||
if (root.empty() || !store.Mount(root) || !store.ReadAll(kFileDataMap, bytes))
|
||||
{
|
||||
WLOG_WARN("moonwell: FileDataID map '%.*s' was not found",
|
||||
int(kFileDataMap.size()), kFileDataMap.data());
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
const size_t comma = line.find(',');
|
||||
if (comma == std::string_view::npos) continue;
|
||||
|
||||
const std::string_view idText = Trim(line.substr(0, comma));
|
||||
std::string_view pathText = Trim(line.substr(comma + 1));
|
||||
uint32_t fileDataId = 0;
|
||||
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
|
||||
fileDataId == 0 || pathText.empty())
|
||||
continue;
|
||||
|
||||
std::string path(pathText);
|
||||
for (char& c : path) if (c == '/') c = '\\';
|
||||
g_paths[fileDataId] = std::move(path);
|
||||
}
|
||||
|
||||
WLOG_INFO("moonwell: loaded %zu FileDataID path(s) from %.*s",
|
||||
g_paths.size(), int(kFileDataMap.size()), kFileDataMap.data());
|
||||
}
|
||||
|
||||
bool Resolve(uint32_t fileDataId, std::string& outPath)
|
||||
{
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
const auto found = g_paths.find(fileDataId);
|
||||
if (found == g_paths.end()) return false;
|
||||
outPath = found->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Registrar
|
||||
{
|
||||
Registrar() { wxl::host::RegisterResolver("moonwell-filedata", &Resolve); }
|
||||
};
|
||||
|
||||
Registrar g_registrar;
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,42 @@ if (-not $env:WOW_HOME) {
|
||||
$WOW_HOME = $env:WOW_HOME
|
||||
Write-Host "WOW_HOME = $WOW_HOME"
|
||||
|
||||
function Test-ExistingFileWriteAccess {
|
||||
param([Parameter(Mandatory=$true)][string]$Path)
|
||||
|
||||
try {
|
||||
$stream = [System.IO.File]::Open(
|
||||
$Path,
|
||||
[System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::ReadWrite
|
||||
)
|
||||
$stream.Close()
|
||||
return $true
|
||||
} catch [System.UnauthorizedAccessException] {
|
||||
return $false
|
||||
} catch {
|
||||
# Sharing violations are handled later by Stop-WowRuntime. They do not
|
||||
# indicate that the whole script needs an elevated token.
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$installedWarcraftXL = Join-Path $WOW_HOME "WarcraftXL.dll"
|
||||
$isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||||
)
|
||||
|
||||
if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
|
||||
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
|
||||
-not $isAdministrator) {
|
||||
Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..."
|
||||
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
|
||||
-WorkingDirectory $ROOT `
|
||||
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
|
||||
exit $elevatedProcess.ExitCode
|
||||
}
|
||||
|
||||
# --- Paths
|
||||
$SRC_DIR = Join-Path $ROOT "src"
|
||||
$DIST_DIR = Join-Path $ROOT "dist"
|
||||
@@ -44,11 +80,49 @@ $TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
||||
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
||||
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
|
||||
|
||||
# --- Ensure tool exists
|
||||
if (!(Test-Path $TOOL)) {
|
||||
Write-Host "Building Rust tool..."
|
||||
Push-Location (Join-Path $ROOT "tool")
|
||||
cargo build --release
|
||||
function Stop-WowRuntime {
|
||||
param([Parameter(Mandatory=$true)][string]$ClientPath)
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\'
|
||||
$runtimeProcesses = @(Get-Process -Name "Wow", "WarcraftXLHost" -ErrorAction SilentlyContinue | Where-Object {
|
||||
try {
|
||||
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
$false
|
||||
}
|
||||
})
|
||||
|
||||
if (-not $runtimeProcesses.Count) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping the running WoW client and WarcraftXL host..."
|
||||
$runtimeProcesses | Stop-Process -Force
|
||||
|
||||
try {
|
||||
$runtimeProcesses | Wait-Process -Timeout 10 -ErrorAction Stop
|
||||
} catch {
|
||||
$stillRunning = @($runtimeProcesses | Where-Object { -not $_.HasExited })
|
||||
if ($stillRunning.Count) {
|
||||
$processNames = ($stillRunning | ForEach-Object { "$($_.ProcessName) (PID $($_.Id))" }) -join ", "
|
||||
throw "Could not stop processes that lock WarcraftXL files: $processNames"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Always rebuild the tool so generated DBC/SQL formats match the current sources.
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if (-not $cargo) {
|
||||
throw "Cargo was not found; the MPQ/DBC builder cannot be refreshed."
|
||||
}
|
||||
Write-Host "Building Rust tool..."
|
||||
Push-Location (Join-Path $ROOT "tool")
|
||||
try {
|
||||
& $cargo.Source build --release
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Rust tool build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
@@ -68,6 +142,12 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
|
||||
|
||||
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
|
||||
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
|
||||
Stop-WowRuntime -ClientPath $WOW_HOME
|
||||
$creatureCache = Join-Path $WOW_HOME "Cache\WDB\ruRU\creaturecache.wdb"
|
||||
if (Test-Path -LiteralPath $creatureCache -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $creatureCache -Force
|
||||
Write-Host "Cleared stale creature cache: $creatureCache"
|
||||
}
|
||||
Write-Host "Building and packaging WarcraftXL..."
|
||||
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
|
||||
-PackagePath $DIST_DIR -Deploy
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.3 MiB |
Binary file not shown.
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"fileDataID": 3730958,
|
||||
"textures": [
|
||||
{
|
||||
"fileDataID": 3732460,
|
||||
"file": "catslime_3732460.blp"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732461,
|
||||
"file": "catslime_3732461.blp"
|
||||
}
|
||||
],
|
||||
"skins": [
|
||||
{
|
||||
"fileDataID": 3732472,
|
||||
"file": "catslime00.skin"
|
||||
}
|
||||
],
|
||||
"lodSkins": [
|
||||
{
|
||||
"fileDataID": 3732473,
|
||||
"file": "catslime_lod01.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732474,
|
||||
"file": "catslime_lod02.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732475,
|
||||
"file": "catslime_lod03.skin"
|
||||
}
|
||||
],
|
||||
"anims": [
|
||||
{
|
||||
"fileDataID": 3732462,
|
||||
"file": "catslime0097-00.anim",
|
||||
"animID": 97,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732463,
|
||||
"file": "catslime0096-00.anim",
|
||||
"animID": 96,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732464,
|
||||
"file": "catslime0098-00.anim",
|
||||
"animID": 98,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732465,
|
||||
"file": "catslime0100-00.anim",
|
||||
"animID": 100,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732466,
|
||||
"file": "catslime0099-00.anim",
|
||||
"animID": 99,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732467,
|
||||
"file": "catslime0101-00.anim",
|
||||
"animID": 101,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732468,
|
||||
"file": "catslime0069-00.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732469,
|
||||
"file": "catslime0123-00.anim",
|
||||
"animID": 123,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732470,
|
||||
"file": "catslime0061-00.anim",
|
||||
"animID": 61,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732471,
|
||||
"file": "catslime0075-00.anim",
|
||||
"animID": 75,
|
||||
"subAnimID": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"fileDataID": 467885,
|
||||
"textures": [
|
||||
{
|
||||
"fileDataID": 467884,
|
||||
"file": "babymoonkinne.blp"
|
||||
},
|
||||
{
|
||||
"fileDataID": 944915,
|
||||
"file": "armorreflect4.blp"
|
||||
}
|
||||
],
|
||||
"skins": [
|
||||
{
|
||||
"fileDataID": 473055,
|
||||
"file": "babymoonkin_ne00.skin"
|
||||
}
|
||||
],
|
||||
"lodSkins": [],
|
||||
"anims": [
|
||||
{
|
||||
"fileDataID": 473051,
|
||||
"file": "babymoonkin_ne0074-00.anim",
|
||||
"animID": 74,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473072,
|
||||
"file": "babymoonkin_ne0062-00.anim",
|
||||
"animID": 62,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473068,
|
||||
"file": "babymoonkin_ne0060-00.anim",
|
||||
"animID": 60,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473061,
|
||||
"file": "babymoonkin_ne0070-00.anim",
|
||||
"animID": 70,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473038,
|
||||
"file": "babymoonkin_ne0097-00.anim",
|
||||
"animID": 97,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473067,
|
||||
"file": "babymoonkin_ne0096-00.anim",
|
||||
"animID": 96,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473047,
|
||||
"file": "babymoonkin_ne0098-00.anim",
|
||||
"animID": 98,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473040,
|
||||
"file": "babymoonkin_ne0100-00.anim",
|
||||
"animID": 100,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473035,
|
||||
"file": "babymoonkin_ne0099-00.anim",
|
||||
"animID": 99,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473041,
|
||||
"file": "babymoonkin_ne0101-00.anim",
|
||||
"animID": 101,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473031,
|
||||
"file": "babymoonkin_ne0069-00.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473050,
|
||||
"file": "babymoonkin_ne0069-01.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 1
|
||||
},
|
||||
{
|
||||
"fileDataID": 473046,
|
||||
"file": "babymoonkin_ne0069-02.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 2
|
||||
},
|
||||
{
|
||||
"fileDataID": 473042,
|
||||
"file": "babymoonkin_ne0069-03.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 3
|
||||
},
|
||||
{
|
||||
"fileDataID": 473060,
|
||||
"file": "babymoonkin_ne0061-00.anim",
|
||||
"animID": 61,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473030,
|
||||
"file": "babymoonkin_ne0067-00.anim",
|
||||
"animID": 67,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473037,
|
||||
"file": "babymoonkin_ne0075-00.anim",
|
||||
"animID": 75,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473069,
|
||||
"file": "babymoonkin_ne0133-00.anim",
|
||||
"animID": 133,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473057,
|
||||
"file": "babymoonkin_ne0134-00.anim",
|
||||
"animID": 134,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473074,
|
||||
"file": "babymoonkin_ne0123-00.anim",
|
||||
"animID": 123,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473056,
|
||||
"file": "babymoonkin_ne0128-00.anim",
|
||||
"animID": 128,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473029,
|
||||
"file": "babymoonkin_ne0129-00.anim",
|
||||
"animID": 129,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473073,
|
||||
"file": "babymoonkin_ne0079-00.anim",
|
||||
"animID": 79,
|
||||
"subAnimID": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"fileDataID": 467887,
|
||||
"textures": [
|
||||
{
|
||||
"fileDataID": 467886,
|
||||
"file": "babymoonkinta.blp"
|
||||
},
|
||||
{
|
||||
"fileDataID": 249237,
|
||||
"file": "..\\..\\item\\objectcomponents\\shield\\armorreflect4.blp"
|
||||
}
|
||||
],
|
||||
"skins": [
|
||||
{
|
||||
"fileDataID": 473058,
|
||||
"file": "babymoonkin_ta00.skin"
|
||||
}
|
||||
],
|
||||
"lodSkins": [],
|
||||
"anims": [
|
||||
{
|
||||
"fileDataID": 473066,
|
||||
"file": "babymoonkin_ta0074-00.anim",
|
||||
"animID": 74,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473039,
|
||||
"file": "babymoonkin_ta0062-00.anim",
|
||||
"animID": 62,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473064,
|
||||
"file": "babymoonkin_ta0060-00.anim",
|
||||
"animID": 60,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473075,
|
||||
"file": "babymoonkin_ta0070-00.anim",
|
||||
"animID": 70,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473053,
|
||||
"file": "babymoonkin_ta0097-00.anim",
|
||||
"animID": 97,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473032,
|
||||
"file": "babymoonkin_ta0096-00.anim",
|
||||
"animID": 96,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473076,
|
||||
"file": "babymoonkin_ta0098-00.anim",
|
||||
"animID": 98,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473071,
|
||||
"file": "babymoonkin_ta0100-00.anim",
|
||||
"animID": 100,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473036,
|
||||
"file": "babymoonkin_ta0099-00.anim",
|
||||
"animID": 99,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473048,
|
||||
"file": "babymoonkin_ta0101-00.anim",
|
||||
"animID": 101,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473065,
|
||||
"file": "babymoonkin_ta0069-00.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473044,
|
||||
"file": "babymoonkin_ta0069-01.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 1
|
||||
},
|
||||
{
|
||||
"fileDataID": 473070,
|
||||
"file": "babymoonkin_ta0069-02.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 2
|
||||
},
|
||||
{
|
||||
"fileDataID": 473052,
|
||||
"file": "babymoonkin_ta0069-03.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 3
|
||||
},
|
||||
{
|
||||
"fileDataID": 473062,
|
||||
"file": "babymoonkin_ta0061-00.anim",
|
||||
"animID": 61,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473043,
|
||||
"file": "babymoonkin_ta0067-00.anim",
|
||||
"animID": 67,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473045,
|
||||
"file": "babymoonkin_ta0075-00.anim",
|
||||
"animID": 75,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473034,
|
||||
"file": "babymoonkin_ta0133-00.anim",
|
||||
"animID": 133,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473063,
|
||||
"file": "babymoonkin_ta0134-00.anim",
|
||||
"animID": 134,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473054,
|
||||
"file": "babymoonkin_ta0123-00.anim",
|
||||
"animID": 123,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473049,
|
||||
"file": "babymoonkin_ta0128-00.anim",
|
||||
"animID": 128,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473033,
|
||||
"file": "babymoonkin_ta0129-00.anim",
|
||||
"animID": 129,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 473059,
|
||||
"file": "babymoonkin_ta0079-00.anim",
|
||||
"animID": 79,
|
||||
"subAnimID": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
Binary file not shown.
@@ -9,6 +9,7 @@
|
||||
|
||||
AchievementFactions.lua
|
||||
MoonWellClient.lua
|
||||
SoloLFGCompat.lua
|
||||
EncounterJournal\EncounterJournalItemCache.lua
|
||||
EncounterJournal\EncounterJournalCompat.lua
|
||||
EncounterJournal\SharedXML\SoundKitConstants.lua
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
local SOLO_RANDOM_DUNGEONS = {
|
||||
[1258] = true,
|
||||
[1259] = true,
|
||||
[1260] = true,
|
||||
[1261] = true,
|
||||
[1262] = true,
|
||||
}
|
||||
|
||||
local GetStockTexCoordsForRole = GetTexCoordsForRole
|
||||
|
||||
function GetTexCoordsForRole(role)
|
||||
if role == "UNKNOWN" then
|
||||
local proposalExists, _, dungeonID = GetLFGProposal()
|
||||
if proposalExists and SOLO_RANDOM_DUNGEONS[dungeonID] then
|
||||
local _, tank, healer = GetLFGRoles()
|
||||
if tank then
|
||||
role = "TANK"
|
||||
elseif healer then
|
||||
role = "HEALER"
|
||||
else
|
||||
role = "DAMAGER"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return GetStockTexCoordsForRole(role)
|
||||
end
|
||||
Generated
+288
-7
@@ -82,6 +82,18 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -103,6 +115,12 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -170,6 +188,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -253,6 +277,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -313,12 +343,38 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.74.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"num-complex",
|
||||
"pulp",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -381,6 +437,27 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -416,6 +493,39 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-blp"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b863207a84a413c9e3e94909828ac796d24b1a6f53baa88f186d4cf61eca691f"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"image",
|
||||
"log",
|
||||
"nom",
|
||||
"spin",
|
||||
"texpresso",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "implode"
|
||||
version = "0.1.1"
|
||||
@@ -459,6 +569,15 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
@@ -484,6 +603,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "libbz2-rs-sys"
|
||||
version = "0.2.2"
|
||||
@@ -558,6 +683,12 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -568,6 +699,16 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -594,6 +735,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -665,6 +816,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -703,7 +860,20 @@ checksum = "5a7ff566c5cb263cf6a9f3b8b8ccbeaf236c0b3516c184134c062863258b3789"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"indicatif",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -740,6 +910,38 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulp"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"libm",
|
||||
"num-complex",
|
||||
"paste",
|
||||
"pulp-wasm-simd-flag",
|
||||
"raw-cpuid",
|
||||
"reborrow",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulp-wasm-simd-flag"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -819,6 +1021,15 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
@@ -839,13 +1050,19 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reborrow"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -874,7 +1091,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -915,6 +1132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -988,6 +1206,9 @@ name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
@@ -1035,13 +1256,43 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "texpresso"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8550677e2259d675a7841cb1403db35f330cc9e58674c8c5caa12dd12c51dc71"
|
||||
dependencies = [
|
||||
"libm",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1055,10 +1306,25 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"image-blp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"walkdir",
|
||||
"wow-mpq",
|
||||
]
|
||||
@@ -1206,7 +1472,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -1222,6 +1488,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
@@ -1377,7 +1649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -1434,7 +1706,7 @@ dependencies = [
|
||||
"rsa",
|
||||
"sha1",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1468,3 +1740,12 @@ name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -6,3 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
walkdir = "2.5.0"
|
||||
wow-mpq = "0.6.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
image = { version = "0.24.9", default-features = false, features = ["jpeg", "png", "tga"] }
|
||||
image-blp = "1.2.0"
|
||||
|
||||
@@ -0,0 +1,923 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const MODEL_FIELDS: u32 = 28;
|
||||
const MODEL_RECORD_SIZE: u32 = 112;
|
||||
const DISPLAY_FIELDS: u32 = 16;
|
||||
const DISPLAY_RECORD_SIZE: u32 = 64;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Registry {
|
||||
pub base_dbc_dir: String,
|
||||
#[serde(default = "default_model_id")]
|
||||
pub model_data_id_start: u32,
|
||||
#[serde(default = "default_display_id")]
|
||||
pub display_info_id_start: u32,
|
||||
#[serde(default = "default_creature_entry")]
|
||||
pub creature_entry_id_start: u32,
|
||||
#[serde(default = "default_summon_spell_id")]
|
||||
pub summon_spell_id_start: u32,
|
||||
#[serde(default = "default_skill_line_ability_id")]
|
||||
pub skill_line_ability_id_start: u32,
|
||||
#[serde(default = "default_spell_icon_id")]
|
||||
pub spell_icon_id_start: u32,
|
||||
pub defaults: Defaults,
|
||||
#[serde(default)]
|
||||
pub mounts: Vec<EntrySpec>,
|
||||
#[serde(default)]
|
||||
pub pets: Vec<EntrySpec>,
|
||||
}
|
||||
|
||||
fn default_model_id() -> u32 {
|
||||
50_000
|
||||
}
|
||||
fn default_display_id() -> u32 {
|
||||
50_000
|
||||
}
|
||||
fn default_creature_entry() -> u32 {
|
||||
5_000_000
|
||||
}
|
||||
fn default_summon_spell_id() -> u32 {
|
||||
110_000
|
||||
}
|
||||
fn default_skill_line_ability_id() -> u32 {
|
||||
30_000
|
||||
}
|
||||
fn default_spell_icon_id() -> u32 {
|
||||
50_000
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Defaults {
|
||||
pub mount_template_display_id: u32,
|
||||
pub pet_template_display_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum EntrySpec {
|
||||
Folder(String),
|
||||
Detailed(Box<EntryOptions>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct EntryOptions {
|
||||
pub folder: String,
|
||||
pub key: Option<String>,
|
||||
pub archive: Option<String>,
|
||||
pub model_file: Option<String>,
|
||||
pub template_display_id: Option<u32>,
|
||||
pub display_scale: Option<f32>,
|
||||
pub model_scale: Option<f32>,
|
||||
pub collision_width: Option<f32>,
|
||||
pub collision_height: Option<f32>,
|
||||
pub mount_height: Option<f32>,
|
||||
pub flags: Option<u32>,
|
||||
pub name: Option<String>,
|
||||
pub name_ru: Option<String>,
|
||||
pub race_mask: Option<u32>,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl EntrySpec {
|
||||
fn options(&self) -> EntryOptions {
|
||||
match self {
|
||||
EntrySpec::Folder(folder) => EntryOptions {
|
||||
folder: folder.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
EntrySpec::Detailed(options) => EntryOptions {
|
||||
folder: options.folder.clone(),
|
||||
key: options.key.clone(),
|
||||
archive: options.archive.clone(),
|
||||
model_file: options.model_file.clone(),
|
||||
template_display_id: options.template_display_id,
|
||||
display_scale: options.display_scale,
|
||||
model_scale: options.model_scale,
|
||||
collision_width: options.collision_width,
|
||||
collision_height: options.collision_height,
|
||||
mount_height: options.mount_height,
|
||||
flags: options.flags,
|
||||
name: options.name.clone(),
|
||||
name_ru: options.name_ru.clone(),
|
||||
race_mask: options.race_mask,
|
||||
icon: options.icon.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct LockFile {
|
||||
#[serde(default)]
|
||||
models: BTreeMap<String, LockedIds>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct LockedIds {
|
||||
model_data_id: u32,
|
||||
display_info_id: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
creature_entry: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
summon_spell_id: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
skill_line_ability_id: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
spell_icon_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PreparedEntry {
|
||||
key: String,
|
||||
folder: PathBuf,
|
||||
archive: String,
|
||||
model_file: String,
|
||||
file_data: Vec<(u32, String)>,
|
||||
assets: Vec<(PathBuf, String)>,
|
||||
template_display_id: u32,
|
||||
ids: LockedIds,
|
||||
options: EntryOptions,
|
||||
summon: bool,
|
||||
name: String,
|
||||
name_ru: String,
|
||||
icon_path: Option<String>,
|
||||
icon_source: Option<PathBuf>,
|
||||
icon_bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct GeneratedCreatures {
|
||||
pub assets: Vec<(PathBuf, String)>,
|
||||
pub icons: Vec<(PathBuf, Vec<u8>, String)>,
|
||||
pub file_data_csv: Vec<u8>,
|
||||
pub creature_model_data: Vec<u8>,
|
||||
pub creature_display_info: Vec<u8>,
|
||||
pub spell_overrides: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Wdbc {
|
||||
field_count: u32,
|
||||
record_size: u32,
|
||||
records: Vec<Vec<u8>>,
|
||||
strings: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Wdbc {
|
||||
fn read(path: &Path, fields: u32, record_size: u32) -> Result<Self, Box<dyn Error>> {
|
||||
let bytes = fs::read(path)?;
|
||||
if bytes.len() < 20 || &bytes[0..4] != b"WDBC" {
|
||||
return Err(format!("{} is not a WDBC file", path.display()).into());
|
||||
}
|
||||
let count = read_u32(&bytes, 4)? as usize;
|
||||
let actual_fields = read_u32(&bytes, 8)?;
|
||||
let actual_size = read_u32(&bytes, 12)?;
|
||||
let string_size = read_u32(&bytes, 16)? as usize;
|
||||
if actual_fields != fields || actual_size != record_size {
|
||||
return Err(format!(
|
||||
"{} has unexpected layout: fields={}, record_size={}",
|
||||
path.display(),
|
||||
actual_fields,
|
||||
actual_size
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let records_end = 20usize
|
||||
.checked_add(
|
||||
count
|
||||
.checked_mul(record_size as usize)
|
||||
.ok_or("DBC size overflow")?,
|
||||
)
|
||||
.ok_or("DBC size overflow")?;
|
||||
if records_end
|
||||
.checked_add(string_size)
|
||||
.ok_or("DBC size overflow")?
|
||||
!= bytes.len()
|
||||
{
|
||||
return Err(format!("{} has an invalid header size", path.display()).into());
|
||||
}
|
||||
let mut records = Vec::with_capacity(count + 16);
|
||||
for record in bytes[20..records_end].chunks_exact(record_size as usize) {
|
||||
records.push(record.to_vec());
|
||||
}
|
||||
Ok(Self {
|
||||
field_count: fields,
|
||||
record_size,
|
||||
records,
|
||||
strings: bytes[records_end..].to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn record(&self, id: u32) -> Option<&Vec<u8>> {
|
||||
self.records
|
||||
.iter()
|
||||
.find(|row| read_u32(row, 0).ok() == Some(id))
|
||||
}
|
||||
|
||||
fn add_string(&mut self, value: &str) -> u32 {
|
||||
if value.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let offset = self.strings.len() as u32;
|
||||
self.strings.extend_from_slice(value.as_bytes());
|
||||
self.strings.push(0);
|
||||
offset
|
||||
}
|
||||
|
||||
fn build(mut self) -> Vec<u8> {
|
||||
self.records
|
||||
.sort_by_key(|row| read_u32(row, 0).unwrap_or(0));
|
||||
let mut bytes = Vec::with_capacity(
|
||||
20 + self.records.len() * self.record_size as usize + self.strings.len(),
|
||||
);
|
||||
bytes.extend_from_slice(b"WDBC");
|
||||
bytes.extend_from_slice(&(self.records.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&self.field_count.to_le_bytes());
|
||||
bytes.extend_from_slice(&self.record_size.to_le_bytes());
|
||||
bytes.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
||||
for row in self.records {
|
||||
bytes.extend_from_slice(&row);
|
||||
}
|
||||
bytes.extend_from_slice(&self.strings);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, Box<dyn Error>> {
|
||||
let value = bytes
|
||||
.get(offset..offset + 4)
|
||||
.ok_or("record field is out of bounds")?;
|
||||
Ok(u32::from_le_bytes(value.try_into()?))
|
||||
}
|
||||
|
||||
fn write_u32(bytes: &mut [u8], field: usize, value: u32) -> Result<(), Box<dyn Error>> {
|
||||
let offset = field.checked_mul(4).ok_or("record field overflow")?;
|
||||
bytes
|
||||
.get_mut(offset..offset + 4)
|
||||
.ok_or("record field is out of bounds")?
|
||||
.copy_from_slice(&value.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_f32(bytes: &mut [u8], field: usize, value: f32) -> Result<(), Box<dyn Error>> {
|
||||
write_u32(bytes, field, value.to_bits())
|
||||
}
|
||||
|
||||
fn normalized_archive(value: &str) -> String {
|
||||
value.trim_matches(['/', '\\']).replace('\\', "/")
|
||||
}
|
||||
|
||||
fn resolve_path(project_root: &Path, value: &str) -> PathBuf {
|
||||
let path = PathBuf::from(value);
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
project_root.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_key(folder: &Path) -> Result<String, Box<dyn Error>> {
|
||||
Ok(folder
|
||||
.file_name()
|
||||
.ok_or("creature folder has no name")?
|
||||
.to_string_lossy()
|
||||
.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn default_archive(folder: &Path) -> Result<String, Box<dyn Error>> {
|
||||
Ok(format!(
|
||||
"Creature/{}",
|
||||
folder
|
||||
.file_name()
|
||||
.ok_or("creature folder has no name")?
|
||||
.to_string_lossy()
|
||||
))
|
||||
}
|
||||
|
||||
fn find_manifest(folder: &Path) -> Result<PathBuf, Box<dyn Error>> {
|
||||
let manifests: Vec<PathBuf> = fs::read_dir(folder)?
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.is_some_and(|name| name.to_string_lossy().ends_with(".manifest.json"))
|
||||
})
|
||||
.collect();
|
||||
match manifests.as_slice() {
|
||||
[manifest] => Ok(manifest.clone()),
|
||||
[] => Err(format!("no *.manifest.json in {}", folder.display()).into()),
|
||||
_ => Err(format!("more than one *.manifest.json in {}", folder.display()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_model(folder: &Path, explicit: Option<&str>) -> Result<String, Box<dyn Error>> {
|
||||
if let Some(model) = explicit {
|
||||
return Ok(model.to_string());
|
||||
}
|
||||
let models: Vec<String> = fs::read_dir(folder)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.path()
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("m2"))
|
||||
})
|
||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
match models.as_slice() {
|
||||
[model] => Ok(model.clone()),
|
||||
[] => Err(format!("no .m2 file in {}", folder.display()).into()),
|
||||
_ => Err(format!(
|
||||
"more than one .m2 file in {}; set model_file",
|
||||
folder.display()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_manifest_files(value: &Value, out: &mut Vec<(u32, String)>) {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
if let (Some(id), Some(file)) = (
|
||||
object.get("fileDataID").and_then(Value::as_u64),
|
||||
object.get("file").and_then(Value::as_str),
|
||||
) && id <= u32::MAX as u64
|
||||
{
|
||||
out.push((id as u32, file.to_string()));
|
||||
}
|
||||
for child in object.values() {
|
||||
collect_manifest_files(child, out);
|
||||
}
|
||||
}
|
||||
Value::Array(array) => {
|
||||
for child in array {
|
||||
collect_manifest_files(child, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_free(start: u32, used: &HashSet<u32>) -> u32 {
|
||||
let mut value = start;
|
||||
while used.contains(&value) {
|
||||
value = value.checked_add(1).expect("custom ID range exhausted");
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn allocate_ids(
|
||||
lock: &mut LockFile,
|
||||
key: &str,
|
||||
registry: &Registry,
|
||||
summon: bool,
|
||||
custom_icon: bool,
|
||||
) -> LockedIds {
|
||||
let used_models: HashSet<u32> = lock.models.values().map(|ids| ids.model_data_id).collect();
|
||||
let used_displays: HashSet<u32> = lock
|
||||
.models
|
||||
.values()
|
||||
.map(|ids| ids.display_info_id)
|
||||
.collect();
|
||||
let used_creatures: HashSet<u32> = lock
|
||||
.models
|
||||
.values()
|
||||
.filter_map(|ids| ids.creature_entry)
|
||||
.collect();
|
||||
let used_spells: HashSet<u32> = lock
|
||||
.models
|
||||
.values()
|
||||
.filter_map(|ids| ids.summon_spell_id)
|
||||
.collect();
|
||||
let used_skill_rows: HashSet<u32> = lock
|
||||
.models
|
||||
.values()
|
||||
.filter_map(|ids| ids.skill_line_ability_id)
|
||||
.collect();
|
||||
let used_spell_icons: HashSet<u32> = lock
|
||||
.models
|
||||
.values()
|
||||
.filter_map(|ids| ids.spell_icon_id)
|
||||
.collect();
|
||||
let mut ids = lock.models.get(key).cloned().unwrap_or(LockedIds {
|
||||
model_data_id: next_free(registry.model_data_id_start, &used_models),
|
||||
display_info_id: next_free(registry.display_info_id_start, &used_displays),
|
||||
creature_entry: None,
|
||||
summon_spell_id: None,
|
||||
skill_line_ability_id: None,
|
||||
spell_icon_id: None,
|
||||
});
|
||||
if summon {
|
||||
if ids.creature_entry.is_none() {
|
||||
ids.creature_entry = Some(next_free(registry.creature_entry_id_start, &used_creatures));
|
||||
}
|
||||
if ids.summon_spell_id.is_none() {
|
||||
ids.summon_spell_id = Some(next_free(registry.summon_spell_id_start, &used_spells));
|
||||
}
|
||||
if ids.skill_line_ability_id.is_none() {
|
||||
ids.skill_line_ability_id = Some(next_free(
|
||||
registry.skill_line_ability_id_start,
|
||||
&used_skill_rows,
|
||||
));
|
||||
}
|
||||
if custom_icon && ids.spell_icon_id.is_none() {
|
||||
ids.spell_icon_id = Some(next_free(registry.spell_icon_id_start, &used_spell_icons));
|
||||
}
|
||||
}
|
||||
lock.models.insert(key.to_string(), ids.clone());
|
||||
ids
|
||||
}
|
||||
|
||||
fn validate_text(value: &str, field: &str, key: &str) -> Result<(), Box<dyn Error>> {
|
||||
if value.contains(['\t', '\r', '\n']) {
|
||||
return Err(format!("{} for '{}' contains a tab or newline", field, key).into());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_icon_path(project_root: &Path, folder: &Path, value: &str) -> PathBuf {
|
||||
let path = PathBuf::from(value);
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
let beside_model = folder.join(&path);
|
||||
if beside_model.is_file() {
|
||||
beside_model
|
||||
} else {
|
||||
project_root.join(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_file_stem(key: &str) -> String {
|
||||
let sanitized: String = key
|
||||
.chars()
|
||||
.map(|character| {
|
||||
if character.is_ascii_alphanumeric() || character == '_' {
|
||||
character
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("MoonWell_{sanitized}")
|
||||
}
|
||||
|
||||
fn prepare_entry(
|
||||
project_root: &Path,
|
||||
spec: &EntrySpec,
|
||||
template_default: u32,
|
||||
summon: bool,
|
||||
lock: &mut LockFile,
|
||||
registry: &Registry,
|
||||
) -> Result<PreparedEntry, Box<dyn Error>> {
|
||||
let options = spec.options();
|
||||
let folder = resolve_path(project_root, &options.folder).canonicalize()?;
|
||||
if !folder.is_dir() {
|
||||
return Err(format!("creature folder not found: {}", folder.display()).into());
|
||||
}
|
||||
let key = options.key.clone().unwrap_or(default_key(&folder)?);
|
||||
let name = options.name.clone().unwrap_or_else(|| key.clone());
|
||||
let name_ru = options.name_ru.clone().unwrap_or_else(|| name.clone());
|
||||
validate_text(&name, "name", &key)?;
|
||||
validate_text(&name_ru, "name_ru", &key)?;
|
||||
let archive = normalized_archive(
|
||||
options
|
||||
.archive
|
||||
.as_deref()
|
||||
.unwrap_or(&default_archive(&folder)?),
|
||||
);
|
||||
let model_file = find_model(&folder, options.model_file.as_deref())?;
|
||||
let manifest_path = find_manifest(&folder)?;
|
||||
let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?;
|
||||
let main_id = manifest
|
||||
.get("fileDataID")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| format!("{} has no root fileDataID", manifest_path.display()))?;
|
||||
if main_id > u32::MAX as u64 {
|
||||
return Err("model FileDataID is too large".into());
|
||||
}
|
||||
|
||||
let mut dependencies = Vec::new();
|
||||
collect_manifest_files(&manifest, &mut dependencies);
|
||||
let mut file_data = vec![(main_id as u32, format!("{archive}/{model_file}"))];
|
||||
let mut assets = vec![(folder.join(&model_file), format!("{archive}/{model_file}"))];
|
||||
for (file_data_id, file) in dependencies {
|
||||
let source = folder.join(&file);
|
||||
if !source.is_file() {
|
||||
return Err(format!("manifest dependency not found: {}", source.display()).into());
|
||||
}
|
||||
file_data.push((file_data_id, format!("{archive}/{file}")));
|
||||
assets.push((source, format!("{archive}/{file}")));
|
||||
}
|
||||
if options.icon.is_some() && !summon {
|
||||
return Err(format!("icon is only supported for pet '{}'", key).into());
|
||||
}
|
||||
let (icon_path, icon_source, icon_bytes) = if let Some(value) = options.icon.as_deref() {
|
||||
let source = resolve_icon_path(project_root, &folder, value);
|
||||
if !source.is_file() {
|
||||
return Err(format!("icon source not found: {}", source.display()).into());
|
||||
}
|
||||
let stem = icon_file_stem(&key);
|
||||
(
|
||||
Some(format!("Interface\\Icons\\{stem}")),
|
||||
Some(source.clone()),
|
||||
Some(crate::icon::convert_icon_bytes(&source)?),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
let ids = allocate_ids(lock, &key, registry, summon, icon_path.is_some());
|
||||
Ok(PreparedEntry {
|
||||
key,
|
||||
folder,
|
||||
archive,
|
||||
model_file,
|
||||
file_data,
|
||||
assets,
|
||||
template_display_id: options.template_display_id.unwrap_or(template_default),
|
||||
ids,
|
||||
options,
|
||||
summon,
|
||||
name,
|
||||
name_ru,
|
||||
icon_path,
|
||||
icon_source,
|
||||
icon_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_dbc(
|
||||
base_dir: &Path,
|
||||
entries: &[PreparedEntry],
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn Error>> {
|
||||
let mut models = Wdbc::read(
|
||||
&base_dir.join("CreatureModelData.dbc"),
|
||||
MODEL_FIELDS,
|
||||
MODEL_RECORD_SIZE,
|
||||
)?;
|
||||
let mut displays = Wdbc::read(
|
||||
&base_dir.join("CreatureDisplayInfo.dbc"),
|
||||
DISPLAY_FIELDS,
|
||||
DISPLAY_RECORD_SIZE,
|
||||
)?;
|
||||
let base_models = models.clone();
|
||||
let base_displays = displays.clone();
|
||||
|
||||
for entry in entries {
|
||||
if base_models.record(entry.ids.model_data_id).is_some()
|
||||
|| base_displays.record(entry.ids.display_info_id).is_some()
|
||||
{
|
||||
return Err(format!("custom IDs for '{}' collide with base DBC", entry.key).into());
|
||||
}
|
||||
let template_display = base_displays
|
||||
.record(entry.template_display_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"template display {} not found for '{}'",
|
||||
entry.template_display_id, entry.key
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
let template_model_id = read_u32(&template_display, 4)?;
|
||||
let mut model = base_models
|
||||
.record(template_model_id)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"model {} for template display {} not found",
|
||||
template_model_id, entry.template_display_id
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
let mut display = template_display;
|
||||
|
||||
let model_stem = Path::new(&entry.model_file)
|
||||
.file_stem()
|
||||
.ok_or("model file has no stem")?
|
||||
.to_string_lossy();
|
||||
let dbc_model_path = format!("{}\\{}.mdx", entry.archive.replace('/', "\\"), model_stem);
|
||||
let string_offset = models.add_string(&dbc_model_path);
|
||||
write_u32(&mut model, 0, entry.ids.model_data_id)?;
|
||||
write_u32(&mut model, 2, string_offset)?;
|
||||
if let Some(value) = entry.options.flags {
|
||||
write_u32(&mut model, 1, value)?;
|
||||
}
|
||||
if let Some(value) = entry.options.model_scale {
|
||||
write_f32(&mut model, 4, value)?;
|
||||
}
|
||||
if let Some(value) = entry.options.collision_width {
|
||||
write_f32(&mut model, 14, value)?;
|
||||
}
|
||||
if let Some(value) = entry.options.collision_height {
|
||||
write_f32(&mut model, 15, value)?;
|
||||
}
|
||||
if let Some(value) = entry.options.mount_height {
|
||||
write_f32(&mut model, 16, value)?;
|
||||
}
|
||||
|
||||
write_u32(&mut display, 0, entry.ids.display_info_id)?;
|
||||
write_u32(&mut display, 1, entry.ids.model_data_id)?;
|
||||
if let Some(value) = entry.options.display_scale {
|
||||
write_f32(&mut display, 4, value)?;
|
||||
}
|
||||
// Modern models carry their textures through TXID/FileDataID. Template skin names must not override them.
|
||||
for field in 6..=9 {
|
||||
write_u32(&mut display, field, 0)?;
|
||||
}
|
||||
|
||||
models.records.push(model);
|
||||
displays.records.push(display);
|
||||
}
|
||||
Ok((models.build(), displays.build()))
|
||||
}
|
||||
|
||||
fn sql_string(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\\', "\\\\").replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn generate_server_sql(entries: &[PreparedEntry]) -> Result<String, Box<dyn Error>> {
|
||||
let mut sql = String::from(
|
||||
"-- Generated by MoonWell custom creature builder.\n\
|
||||
-- Apply to acore_world, then restart ac-worldserver.\n\
|
||||
SET NAMES utf8mb4;\n\
|
||||
START TRANSACTION;\n\n",
|
||||
);
|
||||
for entry in entries.iter().filter(|entry| entry.summon) {
|
||||
let creature = entry
|
||||
.ids
|
||||
.creature_entry
|
||||
.ok_or("pet has no creature entry")?;
|
||||
let spell = entry
|
||||
.ids
|
||||
.summon_spell_id
|
||||
.ok_or("pet has no summon spell ID")?;
|
||||
let skill_row = entry
|
||||
.ids
|
||||
.skill_line_ability_id
|
||||
.ok_or("pet has no skill-line row ID")?;
|
||||
let name_en = sql_string(&entry.name);
|
||||
let name_ru = sql_string(&entry.name_ru);
|
||||
let description_en = sql_string(&format!(
|
||||
"Summons or dismisses your companion: {}.",
|
||||
entry.name
|
||||
));
|
||||
let description_ru = sql_string(&format!(
|
||||
"Призывает или отпускает спутника: {}.",
|
||||
entry.name_ru
|
||||
));
|
||||
let race_mask = entry.options.race_mask.unwrap_or(0);
|
||||
let spell_icon_id = entry.ids.spell_icon_id.unwrap_or(2686);
|
||||
|
||||
sql.push_str(&format!(
|
||||
"-- {}\n\
|
||||
INSERT INTO creature_template\n\
|
||||
(entry,name,minlevel,maxlevel,faction,speed_walk,speed_run,unit_class,unit_flags,unit_flags2,type,MovementType,RegenHealth,VerifiedBuild)\n\
|
||||
VALUES ({creature},{name_en},1,1,188,1,1.14286,1,0,2048,12,0,1,12340)\n\
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name),faction=VALUES(faction),type=VALUES(type),unit_flags2=VALUES(unit_flags2);\n\
|
||||
INSERT INTO creature_template_locale\n\
|
||||
(entry,locale,Name,Title,VerifiedBuild)\n\
|
||||
VALUES ({creature},'ruRU',{name_ru},'',12340)\n\
|
||||
ON DUPLICATE KEY UPDATE Name=VALUES(Name),Title=VALUES(Title),VerifiedBuild=VALUES(VerifiedBuild);\n\
|
||||
INSERT INTO creature_model_info\n\
|
||||
(DisplayID,BoundingRadius,CombatReach,Gender,DisplayID_Other_Gender,VerifiedBuild)\n\
|
||||
VALUES ({},0.25,0.5,2,0,12340)\n\
|
||||
ON DUPLICATE KEY UPDATE BoundingRadius=VALUES(BoundingRadius),CombatReach=VALUES(CombatReach),Gender=VALUES(Gender),DisplayID_Other_Gender=VALUES(DisplayID_Other_Gender),VerifiedBuild=VALUES(VerifiedBuild);\n\
|
||||
INSERT INTO creature_template_model\n\
|
||||
(CreatureID,Idx,CreatureDisplayID,DisplayScale,Probability,VerifiedBuild)\n\
|
||||
VALUES ({creature},0,{},1,1,12340)\n\
|
||||
ON DUPLICATE KEY UPDATE CreatureDisplayID=VALUES(CreatureDisplayID),DisplayScale=VALUES(DisplayScale),Probability=VALUES(Probability);\n\n",
|
||||
entry.key, entry.ids.display_info_id, entry.ids.display_info_id
|
||||
));
|
||||
sql.push_str(&format!(
|
||||
"INSERT INTO spell_dbc\n\
|
||||
(ID,Attributes,AttributesEx3,CastingTimeIndex,InterruptFlags,ProcChance,DurationIndex,RangeIndex,EquippedItemClass,Effect_1,EffectDieSides_1,EffectBasePoints_1,ImplicitTargetA_1,EffectMultipleValue_1,EffectMiscValue_1,EffectMiscValueB_1,SpellVisualID_1,SpellIconID,Name_Lang_enUS,Name_Lang_ruRU,Description_Lang_enUS,Description_Lang_ruRU,StartRecoveryCategory,StartRecoveryTime,EffectChainAmplitude_1,EffectChainAmplitude_2,EffectChainAmplitude_3,SchoolMask)\n\
|
||||
VALUES ({spell},262416,536870912,1,31,101,21,1,-1,28,1,1,32,1000,{creature},41,353,{spell_icon_id},{name_en},{name_ru},{description_en},{description_ru},133,1500,1,1,1,1)\n\
|
||||
ON DUPLICATE KEY UPDATE EffectMiscValue_1=VALUES(EffectMiscValue_1),SpellIconID=VALUES(SpellIconID),Name_Lang_enUS=VALUES(Name_Lang_enUS),Name_Lang_ruRU=VALUES(Name_Lang_ruRU),Description_Lang_enUS=VALUES(Description_Lang_enUS),Description_Lang_ruRU=VALUES(Description_Lang_ruRU);\n\
|
||||
INSERT INTO skilllineability_dbc\n\
|
||||
(ID,SkillLine,Spell,RaceMask,ClassMask,ExcludeRace,ExcludeClass,MinSkillLineRank,SupercededBySpell,AcquireMethod,TrivialSkillLineRankHigh,TrivialSkillLineRankLow,CharacterPoints_1,CharacterPoints_2)\n\
|
||||
VALUES ({skill_row},778,{spell},{race_mask},0,0,0,1,0,0,0,0,0,0)\n\
|
||||
ON DUPLICATE KEY UPDATE SkillLine=VALUES(SkillLine),Spell=VALUES(Spell),RaceMask=VALUES(RaceMask),MinSkillLineRank=VALUES(MinSkillLineRank);\n\n"
|
||||
));
|
||||
}
|
||||
sql.push_str("COMMIT;\n");
|
||||
Ok(sql)
|
||||
}
|
||||
|
||||
pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dyn Error>> {
|
||||
let registry_path = project_root.join("custom-creatures.json");
|
||||
if !registry_path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
let registry: Registry = serde_json::from_slice(&fs::read(®istry_path)?)?;
|
||||
let lock_path = project_root.join("custom-creatures.lock.json");
|
||||
let mut lock: LockFile = if lock_path.is_file() {
|
||||
serde_json::from_slice(&fs::read(&lock_path)?)?
|
||||
} else {
|
||||
LockFile::default()
|
||||
};
|
||||
let original_lock = serde_json::to_vec(&lock)?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut keys = HashSet::new();
|
||||
for spec in ®istry.mounts {
|
||||
let entry = prepare_entry(
|
||||
project_root,
|
||||
spec,
|
||||
registry.defaults.mount_template_display_id,
|
||||
false,
|
||||
&mut lock,
|
||||
®istry,
|
||||
)?;
|
||||
if !keys.insert(entry.key.clone()) {
|
||||
return Err(format!("duplicate creature key '{}'", entry.key).into());
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
for spec in ®istry.pets {
|
||||
let entry = prepare_entry(
|
||||
project_root,
|
||||
spec,
|
||||
registry.defaults.pet_template_display_id,
|
||||
true,
|
||||
&mut lock,
|
||||
®istry,
|
||||
)?;
|
||||
if !keys.insert(entry.key.clone()) {
|
||||
return Err(format!("duplicate creature key '{}'", entry.key).into());
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
let updated_lock = serde_json::to_vec(&lock)?;
|
||||
if updated_lock != original_lock || !lock_path.exists() {
|
||||
let mut pretty = serde_json::to_vec_pretty(&lock)?;
|
||||
pretty.push(b'\n');
|
||||
fs::write(&lock_path, pretty)?;
|
||||
println!("Updated creature ID lock: {}", lock_path.display());
|
||||
}
|
||||
|
||||
let mut all_ids = BTreeMap::new();
|
||||
let mut active_model_ids = HashSet::new();
|
||||
let mut active_display_ids = HashSet::new();
|
||||
let mut active_creature_entries = HashSet::new();
|
||||
let mut active_spell_ids = HashSet::new();
|
||||
let mut active_skill_rows = HashSet::new();
|
||||
let mut active_spell_icon_ids = HashSet::new();
|
||||
let mut assets = Vec::new();
|
||||
let mut icons = Vec::new();
|
||||
for entry in &entries {
|
||||
if !active_model_ids.insert(entry.ids.model_data_id) {
|
||||
return Err(format!(
|
||||
"duplicate modelDataID {} in creature lock",
|
||||
entry.ids.model_data_id
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if !active_display_ids.insert(entry.ids.display_info_id) {
|
||||
return Err(format!(
|
||||
"duplicate displayInfoID {} in creature lock",
|
||||
entry.ids.display_info_id
|
||||
)
|
||||
.into());
|
||||
}
|
||||
for (value, used, label) in [
|
||||
(
|
||||
entry.ids.creature_entry,
|
||||
&mut active_creature_entries,
|
||||
"creature entry",
|
||||
),
|
||||
(
|
||||
entry.ids.summon_spell_id,
|
||||
&mut active_spell_ids,
|
||||
"summon spell ID",
|
||||
),
|
||||
(
|
||||
entry.ids.skill_line_ability_id,
|
||||
&mut active_skill_rows,
|
||||
"skill-line row ID",
|
||||
),
|
||||
(
|
||||
entry.ids.spell_icon_id,
|
||||
&mut active_spell_icon_ids,
|
||||
"spell icon ID",
|
||||
),
|
||||
] {
|
||||
if let Some(value) = value
|
||||
&& !used.insert(value)
|
||||
{
|
||||
return Err(format!("duplicate {label} {value} in creature lock").into());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Creature {}: modelDataID={}, displayInfoID={}, source={}",
|
||||
entry.key,
|
||||
entry.ids.model_data_id,
|
||||
entry.ids.display_info_id,
|
||||
entry.folder.display()
|
||||
);
|
||||
for (id, path) in &entry.file_data {
|
||||
if let Some(existing) = all_ids.get(id) {
|
||||
if existing != path {
|
||||
return Err(format!(
|
||||
"FileDataID {} maps to both '{}' and '{}'",
|
||||
id, existing, path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
all_ids.insert(*id, path.clone());
|
||||
}
|
||||
}
|
||||
assets.extend(entry.assets.clone());
|
||||
if let (Some(source), Some(bytes), Some(path)) = (
|
||||
entry.icon_source.as_ref(),
|
||||
entry.icon_bytes.as_ref(),
|
||||
entry.icon_path.as_ref(),
|
||||
) {
|
||||
icons.push((
|
||||
source.clone(),
|
||||
bytes.clone(),
|
||||
format!("{}.blp", path.replace('\\', "/")),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut csv = String::from("# FileDataID,archive path\n");
|
||||
for (id, path) in all_ids {
|
||||
csv.push_str(&format!("{id},{}\n", path.replace('/', "\\")));
|
||||
}
|
||||
let mut spell_overrides = String::from(
|
||||
"# spellId\ttemplateSpellId\tcreatureEntry\tskillLineAbilityId\traceMask\tspellIconId\ticonPath\tname\n",
|
||||
);
|
||||
for entry in entries.iter().filter(|entry| entry.summon) {
|
||||
spell_overrides.push_str(&format!(
|
||||
"{}\t70613\t{}\t{}\t{}\t{}\t{}\t{}\n",
|
||||
entry
|
||||
.ids
|
||||
.summon_spell_id
|
||||
.ok_or("pet has no summon spell ID")?,
|
||||
entry
|
||||
.ids
|
||||
.creature_entry
|
||||
.ok_or("pet has no creature entry")?,
|
||||
entry
|
||||
.ids
|
||||
.skill_line_ability_id
|
||||
.ok_or("pet has no skill-line row ID")?,
|
||||
entry.options.race_mask.unwrap_or(0),
|
||||
entry.ids.spell_icon_id.unwrap_or(0),
|
||||
entry.icon_path.as_deref().unwrap_or("-"),
|
||||
entry.name_ru
|
||||
));
|
||||
}
|
||||
|
||||
let base_dir = resolve_path(project_root, ®istry.base_dbc_dir);
|
||||
let (model_dbc, display_dbc) = generate_dbc(&base_dir, &entries)?;
|
||||
// Keep server-only artifacts outside the client package directory. Both run.ps1 and deploy.ps1
|
||||
// mirror the complete package directory into the client and launcher manifest.
|
||||
let server_dir = project_root.join("build").join("server-dbc");
|
||||
fs::create_dir_all(&server_dir)?;
|
||||
fs::write(server_dir.join("CreatureModelData.dbc"), &model_dbc)?;
|
||||
fs::write(server_dir.join("CreatureDisplayInfo.dbc"), &display_dbc)?;
|
||||
fs::write(
|
||||
server_dir.join("custom-creatures.sql"),
|
||||
generate_server_sql(&entries)?,
|
||||
)?;
|
||||
let mut id_map = String::from(
|
||||
"key,type,modelDataID,displayInfoID,creatureEntry,summonSpellID,skillLineAbilityID,spellIconID\n",
|
||||
);
|
||||
for (kind, specs) in [("mount", ®istry.mounts), ("pet", ®istry.pets)] {
|
||||
for spec in specs {
|
||||
let options = spec.options();
|
||||
let folder = resolve_path(project_root, &options.folder).canonicalize()?;
|
||||
let key = options.key.unwrap_or(default_key(&folder)?);
|
||||
let ids = lock.models.get(&key).ok_or("lock entry disappeared")?;
|
||||
id_map.push_str(&format!(
|
||||
"{key},{kind},{},{},{},{},{},{}\n",
|
||||
ids.model_data_id,
|
||||
ids.display_info_id,
|
||||
ids.creature_entry
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
ids.summon_spell_id
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
ids.skill_line_ability_id
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
ids.spell_icon_id
|
||||
.map_or_else(String::new, |value| value.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
fs::write(server_dir.join("custom-creatures.csv"), id_map)?;
|
||||
|
||||
Ok(Some(GeneratedCreatures {
|
||||
assets,
|
||||
icons,
|
||||
file_data_csv: csv.into_bytes(),
|
||||
creature_model_data: model_dbc,
|
||||
creature_display_info: display_dbc,
|
||||
spell_overrides: spell_overrides.into_bytes(),
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use image::GenericImageView;
|
||||
use image::imageops::FilterType as ImageFilterType;
|
||||
use image_blp::convert::{Blp2Format, BlpTarget, FilterType, image_to_blp};
|
||||
use image_blp::encode::encode_blp;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const MAX_ICON_SIZE: u32 = 512;
|
||||
|
||||
struct EncodedIcon {
|
||||
bytes: Vec<u8>,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
dimension: u32,
|
||||
}
|
||||
|
||||
fn target_dimension(width: u32, height: u32) -> Result<u32, Box<dyn Error>> {
|
||||
let largest = width.max(height).max(4);
|
||||
let dimension = largest
|
||||
.checked_next_power_of_two()
|
||||
.ok_or("icon dimensions are too large")?;
|
||||
if dimension > MAX_ICON_SIZE {
|
||||
return Err(format!(
|
||||
"icon is too large: {width}x{height}; maximum converted size is {MAX_ICON_SIZE}x{MAX_ICON_SIZE}"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(dimension)
|
||||
}
|
||||
|
||||
fn encode_icon(input: &Path) -> Result<EncodedIcon, Box<dyn Error>> {
|
||||
let source = image::io::Reader::open(input)?
|
||||
.with_guessed_format()?
|
||||
.decode()?;
|
||||
let (width, height) = source.dimensions();
|
||||
let dimension = target_dimension(width, height)?;
|
||||
let resized = source.resize_exact(dimension, dimension, ImageFilterType::Lanczos3);
|
||||
let blp = image_to_blp(
|
||||
resized,
|
||||
true,
|
||||
BlpTarget::Blp2(Blp2Format::Dxt1 {
|
||||
has_alpha: false,
|
||||
compress_algorithm: Default::default(),
|
||||
}),
|
||||
FilterType::Lanczos3,
|
||||
)?;
|
||||
Ok(EncodedIcon {
|
||||
bytes: encode_blp(&blp)?,
|
||||
source_width: width,
|
||||
source_height: height,
|
||||
dimension,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_icon_bytes(input: &Path) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
Ok(encode_icon(input)?.bytes)
|
||||
}
|
||||
|
||||
pub fn default_output_path(input: &Path) -> PathBuf {
|
||||
input.with_extension("blp")
|
||||
}
|
||||
|
||||
pub fn convert_icon(input: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
|
||||
if !input.is_file() {
|
||||
return Err(format!("icon source not found: {}", input.display()).into());
|
||||
}
|
||||
let encoded = encode_icon(input)?;
|
||||
if let Some(parent) = output.parent()
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(output, encoded.bytes)?;
|
||||
println!(
|
||||
"Converted icon: {} ({}x{}) -> {} (BLP2 DXT1, {}x{}, mipmaps)",
|
||||
input.display(),
|
||||
encoded.source_width,
|
||||
encoded.source_height,
|
||||
output.display(),
|
||||
encoded.dimension,
|
||||
encoded.dimension
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rounds_icon_dimensions_to_supported_power_of_two() {
|
||||
assert_eq!(target_dimension(56, 56).unwrap(), 64);
|
||||
assert_eq!(target_dimension(128, 64).unwrap(), 128);
|
||||
assert!(target_dimension(513, 512).is_err());
|
||||
}
|
||||
}
|
||||
+102
-3
@@ -7,6 +7,10 @@ use walkdir::WalkDir;
|
||||
use wow_mpq::compression::flags;
|
||||
use wow_mpq::{ArchiveBuilder, FormatVersion, ListfileOption};
|
||||
|
||||
mod creatures;
|
||||
mod icon;
|
||||
use creatures::GeneratedCreatures;
|
||||
|
||||
/// Checks if a directory name looks like a WoW locale folder (e.g. ruRU, enUS, deDE).
|
||||
fn is_locale_dir(name: &str) -> bool {
|
||||
if name.len() != 4 {
|
||||
@@ -19,7 +23,11 @@ fn is_locale_dir(name: &str) -> bool {
|
||||
&& b[3].is_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn build_mpq(input_dir: &Path, output_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
fn build_mpq(
|
||||
input_dir: &Path,
|
||||
output_path: &Path,
|
||||
creatures: Option<&GeneratedCreatures>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut output = output_path.to_path_buf();
|
||||
output.set_extension("MPQ");
|
||||
|
||||
@@ -36,12 +44,26 @@ fn build_mpq(input_dir: &Path, output_path: &Path) -> Result<(), Box<dyn Error>>
|
||||
.default_compression(flags::ZLIB)
|
||||
.listfile_option(ListfileOption::Generate);
|
||||
|
||||
let loose_sources: std::collections::HashSet<PathBuf> = creatures
|
||||
.into_iter()
|
||||
.flat_map(|generated| generated.assets.iter())
|
||||
.filter_map(|(source, _)| source.canonicalize().ok())
|
||||
.collect();
|
||||
let generated_icon_sources: std::collections::HashSet<PathBuf> = creatures
|
||||
.into_iter()
|
||||
.flat_map(|generated| generated.icons.iter())
|
||||
.filter_map(|(source, _, _)| source.canonicalize().ok())
|
||||
.collect();
|
||||
|
||||
for entry in WalkDir::new(&base)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let full_path = entry.path().canonicalize()?;
|
||||
if loose_sources.contains(&full_path) || generated_icon_sources.contains(&full_path) {
|
||||
continue;
|
||||
}
|
||||
let rel_path = match full_path.strip_prefix(&base) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -50,20 +72,82 @@ fn build_mpq(input_dir: &Path, output_path: &Path) -> Result<(), Box<dyn Error>>
|
||||
}
|
||||
};
|
||||
let archive_path = rel_path.to_string_lossy().replace('\\', "/");
|
||||
if archive_path.ends_with(".manifest.json") || archive_path == "WXLFileData.csv" {
|
||||
continue;
|
||||
}
|
||||
println!(" Adding: {}", archive_path);
|
||||
builder = builder.add_file(&full_path, &archive_path);
|
||||
}
|
||||
|
||||
if let Some(generated) = creatures {
|
||||
for (_, bytes, archive_path) in &generated.icons {
|
||||
println!(" Adding generated icon: {archive_path}");
|
||||
builder = builder.add_file_data(bytes.clone(), archive_path);
|
||||
}
|
||||
println!(" Adding generated: WXLFileData.csv");
|
||||
builder = builder.add_file_data(generated.file_data_csv.clone(), "WXLFileData.csv");
|
||||
println!(" Adding generated: WXLSpellOverrides.tsv");
|
||||
builder = builder.add_file_data(generated.spell_overrides.clone(), "WXLSpellOverrides.tsv");
|
||||
println!(" Adding generated: DBFilesClient/CreatureModelData.dbc");
|
||||
builder = builder.add_file_data(
|
||||
generated.creature_model_data.clone(),
|
||||
"DBFilesClient/CreatureModelData.dbc",
|
||||
);
|
||||
println!(" Adding generated: DBFilesClient/CreatureDisplayInfo.dbc");
|
||||
builder = builder.add_file_data(
|
||||
generated.creature_display_info.clone(),
|
||||
"DBFilesClient/CreatureDisplayInfo.dbc",
|
||||
);
|
||||
}
|
||||
|
||||
builder.build(&output)?;
|
||||
println!("Created: {}", output.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stage_loose_assets(
|
||||
output_dir: &Path,
|
||||
creatures: &GeneratedCreatures,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let loose_root = output_dir.join("Data").join("Patch-WXL.MPQ");
|
||||
for (source, archive_path) in &creatures.assets {
|
||||
let destination = loose_root.join(archive_path.replace('/', "\\"));
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(source, &destination)?;
|
||||
println!("Staging loose modern asset: {}", archive_path);
|
||||
if source
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("m2"))
|
||||
{
|
||||
let mdx_alias = destination.with_extension("mdx");
|
||||
fs::copy(source, &mdx_alias)?;
|
||||
println!("Staging WoW 3.3.5 model alias: {}", mdx_alias.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.get(1).is_some_and(|arg| arg == "icon") {
|
||||
if !(args.len() == 3 || args.len() == 4) {
|
||||
eprintln!("Usage: tool icon <input.jpg|png|tga> [output.blp]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let input = PathBuf::from(&args[2]);
|
||||
let output = args
|
||||
.get(3)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| icon::default_output_path(&input));
|
||||
return icon::convert_icon(&input, &output);
|
||||
}
|
||||
|
||||
if args.len() != 3 {
|
||||
eprintln!("Usage: tool <src_dir> <output_dir>");
|
||||
eprintln!(" tool icon <input.jpg|png|tga> [output.blp]");
|
||||
eprintln!();
|
||||
eprintln!(" src_dir — root of source tree (must contain Data/)");
|
||||
eprintln!(" output_dir — destination root; MPQs are placed mirroring the src structure");
|
||||
@@ -72,6 +156,16 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let src_dir = PathBuf::from(&args[1]);
|
||||
let output_dir = PathBuf::from(&args[2]);
|
||||
let project_root = src_dir
|
||||
.canonicalize()?
|
||||
.parent()
|
||||
.ok_or("src_dir has no parent")?
|
||||
.to_path_buf();
|
||||
let generated_creatures = creatures::prepare(&project_root)?;
|
||||
|
||||
if let Some(generated) = generated_creatures.as_ref() {
|
||||
stage_loose_assets(&output_dir, generated)?;
|
||||
}
|
||||
|
||||
let data_dir = src_dir.join("Data");
|
||||
if !data_dir.exists() {
|
||||
@@ -97,12 +191,17 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
let sub_name = sub.file_name().to_string_lossy().to_string();
|
||||
let output_mpq = output_dir.join("Data").join(&name).join(&sub_name);
|
||||
build_mpq(&sub_path, &output_mpq)?;
|
||||
build_mpq(&sub_path, &output_mpq, None)?;
|
||||
}
|
||||
} else {
|
||||
// Regular patch dir, e.g. patch-4/ or patch-Z/
|
||||
let output_mpq = output_dir.join("Data").join(&name);
|
||||
build_mpq(&path, &output_mpq)?;
|
||||
let creature_files = if name.eq_ignore_ascii_case("patch-Z") {
|
||||
generated_creatures.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
build_mpq(&path, &output_mpq, creature_files)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/warcraftxl updated: 9d981125e1...ee9d8a92a3
Reference in New Issue
Block a user