diff --git a/CMakeLists.txt b/CMakeLists.txt index 613012da..8106acba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -714,6 +714,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_localization.cpp src/pipeline/wowee_global_channels.cpp src/pipeline/wowee_addon_manifest.cpp + src/pipeline/wowee_spell_pack.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1591,6 +1592,7 @@ add_executable(wowee_editor tools/editor/cli_localization_catalog.cpp tools/editor/cli_global_channels_catalog.cpp tools/editor/cli_addon_manifest_catalog.cpp + tools/editor/cli_spell_pack_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1787,6 +1789,7 @@ add_executable(wowee_editor src/pipeline/wowee_localization.cpp src/pipeline/wowee_global_channels.cpp src/pipeline/wowee_addon_manifest.cpp + src/pipeline/wowee_spell_pack.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_pack.hpp b/include/pipeline/wowee_spell_pack.hpp new file mode 100644 index 00000000..76ca4699 --- /dev/null +++ b/include/pipeline/wowee_spell_pack.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Pack catalog (.wspk) — novel +// replacement for the implicit per-class spellbook +// layout that vanilla WoW derived from +// SkillLineAbility.dbc + SpellTabIcon mappings + the +// hard-coded per-spec tab order baked into the client +// UI. Each WSPK entry binds one (classId, tabIndex) +// pair to an ordered list of spellIds shown in that +// spellbook tab. +// +// Cross-references with previously-added formats: +// WSPL: spellIds in the ordered list are looked up +// against WSPL spell catalog at runtime. +// WCDB: classId references the playable-class +// catalog (currently 1..11 in vanilla: +// Warrior=1 ... Druid=11). +// +// Binary layout (little-endian): +// magic[4] = "WSPK" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// packId (uint32) — surrogate primary key +// for cross-format +// --catalog-find lookups +// classId (uint8) — 1..11 vanilla class +// tabIndex (uint8) — 0=General/3 spec tabs +// iconIndex (uint8) — SpellIcon row id for +// the tab header glyph +// pad0 (uint8) +// tabNameLen + tabName — display label for the +// spellbook tab +// spellCount (uint32) +// spellIds (uint32 × count) — ordered display +// list (top-to- +// bottom in tab) +struct WoweeSpellPack { + struct Entry { + uint32_t packId = 0; + uint8_t classId = 0; + uint8_t tabIndex = 0; + uint8_t iconIndex = 0; + uint8_t pad0 = 0; + std::string tabName; + std::vector spellIds; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t packId) const; + const Entry* findByClassTab(uint8_t classId, + uint8_t tabIndex) const; + + // Returns all packs for a class (typically 4: General + // + 3 spec tabs). Used by the spellbook-screen UI to + // populate per-class tab order. + std::vector findByClass(uint8_t classId) const; +}; + +class WoweeSpellPackLoader { +public: + static bool save(const WoweeSpellPack& cat, + const std::string& basePath); + static WoweeSpellPack load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-spk* variants. + // + // makeWarriorPack — 4 tabs (General + 3 trees: + // Arms/Fury/Protection). Each + // tab seeded with canonical + // vanilla spellIds. + // makeMagePack — 4 tabs (General + Arcane/ + // Fire/Frost). Frost tab + // includes Frostbolt rank-1 + // spellId 116 — the canonical + // "every mage starts here" spell. + // makeRoguePack — 4 tabs (General + Assassin/ + // Combat/Subtlety). Combat tab + // seeded with poison-application + // and lethality picks. + static WoweeSpellPack makeWarriorPack(const std::string& catalogName); + static WoweeSpellPack makeMagePack(const std::string& catalogName); + static WoweeSpellPack makeRoguePack(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_pack.cpp b/src/pipeline/wowee_spell_pack.cpp new file mode 100644 index 00000000..c86da520 --- /dev/null +++ b/src/pipeline/wowee_spell_pack.cpp @@ -0,0 +1,290 @@ +#include "pipeline/wowee_spell_pack.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'P', 'K'}; +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; +} + +void writeU32Vec(std::ofstream& os, + const std::vector& v) { + uint32_t n = static_cast(v.size()); + writePOD(os, n); + if (n > 0) { + os.write(reinterpret_cast(v.data()), + static_cast(n * sizeof(uint32_t))); + } +} + +bool readU32Vec(std::ifstream& is, std::vector& v) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > 4096) return false; + v.resize(n); + if (n > 0) { + is.read(reinterpret_cast(v.data()), + static_cast(n * sizeof(uint32_t))); + if (is.gcount() != + static_cast(n * sizeof(uint32_t))) { + v.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wspk") { + base += ".wspk"; + } + return base; +} + +} // namespace + +const WoweeSpellPack::Entry* +WoweeSpellPack::findById(uint32_t packId) const { + for (const auto& e : entries) + if (e.packId == packId) return &e; + return nullptr; +} + +const WoweeSpellPack::Entry* +WoweeSpellPack::findByClassTab(uint8_t classId, + uint8_t tabIndex) const { + for (const auto& e : entries) + if (e.classId == classId && e.tabIndex == tabIndex) + return &e; + return nullptr; +} + +std::vector +WoweeSpellPack::findByClass(uint8_t classId) const { + std::vector out; + for (const auto& e : entries) + if (e.classId == classId) out.push_back(&e); + return out; +} + +bool WoweeSpellPackLoader::save(const WoweeSpellPack& 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.packId); + writePOD(os, e.classId); + writePOD(os, e.tabIndex); + writePOD(os, e.iconIndex); + writePOD(os, e.pad0); + writeStr(os, e.tabName); + writeU32Vec(os, e.spellIds); + } + return os.good(); +} + +WoweeSpellPack WoweeSpellPackLoader::load( + const std::string& basePath) { + WoweeSpellPack 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.packId) || + !readPOD(is, e.classId) || + !readPOD(is, e.tabIndex) || + !readPOD(is, e.iconIndex) || + !readPOD(is, e.pad0)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.tabName)) { + out.entries.clear(); return out; + } + if (!readU32Vec(is, e.spellIds)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellPackLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +namespace { + +// Helper to build one tab entry. classId follows +// vanilla DBC class IDs: Warrior=1, Mage=8, Rogue=4. +struct TabSpec { + uint32_t packId; + uint8_t classId; + uint8_t tabIndex; + uint8_t iconIndex; + const char* tabName; + std::vector spellIds; +}; + +WoweeSpellPack makeFromTabs(const std::string& catalogName, + std::vector tabs) { + using P = WoweeSpellPack; + WoweeSpellPack c; + c.name = catalogName; + for (auto& t : tabs) { + P::Entry e; + e.packId = t.packId; + e.classId = t.classId; + e.tabIndex = t.tabIndex; + e.iconIndex = t.iconIndex; + e.tabName = t.tabName; + e.spellIds = std::move(t.spellIds); + c.entries.push_back(std::move(e)); + } + return c; +} + +} // namespace + +WoweeSpellPack WoweeSpellPackLoader::makeWarriorPack( + const std::string& catalogName) { + // classId=1 (Warrior). Tab 0=General, 1=Arms, + // 2=Fury, 3=Protection. SpellIds are canonical + // vanilla low-rank picks: Charge=100, Heroic + // Strike=78, Mortal Strike=12294, Bloodthirst=23881, + // Shield Block=2565, etc. + return makeFromTabs(catalogName, { + {1001, 1, 0, 1, "General", + {78, // Heroic Strike rank 1 + 100, // Charge rank 1 + 6673, // Battle Shout rank 1 + 2457, // Battle Stance + }}, + {1002, 1, 1, 30, "Arms", + {12294, // Mortal Strike + 1680, // Whirlwind + 7384, // Overpower + }}, + {1003, 1, 2, 31, "Fury", + {23881, // Bloodthirst + 5308, // Execute + 1719, // Recklessness + }}, + {1004, 1, 3, 32, "Protection", + {2565, // Shield Block + 871, // Shield Wall + 355, // Taunt + }}, + }); +} + +WoweeSpellPack WoweeSpellPackLoader::makeMagePack( + const std::string& catalogName) { + // classId=8 (Mage). Frost tab includes Frostbolt + // rank 1 (spellId 116) — the canonical "every + // mage starts with this" spell. + return makeFromTabs(catalogName, { + {2001, 8, 0, 5, "General", + {133, // Fireball rank 1 + 168, // Frost Armor rank 1 + 1459, // Arcane Intellect rank 1 + }}, + {2002, 8, 1, 50, "Arcane", + {1449, // Arcane Explosion rank 1 + 5143, // Arcane Missiles rank 1 + 1953, // Blink + }}, + {2003, 8, 2, 51, "Fire", + {2120, // Flamestrike rank 1 + 11366, // Pyroblast rank 1 + 2948, // Scorch rank 1 + }}, + {2004, 8, 3, 52, "Frost", + {116, // Frostbolt rank 1 — every mage + // begins here + 122, // Frost Nova rank 1 + 10, // Blizzard rank 1 + }}, + }); +} + +WoweeSpellPack WoweeSpellPackLoader::makeRoguePack( + const std::string& catalogName) { + // classId=4 (Rogue). Combat tab seeded with + // poison-application + lethality picks. + return makeFromTabs(catalogName, { + {3001, 4, 0, 7, "General", + {1752, // Sinister Strike rank 1 + 1784, // Stealth rank 1 + 921, // Pickpocket + }}, + {3002, 4, 1, 70, "Assassination", + {703, // Garrote rank 1 + 8676, // Ambush rank 1 + 2098, // Eviscerate rank 1 + }}, + {3003, 4, 2, 71, "Combat", + {2983, // Sprint rank 1 + 1856, // Vanish rank 1 + 8647, // Expose Armor rank 1 + }}, + {3004, 4, 3, 72, "Subtlety", + {1857, // Vanish rank 2 + 5277, // Evasion + 14185, // Preparation + }}, + }); +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index e956a3e1..af7692fc 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -386,6 +386,8 @@ const char* const kArgRequired[] = { "--gen-mod", "--gen-mod-ui", "--gen-mod-util", "--info-wmod", "--validate-wmod", "--export-wmod-json", "--import-wmod-json", + "--gen-spk-warrior", "--gen-spk-mage", "--gen-spk-rogue", + "--info-wspk", "--validate-wspk", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 43d423c0..b1b92bf6 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -170,6 +170,7 @@ #include "cli_localization_catalog.hpp" #include "cli_global_channels_catalog.hpp" #include "cli_addon_manifest_catalog.hpp" +#include "cli_spell_pack_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -385,6 +386,7 @@ constexpr DispatchFn kDispatchTable[] = { handleLocalizationCatalog, handleGlobalChannelsCatalog, handleAddonManifestCatalog, + handleSpellPackCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 33687f36..146ff7ba 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -128,6 +128,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','L','A','N'}, ".wlan", "i18n", "--info-wlan", "Localization catalog"}, {{'W','G','C','H'}, ".wgch", "chat", "--info-wgch", "Global chat channel catalog"}, {{'W','M','O','D'}, ".wmod", "addons", "--info-wmod", "Addon manifest catalog"}, + {{'W','S','P','K'}, ".wspk", "spells", "--info-wspk", "Spell pack catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 25576a2d..f8e36848 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2517,6 +2517,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wmod to a human-editable JSON sidecar (defaults to .wmod.json; emits dependencies and optionalDependencies as JSON int arrays)\n"); std::printf(" --import-wmod-json [out-base]\n"); std::printf(" Import a .wmod.json sidecar back into binary .wmod (dependency arrays accept JSON int arrays — round-trips chained dep graphs byte-identical)\n"); + std::printf(" --gen-spk-warrior [name]\n"); + std::printf(" Emit .wspk Warrior class spellbook layout — 4 tabs (General + Arms/Fury/Protection) with canonical vanilla low-rank spellIds\n"); + std::printf(" --gen-spk-mage [name]\n"); + std::printf(" Emit .wspk Mage class spellbook layout — 4 tabs (General + Arcane/Fire/Frost) including Frostbolt rank 1 (spellId 116)\n"); + std::printf(" --gen-spk-rogue [name]\n"); + std::printf(" Emit .wspk Rogue class spellbook layout — 4 tabs (General + Assassination/Combat/Subtlety) with poison + lethality picks\n"); + std::printf(" --info-wspk [--json]\n"); + std::printf(" Print WSPK entries (packId / classId+name / tabIndex / iconIndex / spell count / tabName)\n"); + std::printf(" --validate-wspk [--json]\n"); + std::printf(" Static checks: packId+tabName required, classId in 1..11, tabIndex in 0..3, no duplicate packIds, no duplicate (classId,tabIndex) pairs (spellbook UI dispatch tie), no zero spellIds, no duplicate spellIds within any tab; warns on classId 6/10 (vanilla DBC gap) and on empty tabs (player would see blank spellbook)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 0893c477..8efa28e8 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -150,6 +150,7 @@ constexpr FormatRow kFormats[] = { {"WLAN", ".wlan", "i18n", "Locale_*.MPQ + DBC trailing strings","Localization catalog (per-language string overlay)"}, {"WGCH", ".wgch", "chat", "ChatChannels.dbc + zone-default joins","Global chat channel catalog (access policy + zone auto-join)"}, {"WMOD", ".wmod", "addons", "per-addon TOC text + load-order rules","Addon manifest catalog (deps + cycle detection)"}, + {"WSPK", ".wspk", "spells", "SkillLineAbility + per-spec tab order","Spell pack catalog (per-class spellbook tab layout)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_pack_catalog.cpp b/tools/editor/cli_spell_pack_catalog.cpp new file mode 100644 index 00000000..89833270 --- /dev/null +++ b/tools/editor/cli_spell_pack_catalog.cpp @@ -0,0 +1,291 @@ +#include "cli_spell_pack_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_pack.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWspkExt(std::string base) { + stripExt(base, ".wspk"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellPack& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellPackLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wspk\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellPack& c, + const std::string& base) { + std::printf("Wrote %s.wspk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" packs : %zu\n", c.entries.size()); +} + +int handleGenWarrior(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WarriorSpellPack"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspkExt(base); + auto c = wowee::pipeline::WoweeSpellPackLoader:: + makeWarriorPack(name); + if (!saveOrError(c, base, "gen-spk-warrior")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMage(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MageSpellPack"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspkExt(base); + auto c = wowee::pipeline::WoweeSpellPackLoader:: + makeMagePack(name); + if (!saveOrError(c, base, "gen-spk-mage")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRogue(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RogueSpellPack"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWspkExt(base); + auto c = wowee::pipeline::WoweeSpellPackLoader:: + makeRoguePack(name); + if (!saveOrError(c, base, "gen-spk-rogue")) return 1; + printGenSummary(c, base); + return 0; +} + +const char* classIdName(uint8_t c) { + // Vanilla 1.12 PlayerClass DBC ids — used for the + // info-table display only. + switch (c) { + case 1: return "Warrior"; + case 2: return "Paladin"; + case 3: return "Hunter"; + case 4: return "Rogue"; + case 5: return "Priest"; + case 7: return "Shaman"; + case 8: return "Mage"; + case 9: return "Warlock"; + case 11: return "Druid"; + default: return "?"; + } +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspkExt(base); + if (!wowee::pipeline::WoweeSpellPackLoader::exists(base)) { + std::fprintf(stderr, "WSPK not found: %s.wspk\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellPackLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wspk"] = base + ".wspk"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"packId", e.packId}, + {"classId", e.classId}, + {"className", classIdName(e.classId)}, + {"tabIndex", e.tabIndex}, + {"iconIndex", e.iconIndex}, + {"tabName", e.tabName}, + {"spellIds", e.spellIds}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSPK: %s.wspk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" packs : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id class tab icon spells tabName\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %2u %-9s %3u %4u %5zu %s\n", + e.packId, e.classId, + classIdName(e.classId), + e.tabIndex, e.iconIndex, + e.spellIds.size(), e.tabName.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWspkExt(base); + if (!wowee::pipeline::WoweeSpellPackLoader::exists(base)) { + std::fprintf(stderr, + "validate-wspk: WSPK not found: %s.wspk\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellPackLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set packIdsSeen; + std::set> classTabPairs; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (packId=" + std::to_string(e.packId); + if (!e.tabName.empty()) ctx += " " + e.tabName; + ctx += ")"; + if (e.packId == 0) + errors.push_back(ctx + ": packId is 0"); + if (e.tabName.empty()) + errors.push_back(ctx + ": tabName is empty"); + // Vanilla classes: 1..11 with id 6 + 10 unused. + if (e.classId == 0 || e.classId > 11) { + errors.push_back(ctx + ": classId " + + std::to_string(e.classId) + + " out of vanilla range (1..11)"); + } + if (e.classId == 6 || e.classId == 10) { + warnings.push_back(ctx + ": classId " + + std::to_string(e.classId) + + " is unused in vanilla (gap in PlayerClass DBC)"); + } + // Tab 0 = General; 1..3 = the three spec trees. + if (e.tabIndex > 3) { + errors.push_back(ctx + ": tabIndex " + + std::to_string(e.tabIndex) + + " out of range (0..3 — General + 3 specs)"); + } + // (classId, tabIndex) MUST be unique — the + // spellbook UI dispatches by this pair, two + // entries with the same pair would tie. + auto pair = std::make_pair(e.classId, e.tabIndex); + if (!classTabPairs.insert(pair).second) { + errors.push_back(ctx + + ": duplicate (classId=" + + std::to_string(e.classId) + + ", tabIndex=" + + std::to_string(e.tabIndex) + + ") — spellbook UI tab dispatch tie"); + } + if (!packIdsSeen.insert(e.packId).second) { + errors.push_back(ctx + ": duplicate packId"); + } + // Per-tab spell uniqueness — the same spellId + // appearing twice in one tab is a copy-paste bug + // (the UI would render it twice). + std::set spellsInTab; + for (uint32_t sid : e.spellIds) { + if (sid == 0) { + errors.push_back(ctx + + ": tab contains spellId 0 (placeholder " + "or copy-paste error)"); + } + if (sid != 0 && !spellsInTab.insert(sid).second) { + errors.push_back(ctx + + ": duplicate spellId " + + std::to_string(sid) + + " within tab — would render twice in " + "spellbook"); + } + } + // Empty tab: warn — General tab with zero spells + // means the player starts with no abilities at + // all on that tree. + if (e.spellIds.empty()) { + warnings.push_back(ctx + + ": tab has zero spells — player would see " + "an empty spellbook tab"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wspk"] = base + ".wspk"; + 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-wspk: %s.wspk\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu packs, all packIds unique, " + "(classId,tabIndex) unique, classId in " + "1..11, tabIndex in 0..3, no duplicate " + "spellIds within any tab\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 handleSpellPackCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-spk-warrior") == 0 && + i + 1 < argc) { + outRc = handleGenWarrior(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spk-mage") == 0 && + i + 1 < argc) { + outRc = handleGenMage(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-spk-rogue") == 0 && + i + 1 < argc) { + outRc = handleGenRogue(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wspk") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wspk") == 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_spell_pack_catalog.hpp b/tools/editor/cli_spell_pack_catalog.hpp new file mode 100644 index 00000000..d427f1c8 --- /dev/null +++ b/tools/editor/cli_spell_pack_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellPackCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee