Generate companion summon spells
This commit is contained in:
+21
-4
@@ -31,7 +31,11 @@
|
||||
2. генерирует `WXLFileData.csv` для WarcraftXL;
|
||||
3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`;
|
||||
4. сохраняет стабильные ID в `custom-creatures.lock.json`;
|
||||
5. кладёт серверные копии DBC и таблицу ID в `build/server-dbc`.
|
||||
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.
|
||||
|
||||
@@ -43,6 +47,8 @@
|
||||
{
|
||||
"folder": "assets/creatures/my_mount",
|
||||
"key": "my_mount",
|
||||
"name": "My Mount",
|
||||
"name_ru": "Мой транспорт",
|
||||
"archive": "Creature/MyMount",
|
||||
"model_file": "my_mount.m2",
|
||||
"template_display_id": 2404,
|
||||
@@ -59,6 +65,17 @@
|
||||
компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC;
|
||||
параметры объекта позволяют их переопределить.
|
||||
|
||||
Сгенерированные DBC сами по себе не создают серверное существо или заклинание. Сервер должен
|
||||
загрузить DBC из `build/server-dbc`, а полученный `displayInfoID` нужно назначить записи питомца,
|
||||
транспорта или соответствующему spell.
|
||||
## Установка на сервер
|
||||
|
||||
Для каждого элемента `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_template_model`, companion spell и строку skill line.
|
||||
Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует
|
||||
другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"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,
|
||||
"defaults": {
|
||||
"mount_template_display_id": 2404,
|
||||
"pet_template_display_id": 5448
|
||||
@@ -10,6 +13,8 @@
|
||||
"pets": [
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/Catslime",
|
||||
"name": "Cat Slime",
|
||||
"name_ru": "Котослизень",
|
||||
"display_scale": 1.0
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"models": {
|
||||
"catslime": {
|
||||
"model_data_id": 50000,
|
||||
"display_info_id": 50000
|
||||
"display_info_id": 50000,
|
||||
"creature_entry": 5000000,
|
||||
"summon_spell_id": 110000,
|
||||
"skill_line_ability_id": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// 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 uint32_t kSpellFields = 234;
|
||||
constexpr uint32_t kSpellRecordSize = kSpellFields * 4;
|
||||
constexpr uint32_t kSkillFields = 14;
|
||||
constexpr uint32_t kSkillRecordSize = kSkillFields * 4;
|
||||
|
||||
struct Override
|
||||
{
|
||||
uint32_t spellId = 0;
|
||||
uint32_t templateSpellId = 0;
|
||||
uint32_t creatureEntry = 0;
|
||||
uint32_t skillLineAbilityId = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
bool g_ready = false;
|
||||
std::vector<uint8_t> g_spellDbc;
|
||||
std::vector<uint8_t> g_skillDbc;
|
||||
|
||||
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::string_view fields[5];
|
||||
size_t fieldStart = 0;
|
||||
bool valid = true;
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
const size_t tab = line.find('\t', fieldStart);
|
||||
if (tab == std::string_view::npos) { valid = false; break; }
|
||||
fields[i] = line.substr(fieldStart, tab - fieldStart);
|
||||
fieldStart = tab + 1;
|
||||
}
|
||||
if (!valid) return false;
|
||||
fields[4] = Trim(line.substr(fieldStart));
|
||||
|
||||
Override item;
|
||||
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]);
|
||||
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);
|
||||
|
||||
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 = "Призывает или отпускает спутника: " + 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
const std::string root = wxl::host::ClientRoot();
|
||||
wxl::host::mpq::MpqStore store;
|
||||
std::vector<uint8_t> overrideBytes, spellBase, skillBase;
|
||||
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) ||
|
||||
!BuildSpellDbc(spellBase, overrides, g_spellDbc) ||
|
||||
!BuildSkillDbc(skillBase, overrides, g_skillDbc))
|
||||
{
|
||||
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)) return false;
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
if (!g_ready) return false;
|
||||
out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Registrar
|
||||
{
|
||||
Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); }
|
||||
};
|
||||
|
||||
Registrar g_registrar;
|
||||
}
|
||||
}
|
||||
+214
-23
@@ -17,6 +17,12 @@ pub struct Registry {
|
||||
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,
|
||||
pub defaults: Defaults,
|
||||
#[serde(default)]
|
||||
pub mounts: Vec<EntrySpec>,
|
||||
@@ -30,6 +36,15 @@ fn default_model_id() -> u32 {
|
||||
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
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Defaults {
|
||||
@@ -57,6 +72,8 @@ pub struct EntryOptions {
|
||||
pub collision_height: Option<f32>,
|
||||
pub mount_height: Option<f32>,
|
||||
pub flags: Option<u32>,
|
||||
pub name: Option<String>,
|
||||
pub name_ru: Option<String>,
|
||||
}
|
||||
|
||||
impl EntrySpec {
|
||||
@@ -78,6 +95,8 @@ impl EntrySpec {
|
||||
collision_height: options.collision_height,
|
||||
mount_height: options.mount_height,
|
||||
flags: options.flags,
|
||||
name: options.name.clone(),
|
||||
name_ru: options.name_ru.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -93,6 +112,12 @@ struct LockFile {
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -106,6 +131,9 @@ struct PreparedEntry {
|
||||
template_display_id: u32,
|
||||
ids: LockedIds,
|
||||
options: EntryOptions,
|
||||
summon: bool,
|
||||
name: String,
|
||||
name_ru: String,
|
||||
}
|
||||
|
||||
pub struct GeneratedCreatures {
|
||||
@@ -113,6 +141,7 @@ pub struct GeneratedCreatures {
|
||||
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)]
|
||||
@@ -318,36 +347,73 @@ fn collect_manifest_files(value: &Value, out: &mut Vec<(u32, String)>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_ids(lock: &mut LockFile, key: &str, model_start: u32, display_start: u32) -> LockedIds {
|
||||
if let Some(ids) = lock.models.get(key) {
|
||||
return ids.clone();
|
||||
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) -> 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 mut model = model_start;
|
||||
while used_models.contains(&model) {
|
||||
model += 1;
|
||||
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 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,
|
||||
});
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut display = display_start;
|
||||
while used_displays.contains(&display) {
|
||||
display += 1;
|
||||
}
|
||||
let ids = LockedIds {
|
||||
model_data_id: model,
|
||||
display_info_id: display,
|
||||
};
|
||||
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 prepare_entry(
|
||||
project_root: &Path,
|
||||
spec: &EntrySpec,
|
||||
template_default: u32,
|
||||
summon: bool,
|
||||
lock: &mut LockFile,
|
||||
registry: &Registry,
|
||||
) -> Result<PreparedEntry, Box<dyn Error>> {
|
||||
@@ -357,6 +423,10 @@ fn prepare_entry(
|
||||
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
|
||||
@@ -386,12 +456,7 @@ fn prepare_entry(
|
||||
file_data.push((file_data_id, format!("{archive}/{file}")));
|
||||
assets.push((source, format!("{archive}/{file}")));
|
||||
}
|
||||
let ids = allocate_ids(
|
||||
lock,
|
||||
&key,
|
||||
registry.model_data_id_start,
|
||||
registry.display_info_id_start,
|
||||
);
|
||||
let ids = allocate_ids(lock, &key, registry, summon);
|
||||
Ok(PreparedEntry {
|
||||
key,
|
||||
folder,
|
||||
@@ -402,6 +467,9 @@ fn prepare_entry(
|
||||
template_display_id: options.template_display_id.unwrap_or(template_default),
|
||||
ids,
|
||||
options,
|
||||
summon,
|
||||
name,
|
||||
name_ru,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -489,6 +557,67 @@ fn generate_dbc(
|
||||
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\
|
||||
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
|
||||
));
|
||||
|
||||
sql.push_str(&format!(
|
||||
"-- {}\n\
|
||||
INSERT INTO creature_template\n\
|
||||
(entry,name,minlevel,maxlevel,faction,speed_walk,speed_run,scale,unit_class,unit_flags,unit_flags2,type,MovementType,RegenHealth,VerifiedBuild)\n\
|
||||
VALUES ({creature},{name_en},1,1,188,1,1.14286,1,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_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
|
||||
));
|
||||
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,2686,{name_en},{name_ru},{description_en},{description_ru},133,1500,1,1,1,1)\n\
|
||||
ON DUPLICATE KEY UPDATE EffectMiscValue_1=VALUES(EffectMiscValue_1),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},0,0,0,0,1,0,0,0,0,0,0)\n\
|
||||
ON DUPLICATE KEY UPDATE SkillLine=VALUES(SkillLine),Spell=VALUES(Spell),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() {
|
||||
@@ -510,6 +639,7 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
project_root,
|
||||
spec,
|
||||
registry.defaults.mount_template_display_id,
|
||||
false,
|
||||
&mut lock,
|
||||
®istry,
|
||||
)?;
|
||||
@@ -523,6 +653,7 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
project_root,
|
||||
spec,
|
||||
registry.defaults.pet_template_display_id,
|
||||
true,
|
||||
&mut lock,
|
||||
®istry,
|
||||
)?;
|
||||
@@ -543,6 +674,9 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
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 assets = Vec::new();
|
||||
for entry in &entries {
|
||||
if !active_model_ids.insert(entry.ids.model_data_id) {
|
||||
@@ -559,6 +693,29 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
)
|
||||
.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",
|
||||
),
|
||||
] {
|
||||
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,
|
||||
@@ -585,6 +742,26 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
for (id, path) in all_ids {
|
||||
csv.push_str(&format!("{id},{}\n", path.replace('/', "\\")));
|
||||
}
|
||||
let mut spell_overrides =
|
||||
String::from("# spellId\ttemplateSpellId\tcreatureEntry\tskillLineAbilityId\tname\n");
|
||||
for entry in entries.iter().filter(|entry| entry.summon) {
|
||||
spell_overrides.push_str(&format!(
|
||||
"{}\t70613\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.name_ru
|
||||
));
|
||||
}
|
||||
|
||||
let base_dir = resolve_path(project_root, ®istry.base_dbc_dir);
|
||||
let (model_dbc, display_dbc) = generate_dbc(&base_dir, &entries)?;
|
||||
@@ -594,7 +771,13 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
fs::create_dir_all(&server_dir)?;
|
||||
fs::write(server_dir.join("CreatureModelData.dbc"), &model_dbc)?;
|
||||
fs::write(server_dir.join("CreatureDisplayInfo.dbc"), &display_dbc)?;
|
||||
let mut id_map = String::from("key,type,modelDataID,displayInfoID\n");
|
||||
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\n",
|
||||
);
|
||||
for (kind, specs) in [("mount", ®istry.mounts), ("pet", ®istry.pets)] {
|
||||
for spec in specs {
|
||||
let options = spec.options();
|
||||
@@ -602,8 +785,15 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
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
|
||||
"{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())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -614,5 +804,6 @@ pub fn prepare(project_root: &Path) -> Result<Option<GeneratedCreatures>, Box<dy
|
||||
file_data_csv: csv.into_bytes(),
|
||||
creature_model_data: model_dbc,
|
||||
creature_display_info: display_dbc,
|
||||
spell_overrides: spell_overrides.into_bytes(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ fn build_mpq(
|
||||
}
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user