Generate companion summon spells

This commit is contained in:
2026-07-23 01:14:05 +04:00
parent 87329f2633
commit 790506fc7a
6 changed files with 510 additions and 28 deletions
+21 -4
View File
@@ -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`.
+5
View File
@@ -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
}
]
+4 -1
View File
@@ -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
View File
@@ -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,
&registry,
)?;
@@ -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,
&registry,
)?;
@@ -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, &registry.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", &registry.mounts), ("pet", &registry.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(),
}))
}
+2
View File
@@ -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(),