From b2b84139aa877460ff2ab069ef6bb4b3384a1f4d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:18:44 -0700 Subject: [PATCH] feat(pipeline): add WCRT (Wowee Creature Template) format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for the AzerothCore-style creature_template SQL table PLUS the Blizzard CreatureTemplate / CreatureFamily / CreatureType.dbc trio. The 14th open format added to the editor. This is the canonical metadata side of creatures shared across every spawn instance: HP, level range, faction, behavior flags, NPC role bits (vendor / trainer / quest-giver / innkeeper), base damage, equipped gear references. Cross-references with the previously-added formats: WSPN.entry.entryId -> WCRT.entry.creatureId WLOT.entry.creatureId -> WCRT.entry.creatureId WCRT.entry.equipped* -> WIT.entry.itemId The 4-format set (WIT + WLOT + WSPN + WCRT) now lets a content pack define a complete RPG zone's creature ecosystem: what creatures are, where they spawn, what they drop, and what gear they carry — entirely in open formats with no SQL dependencies. Format: • magic "WCRT", version 1, little-endian • per entry: creatureId / displayId / name / subname / minLevel..maxLevel / baseHealth + healthPerLevel / baseMana + manaPerLevel / factionId / npcFlags / typeId / familyId / damageMin..Max / attackSpeedMs / baseArmor / walkSpeed + runSpeed / gossipId / equippedMain + equippedOffhand + equippedRanged / aiFlags Enums: • TypeId: Beast / Dragon / Demon / Elemental / Giant / Undead / Humanoid / Critter / Mechanical • FamilyId: Wolf / Cat / Bear / Boar / Raptor / Hyena / Spider / Gorilla / Crab (for Beast types) • NpcFlags: Vendor / QuestGiver / Trainer / Banker / Innkeeper / FlightMaster / Auctioneer / Repair / Stable • Behavior: Passive / Aggressive / FleeLowHp / CallHelp / NoLeash API: WoweeCreatureLoader::save / load / exists / findById; presets makeStarter (1 innkeeper), makeBandit (creatureId=1000 matches WSPN/WLOT bandit references, equips WIT itemId=1001 sword), makeMerchants (creatureIds 4001/4002/4003 match WSPN village labels). CLI added (5 flags, 493 documented total): --gen-creatures / --gen-creatures-bandit / --gen-creatures-merchants --info-wcrt / --validate-wcrt Validator catches: creatureId=0, duplicates, level=0, minLevel>maxLevel, baseHealth=0, damageMin>damageMax, attackSpeed=0, non-positive walk/runSpeed, behavior flag contradictions (passive+aggressive), vendor with aggressive behavior (player can't trade). --- CMakeLists.txt | 3 + include/pipeline/wowee_creatures.hpp | 164 ++++++++++++++ src/pipeline/wowee_creatures.cpp | 272 +++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_creatures_catalog.cpp | 290 +++++++++++++++++++++++++ tools/editor/cli_creatures_catalog.hpp | 11 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 + 8 files changed, 754 insertions(+) create mode 100644 include/pipeline/wowee_creatures.hpp create mode 100644 src/pipeline/wowee_creatures.cpp create mode 100644 tools/editor/cli_creatures_catalog.cpp create mode 100644 tools/editor/cli_creatures_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c09bd51..4161a232 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -601,6 +601,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spawns.cpp src/pipeline/wowee_items.cpp src/pipeline/wowee_loot.cpp + src/pipeline/wowee_creatures.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1348,6 +1349,7 @@ add_executable(wowee_editor tools/editor/cli_spawns_catalog.cpp tools/editor/cli_items_catalog.cpp tools/editor/cli_loot_catalog.cpp + tools/editor/cli_creatures_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1427,6 +1429,7 @@ add_executable(wowee_editor src/pipeline/wowee_spawns.cpp src/pipeline/wowee_items.cpp src/pipeline/wowee_loot.cpp + src/pipeline/wowee_creatures.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_creatures.hpp b/include/pipeline/wowee_creatures.hpp new file mode 100644 index 00000000..21fd2ac5 --- /dev/null +++ b/include/pipeline/wowee_creatures.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Creature Template (.wcrt) — novel replacement +// for AzerothCore-style creature_template SQL tables PLUS +// the CreatureTemplate / CreatureFamily / CreatureType.dbc +// trio. The 14th open format added to the editor. +// +// This is the gameplay data side of creatures: HP, level +// range, faction, AI flags, NPC role bits (vendor / +// trainer / quest-giver / innkeeper), base damage, equipped +// gear references. The WSPN file says where creatures +// spawn; the WLOT file says what they drop on death; the +// WCRT file says what they ARE — the canonical metadata +// shared across every spawn instance. +// +// Cross-references: +// WSPN.entry.entryId → WCRT.entry.creatureId +// WLOT.entry.creatureId → WCRT.entry.creatureId +// WCRT.entry.equipped* → WIT.entry.itemId +// +// Binary layout (little-endian): +// magic[4] = "WCRT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// creatureId (uint32) +// displayId (uint32) +// nameLen + name +// subnameLen + subname +// minLevel (uint16) / maxLevel (uint16) +// baseHealth (uint32) -- at minLevel +// healthPerLevel (uint16) +// baseMana (uint32) +// manaPerLevel (uint16) +// factionId (uint32) +// npcFlags (uint32) +// typeId (uint8) / familyId (uint8) / pad[2] +// damageMin (uint32) / damageMax (uint32) / attackSpeedMs (uint32) +// baseArmor (uint32) +// walkSpeed (float) / runSpeed (float) +// gossipId (uint32) -- 0 if none +// equippedMain (uint32) -- WIT itemId, 0 if none +// equippedOffhand (uint32) +// equippedRanged (uint32) +// aiFlags (uint32) +struct WoweeCreature { + enum NpcFlags : uint32_t { + Vendor = 0x01, + QuestGiver = 0x02, + Trainer = 0x04, + Banker = 0x08, + Innkeeper = 0x10, + FlightMaster = 0x20, + Auctioneer = 0x40, + Repair = 0x80, + Stable = 0x100, + }; + + enum TypeId : uint8_t { + Beast = 1, + Dragon = 2, + Demon = 3, + Elemental = 4, + Giant = 5, + Undead = 6, + Humanoid = 7, + Critter = 8, + Mechanical = 9, + }; + + enum FamilyId : uint8_t { + FamNone = 0, + FamWolf = 1, + FamCat = 2, + FamBear = 3, + FamBoar = 4, + FamRaptor = 5, + FamHyena = 6, + FamSpider = 7, + FamGorilla = 8, + FamCrab = 9, + }; + + enum AiFlags : uint32_t { + AiPassive = 0x01, // never attacks unless attacked + AiAggressive = 0x02, // attacks players within aggro radius + AiFleeLowHp = 0x04, // runs at low health + AiCallHelp = 0x08, // calls allies into combat + AiNoLeash = 0x10, // does not return to spawn point + }; + + struct Entry { + uint32_t creatureId = 0; + uint32_t displayId = 0; + std::string name; + std::string subname; // e.g. "Innkeeper", "Stable Master" + uint16_t minLevel = 1; + uint16_t maxLevel = 1; + uint32_t baseHealth = 50; + uint16_t healthPerLevel = 10; + uint32_t baseMana = 0; + uint16_t manaPerLevel = 0; + uint32_t factionId = 35; // friendly default + uint32_t npcFlags = 0; + uint8_t typeId = Humanoid; + uint8_t familyId = FamNone; + uint32_t damageMin = 1; + uint32_t damageMax = 3; + uint32_t attackSpeedMs = 2000; + uint32_t baseArmor = 0; + float walkSpeed = 1.0f; + float runSpeed = 1.14f; // canonical WoW base + uint32_t gossipId = 0; + uint32_t equippedMain = 0; + uint32_t equippedOffhand = 0; + uint32_t equippedRanged = 0; + uint32_t aiFlags = AiPassive; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by creatureId — nullptr if not present. + const Entry* findById(uint32_t creatureId) const; + + static const char* typeName(uint8_t t); + static const char* familyName(uint8_t f); +}; + +class WoweeCreatureLoader { +public: + static bool save(const WoweeCreature& cat, + const std::string& basePath); + static WoweeCreature load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-creatures* variants. + // + // makeStarter — 1 friendly humanoid (innkeeper), level + // 30, vendor + innkeeper flags. + // makeBandit — 1 hostile humanoid (creatureId=1000, + // matches WSPN camp + WLOT bandit table). + // level 5..7, aggressive AI, equips a + // sword (WIT itemId=1001). + // makeMerchants — 3 NPCs covering the WSPN village + // creatures (innkeeper / smith / alchemist), + // creatureIds 4001/4002/4003. + static WoweeCreature makeStarter(const std::string& catalogName); + static WoweeCreature makeBandit(const std::string& catalogName); + static WoweeCreature makeMerchants(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_creatures.cpp b/src/pipeline/wowee_creatures.cpp new file mode 100644 index 00000000..88e6a962 --- /dev/null +++ b/src/pipeline/wowee_creatures.cpp @@ -0,0 +1,272 @@ +#include "pipeline/wowee_creatures.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'C', 'R', 'T'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wcrt") { + base += ".wcrt"; + } + return base; +} + +} // namespace + +const WoweeCreature::Entry* WoweeCreature::findById(uint32_t creatureId) const { + for (const auto& e : entries) { + if (e.creatureId == creatureId) return &e; + } + return nullptr; +} + +const char* WoweeCreature::typeName(uint8_t t) { + switch (t) { + case Beast: return "beast"; + case Dragon: return "dragon"; + case Demon: return "demon"; + case Elemental: return "elemental"; + case Giant: return "giant"; + case Undead: return "undead"; + case Humanoid: return "humanoid"; + case Critter: return "critter"; + case Mechanical: return "mechanical"; + default: return "unknown"; + } +} + +const char* WoweeCreature::familyName(uint8_t f) { + switch (f) { + case FamNone: return "-"; + case FamWolf: return "wolf"; + case FamCat: return "cat"; + case FamBear: return "bear"; + case FamBoar: return "boar"; + case FamRaptor: return "raptor"; + case FamHyena: return "hyena"; + case FamSpider: return "spider"; + case FamGorilla: return "gorilla"; + case FamCrab: return "crab"; + default: return "?"; + } +} + +bool WoweeCreatureLoader::save(const WoweeCreature& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.creatureId); + writePOD(os, e.displayId); + writeStr(os, e.name); + writeStr(os, e.subname); + writePOD(os, e.minLevel); + writePOD(os, e.maxLevel); + writePOD(os, e.baseHealth); + writePOD(os, e.healthPerLevel); + writePOD(os, e.baseMana); + writePOD(os, e.manaPerLevel); + writePOD(os, e.factionId); + writePOD(os, e.npcFlags); + writePOD(os, e.typeId); + writePOD(os, e.familyId); + uint8_t pad[2] = {0, 0}; + os.write(reinterpret_cast(pad), 2); + writePOD(os, e.damageMin); + writePOD(os, e.damageMax); + writePOD(os, e.attackSpeedMs); + writePOD(os, e.baseArmor); + writePOD(os, e.walkSpeed); + writePOD(os, e.runSpeed); + writePOD(os, e.gossipId); + writePOD(os, e.equippedMain); + writePOD(os, e.equippedOffhand); + writePOD(os, e.equippedRanged); + writePOD(os, e.aiFlags); + } + return os.good(); +} + +WoweeCreature WoweeCreatureLoader::load(const std::string& basePath) { + WoweeCreature out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.creatureId) || + !readPOD(is, e.displayId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.subname)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.minLevel) || + !readPOD(is, e.maxLevel) || + !readPOD(is, e.baseHealth) || + !readPOD(is, e.healthPerLevel) || + !readPOD(is, e.baseMana) || + !readPOD(is, e.manaPerLevel) || + !readPOD(is, e.factionId) || + !readPOD(is, e.npcFlags) || + !readPOD(is, e.typeId) || + !readPOD(is, e.familyId)) { + out.entries.clear(); return out; + } + uint8_t pad[2]; + is.read(reinterpret_cast(pad), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.damageMin) || + !readPOD(is, e.damageMax) || + !readPOD(is, e.attackSpeedMs) || + !readPOD(is, e.baseArmor) || + !readPOD(is, e.walkSpeed) || + !readPOD(is, e.runSpeed) || + !readPOD(is, e.gossipId) || + !readPOD(is, e.equippedMain) || + !readPOD(is, e.equippedOffhand) || + !readPOD(is, e.equippedRanged) || + !readPOD(is, e.aiFlags)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeCreatureLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeCreature WoweeCreatureLoader::makeStarter(const std::string& catalogName) { + WoweeCreature c; + c.name = catalogName; + { + WoweeCreature::Entry e; + e.creatureId = 4001; e.displayId = 50; + e.name = "Bartleby"; e.subname = "Innkeeper"; + e.minLevel = 30; e.maxLevel = 30; + e.baseHealth = 1500; e.healthPerLevel = 0; + e.factionId = 35; + e.npcFlags = WoweeCreature::Innkeeper | + WoweeCreature::Vendor | + WoweeCreature::Repair; + e.typeId = WoweeCreature::Humanoid; + e.aiFlags = WoweeCreature::AiPassive; + c.entries.push_back(e); + } + return c; +} + +WoweeCreature WoweeCreatureLoader::makeBandit(const std::string& catalogName) { + WoweeCreature c; + c.name = catalogName; + { + WoweeCreature::Entry e; + // creatureId = 1000 deliberately matches WSPN.makeCamp + // and WLOT.makeBandit so a content pack already wires + // together end-to-end. + e.creatureId = 1000; e.displayId = 1024; + e.name = "Defias Bandit"; e.subname = ""; + e.minLevel = 5; e.maxLevel = 7; + e.baseHealth = 80; e.healthPerLevel = 12; + e.factionId = 14; // hostile + e.typeId = WoweeCreature::Humanoid; + e.damageMin = 4; e.damageMax = 9; + e.attackSpeedMs = 1800; + e.baseArmor = 50; + e.runSpeed = 1.14f; + // Equipped weapon = WIT itemId 1001 (apprentice sword + // from makeWeapons preset). + e.equippedMain = 1001; + e.aiFlags = WoweeCreature::AiAggressive | + WoweeCreature::AiCallHelp | + WoweeCreature::AiFleeLowHp; + c.entries.push_back(e); + } + return c; +} + +WoweeCreature WoweeCreatureLoader::makeMerchants(const std::string& catalogName) { + WoweeCreature c; + c.name = catalogName; + auto add = [&](uint32_t id, uint32_t disp, const char* name, + const char* sub, uint32_t flags) { + WoweeCreature::Entry e; + e.creatureId = id; e.displayId = disp; + e.name = name; e.subname = sub; + e.minLevel = 30; e.maxLevel = 30; + e.baseHealth = 1500; e.factionId = 35; + e.npcFlags = flags; + e.typeId = WoweeCreature::Humanoid; + e.aiFlags = WoweeCreature::AiPassive; + c.entries.push_back(e); + }; + // creatureIds 4001/4002/4003 deliberately match the WSPN + // makeVillage labels (innkeeper / smith / alchemist). + add(4001, 50, "Mayle Crassell", "Innkeeper", + WoweeCreature::Innkeeper | WoweeCreature::Vendor | + WoweeCreature::Repair); + add(4002, 51, "Hank Steelarm", "Blacksmith", + WoweeCreature::Vendor | WoweeCreature::Repair | + WoweeCreature::Trainer); + add(4003, 52, "Sera Goldroot", "Alchemist", + WoweeCreature::Vendor | WoweeCreature::Trainer); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index f81fd947..c8bfcfcc 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -37,6 +37,8 @@ const char* const kArgRequired[] = { "--export-wit-json", "--import-wit-json", "--gen-loot", "--gen-loot-bandit", "--gen-loot-boss", "--info-wlot", "--validate-wlot", + "--gen-creatures", "--gen-creatures-bandit", "--gen-creatures-merchants", + "--info-wcrt", "--validate-wcrt", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_creatures_catalog.cpp b/tools/editor/cli_creatures_catalog.cpp new file mode 100644 index 00000000..7295757d --- /dev/null +++ b/tools/editor/cli_creatures_catalog.cpp @@ -0,0 +1,290 @@ +#include "cli_creatures_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_creatures.hpp" +#include + +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWcrtExt(std::string base) { + stripExt(base, ".wcrt"); + return base; +} + +void appendNpcFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeCreature::Vendor) s += "vendor "; + if (flags & wowee::pipeline::WoweeCreature::QuestGiver) s += "quest "; + if (flags & wowee::pipeline::WoweeCreature::Trainer) s += "trainer "; + if (flags & wowee::pipeline::WoweeCreature::Banker) s += "banker "; + if (flags & wowee::pipeline::WoweeCreature::Innkeeper) s += "innkeeper "; + if (flags & wowee::pipeline::WoweeCreature::FlightMaster) s += "flight "; + if (flags & wowee::pipeline::WoweeCreature::Auctioneer) s += "auction "; + if (flags & wowee::pipeline::WoweeCreature::Repair) s += "repair "; + if (flags & wowee::pipeline::WoweeCreature::Stable) s += "stable "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +void appendAiFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeCreature::AiPassive) s += "passive "; + if (flags & wowee::pipeline::WoweeCreature::AiAggressive) s += "aggressive "; + if (flags & wowee::pipeline::WoweeCreature::AiFleeLowHp) s += "flee "; + if (flags & wowee::pipeline::WoweeCreature::AiCallHelp) s += "call-help "; + if (flags & wowee::pipeline::WoweeCreature::AiNoLeash) s += "no-leash "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +bool saveOrError(const wowee::pipeline::WoweeCreature& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeCreatureLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wcrt\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeCreature& c, + const std::string& base) { + std::printf("Wrote %s.wcrt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterCreatures"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcrtExt(base); + auto c = wowee::pipeline::WoweeCreatureLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-creatures")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBandit(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BanditCreatures"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcrtExt(base); + auto c = wowee::pipeline::WoweeCreatureLoader::makeBandit(name); + if (!saveOrError(c, base, "gen-creatures-bandit")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMerchants(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "VillageMerchants"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWcrtExt(base); + auto c = wowee::pipeline::WoweeCreatureLoader::makeMerchants(name); + if (!saveOrError(c, base, "gen-creatures-merchants")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWcrtExt(base); + if (!wowee::pipeline::WoweeCreatureLoader::exists(base)) { + std::fprintf(stderr, "WCRT not found: %s.wcrt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCreatureLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wcrt"] = base + ".wcrt"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string ns, ais; + appendNpcFlagsStr(ns, e.npcFlags); + appendAiFlagsStr(ais, e.aiFlags); + arr.push_back({ + {"creatureId", e.creatureId}, + {"displayId", e.displayId}, + {"name", e.name}, + {"subname", e.subname}, + {"minLevel", e.minLevel}, + {"maxLevel", e.maxLevel}, + {"baseHealth", e.baseHealth}, + {"healthPerLevel", e.healthPerLevel}, + {"baseMana", e.baseMana}, + {"manaPerLevel", e.manaPerLevel}, + {"factionId", e.factionId}, + {"npcFlags", e.npcFlags}, + {"npcFlagsStr", ns}, + {"typeId", e.typeId}, + {"typeName", wowee::pipeline::WoweeCreature::typeName(e.typeId)}, + {"familyId", e.familyId}, + {"familyName", wowee::pipeline::WoweeCreature::familyName(e.familyId)}, + {"damageMin", e.damageMin}, + {"damageMax", e.damageMax}, + {"attackSpeedMs", e.attackSpeedMs}, + {"baseArmor", e.baseArmor}, + {"walkSpeed", e.walkSpeed}, + {"runSpeed", e.runSpeed}, + {"gossipId", e.gossipId}, + {"equippedMain", e.equippedMain}, + {"equippedOffhand", e.equippedOffhand}, + {"equippedRanged", e.equippedRanged}, + {"aiFlags", e.aiFlags}, + {"aiFlagsStr", ais}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCRT: %s.wcrt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" entries : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id level hp type faction npc-flags name\n"); + for (const auto& e : c.entries) { + std::string ns; + appendNpcFlagsStr(ns, e.npcFlags); + std::printf(" %4u %2u-%2u %5u %-9s %5u %-15s %s%s%s\n", + e.creatureId, e.minLevel, e.maxLevel, + e.baseHealth, + wowee::pipeline::WoweeCreature::typeName(e.typeId), + e.factionId, ns.c_str(), + e.name.c_str(), + e.subname.empty() ? "" : " <", + e.subname.empty() ? "" : (e.subname + ">").c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWcrtExt(base); + if (!wowee::pipeline::WoweeCreatureLoader::exists(base)) { + std::fprintf(stderr, + "validate-wcrt: WCRT not found: %s.wcrt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeCreatureLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + idsSeen.reserve(c.entries.size()); + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.creatureId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.creatureId == 0) { + errors.push_back(ctx + ": creatureId is 0"); + } + if (e.minLevel == 0) { + errors.push_back(ctx + ": minLevel is 0"); + } + if (e.minLevel > e.maxLevel) { + errors.push_back(ctx + ": minLevel > maxLevel"); + } + if (e.baseHealth == 0) { + errors.push_back(ctx + ": baseHealth is 0 (creature dies on spawn)"); + } + if (e.damageMin > e.damageMax) { + errors.push_back(ctx + ": damageMin > damageMax"); + } + if (e.attackSpeedMs == 0) { + errors.push_back(ctx + ": attackSpeedMs is 0 (would divide by zero)"); + } + if (e.runSpeed <= 0 || e.walkSpeed <= 0) { + errors.push_back(ctx + ": walk/runSpeed must be positive"); + } + // Conflicting AI flags: passive AND aggressive is incoherent. + if ((e.aiFlags & wowee::pipeline::WoweeCreature::AiPassive) && + (e.aiFlags & wowee::pipeline::WoweeCreature::AiAggressive)) { + warnings.push_back(ctx + ": both AiPassive and AiAggressive set"); + } + // Vendor + hostile is rare but possible (gnomish merchants + // surrounded by hostile NPCs); flag as warning to catch typos. + if ((e.npcFlags & wowee::pipeline::WoweeCreature::Vendor) && + (e.aiFlags & wowee::pipeline::WoweeCreature::AiAggressive)) { + warnings.push_back(ctx + + ": vendor with aggressive AI (player can't trade)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.creatureId) { + errors.push_back(ctx + ": duplicate creatureId"); + break; + } + } + idsSeen.push_back(e.creatureId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wcrt"] = base + ".wcrt"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wcrt: %s.wcrt\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu creatures, all creatureIds unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleCreaturesCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-creatures") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-creatures-bandit") == 0 && i + 1 < argc) { + outRc = handleGenBandit(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-creatures-merchants") == 0 && i + 1 < argc) { + outRc = handleGenMerchants(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wcrt") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wcrt") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_creatures_catalog.hpp b/tools/editor/cli_creatures_catalog.hpp new file mode 100644 index 00000000..31c3a086 --- /dev/null +++ b/tools/editor/cli_creatures_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleCreaturesCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 0a02e0cd..e1bb0571 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -41,6 +41,7 @@ #include "cli_spawns_catalog.hpp" #include "cli_items_catalog.hpp" #include "cli_loot_catalog.hpp" +#include "cli_creatures_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -123,6 +124,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpawnsCatalog, handleItemsCatalog, handleLootCatalog, + handleCreaturesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 18d710b6..7580ac26 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -891,6 +891,16 @@ void printUsage(const char* argv0) { std::printf(" Print WLOT loot tables (creatureId / dropCount / money range / per-drop chance + qty + flags)\n"); std::printf(" --validate-wlot [--json]\n"); std::printf(" Static checks: creatureId>0 + unique, chance in 0..100, minQty<=maxQty, money min<=max\n"); + std::printf(" --gen-creatures [name]\n"); + std::printf(" Emit .wcrt starter creature template: 1 friendly innkeeper (vendor + repair flags)\n"); + std::printf(" --gen-creatures-bandit [name]\n"); + std::printf(" Emit .wcrt bandit (creatureId=1000, matches WSPN camp + WLOT bandit table, equips WIT sword)\n"); + std::printf(" --gen-creatures-merchants [name]\n"); + std::printf(" Emit .wcrt 3-NPC village set (innkeeper / smith / alchemist, matches WSPN village creatureIds)\n"); + std::printf(" --info-wcrt [--json]\n"); + std::printf(" Print WCRT entries (id / level / hp / type / faction / npc-flags / name + subname)\n"); + std::printf(" --validate-wcrt [--json]\n"); + std::printf(" Static checks: creatureId>0+unique, level/hp>0, min<=max, attackSpeed>0, AI flag conflicts\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n");