From 42bc024bd3b41a13b8391d597b266699ee01d20b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:54:36 -0700 Subject: [PATCH] feat(pipeline): add WSET (Wowee Item Set / Tier Bonus) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 51st open format — replaces ItemSet.dbc + ItemSetSpell.dbc plus the AzerothCore-style item_set_spell SQL data. Closes the tier-bonus gap left by WIT (which describes individual items but not the set bonuses they grant when worn together). Each entry binds up to 8 piece item IDs to up to 4 bonus thresholds — at N pieces worn, the matching bonus spell activates as an aura. Standard 2/4/6/8-piece tier set pattern is the canonical case; 5-piece PvP sets with 2/4 bonuses are also supported. Cross-references with prior formats — itemIds[] point at WIT.itemId, bonusSpellIds[] point at WSPL.spellId, and requiredSkillId points at WSKL.skillId. requiredClassMask is a 32-bit field (uint32_t) so bit positions match WCHC's classId enum directly — Druid (bit 11 = 0x800) and Mage (bit 8 = 0x100) wouldn't fit in a uint8_t. CLI: --gen-itset (2 raid sets — Battlegear of Wrath + Stormrage Raiment, real WoW item/spell IDs), --gen-itset-tier (4 tier-1 progression sets covering plate / cloth / leather / holy plate), --gen-itset-pvp (3 PvP gladiator 5-piece sets with honor-rank skill thresholds), --info-wset, --validate-wset with --json variants. Validator catches id+name+pieceCount required, pieceCount/bonusCount within array bounds, piece- slot drift (0 IDs within count or non-0 IDs past count), bonus thresholds strictly ascending, no bonus threshold exceeding pieceCount (would never trigger), and spellId=0 in any populated bonus slot. Format graph: 50 → 51 binary formats. CLI flag count: 762 → 767. --- CMakeLists.txt | 3 + include/pipeline/wowee_item_sets.hpp | 112 ++++++++++ src/pipeline/wowee_item_sets.cpp | 275 +++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_item_sets_catalog.cpp | 293 +++++++++++++++++++++++++ tools/editor/cli_item_sets_catalog.hpp | 11 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 710 insertions(+) create mode 100644 include/pipeline/wowee_item_sets.hpp create mode 100644 src/pipeline/wowee_item_sets.cpp create mode 100644 tools/editor/cli_item_sets_catalog.cpp create mode 100644 tools/editor/cli_item_sets_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b839c6f..81b81cb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -639,6 +639,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_player_conditions.cpp src/pipeline/wowee_trade_skills.cpp src/pipeline/wowee_creature_equipment.cpp + src/pipeline/wowee_item_sets.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1429,6 +1430,7 @@ add_executable(wowee_editor tools/editor/cli_player_conditions_catalog.cpp tools/editor/cli_trade_skills_catalog.cpp tools/editor/cli_creature_equipment_catalog.cpp + tools/editor/cli_item_sets_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1546,6 +1548,7 @@ add_executable(wowee_editor src/pipeline/wowee_player_conditions.cpp src/pipeline/wowee_trade_skills.cpp src/pipeline/wowee_creature_equipment.cpp + src/pipeline/wowee_item_sets.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_item_sets.hpp b/include/pipeline/wowee_item_sets.hpp new file mode 100644 index 00000000..e34e164c --- /dev/null +++ b/include/pipeline/wowee_item_sets.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Item Set catalog (.wset) — novel replacement +// for Blizzard's ItemSet.dbc + ItemSetSpell.dbc plus the +// AzerothCore-style item_set_spell SQL data. New open +// format that closes the tier-bonus gap. +// +// Defines tiered item sets like "Battlegear of Wrath" or +// "Vengeful Gladiator's Plate" — N piece IDs (up to 8) plus +// M bonus thresholds (up to 4) where each threshold maps to +// a spell aura that activates when the player wears at least +// `threshold` pieces simultaneously. Standard 2-piece / +// 4-piece / 6-piece / 8-piece set-bonus pattern is the +// canonical case; partial bonuses are supported by leaving +// later thresholds at 0. +// +// Cross-references with previously-added formats: +// WSET.entry.itemIds[] → WIT.itemId +// WSET.entry.bonusSpellIds[] → WSPL.spellId +// WSET.entry.requiredSkillId → WSKL.skillId (optional) +// +// Binary layout (little-endian): +// magic[4] = "WSET" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// setId (uint32) +// nameLen + name +// descLen + description +// pieceCount (uint8) / bonusCount (uint8) / pad[2] +// requiredClassMask (uint32) +// requiredSkillId (uint16) / requiredSkillRank (uint16) +// itemIds[8] (uint32) — unused slots = 0 +// bonusThresholds[4] (uint8) / pad[4] +// bonusSpellIds[4] (uint32) +struct WoweeItemSet { + static constexpr size_t kMaxPieces = 8; + static constexpr size_t kMaxBonuses = 4; + + // Class-mask bits matching WCHC.classId (1=Warrior=bit 1, + // 2=Paladin, 3=Hunter, 4=Rogue, 5=Priest, 6=DK, 7=Shaman, + // 8=Mage, 9=Warlock, 11=Druid). 32-bit so the wider WotLK + // class set (Druid bit 11) fits — same convention as WGLY. + static constexpr uint32_t kClassNone = 0; + static constexpr uint32_t kClassWarrior = 1u << 1; + static constexpr uint32_t kClassPaladin = 1u << 2; + static constexpr uint32_t kClassHunter = 1u << 3; + static constexpr uint32_t kClassRogue = 1u << 4; + static constexpr uint32_t kClassPriest = 1u << 5; + static constexpr uint32_t kClassMage = 1u << 8; + static constexpr uint32_t kClassWarlock = 1u << 9; + static constexpr uint32_t kClassDruid = 1u << 11; + // Convenience composites. + static constexpr uint32_t kClassPlate = kClassWarrior | kClassPaladin; + static constexpr uint32_t kClassCloth = kClassMage | kClassWarlock | + kClassPriest; + + struct Entry { + uint32_t setId = 0; + std::string name; + std::string description; + uint8_t pieceCount = 0; // # of populated itemIds[] + uint8_t bonusCount = 0; // # of populated bonus pairs + uint32_t requiredClassMask = kClassNone; // wide mask + uint16_t requiredSkillId = 0; // WSKL cross-ref + uint16_t requiredSkillRank = 0; + uint32_t itemIds[kMaxPieces] = {0, 0, 0, 0, 0, 0, 0, 0}; + uint8_t bonusThresholds[kMaxBonuses] = {0, 0, 0, 0}; + uint32_t bonusSpellIds[kMaxBonuses] = {0, 0, 0, 0}; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t setId) const; +}; + +class WoweeItemSetLoader { +public: + static bool save(const WoweeItemSet& cat, + const std::string& basePath); + static WoweeItemSet load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-itset* variants. + // + // makeStarter — 2 raid sets (Battlegear of Wrath + // warrior tier-2; Stormrage Raiment + // druid tier-2) with 8-piece layouts. + // makeTier — 4 progression sets across class-role + // archetypes (warrior plate, mage cloth, + // rogue leather, paladin holy plate). + // makePvP — 3 PvP gladiator sets (Vindication, + // Doomcaller, Predatory) with 5-piece + // layouts and 2/4-piece set bonuses. + static WoweeItemSet makeStarter(const std::string& catalogName); + static WoweeItemSet makeTier(const std::string& catalogName); + static WoweeItemSet makePvP(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_item_sets.cpp b/src/pipeline/wowee_item_sets.cpp new file mode 100644 index 00000000..b62bcbcd --- /dev/null +++ b/src/pipeline/wowee_item_sets.cpp @@ -0,0 +1,275 @@ +#include "pipeline/wowee_item_sets.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'E', '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) != ".wset") { + base += ".wset"; + } + return base; +} + +} // namespace + +const WoweeItemSet::Entry* +WoweeItemSet::findById(uint32_t setId) const { + for (const auto& e : entries) if (e.setId == setId) return &e; + return nullptr; +} + +bool WoweeItemSetLoader::save(const WoweeItemSet& 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.setId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.pieceCount); + writePOD(os, e.bonusCount); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.requiredClassMask); + writePOD(os, e.requiredSkillId); + writePOD(os, e.requiredSkillRank); + for (size_t k = 0; k < WoweeItemSet::kMaxPieces; ++k) { + writePOD(os, e.itemIds[k]); + } + for (size_t k = 0; k < WoweeItemSet::kMaxBonuses; ++k) { + writePOD(os, e.bonusThresholds[k]); + } + for (size_t k = 0; k < WoweeItemSet::kMaxBonuses; ++k) { + writePOD(os, e.bonusSpellIds[k]); + } + } + return os.good(); +} + +WoweeItemSet WoweeItemSetLoader::load(const std::string& basePath) { + WoweeItemSet 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.setId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.pieceCount) || + !readPOD(is, e.bonusCount)) { + out.entries.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.requiredClassMask) || + !readPOD(is, e.requiredSkillId) || + !readPOD(is, e.requiredSkillRank)) { + out.entries.clear(); return out; + } + for (size_t k = 0; k < WoweeItemSet::kMaxPieces; ++k) { + if (!readPOD(is, e.itemIds[k])) { + out.entries.clear(); return out; + } + } + for (size_t k = 0; k < WoweeItemSet::kMaxBonuses; ++k) { + if (!readPOD(is, e.bonusThresholds[k])) { + out.entries.clear(); return out; + } + } + for (size_t k = 0; k < WoweeItemSet::kMaxBonuses; ++k) { + if (!readPOD(is, e.bonusSpellIds[k])) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeItemSetLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeItemSet WoweeItemSetLoader::makeStarter( + const std::string& catalogName) { + WoweeItemSet c; + c.name = catalogName; + { + // Battlegear of Wrath — Warrior tier-2 (8 pieces). + // Real WoW item / spell IDs from the canonical set. + WoweeItemSet::Entry e; + e.setId = 1; e.name = "Battlegear of Wrath"; + e.description = "Warrior tier-2 plate set from Blackwing Lair " + "and Molten Core."; + e.pieceCount = 8; + e.requiredClassMask = WoweeItemSet::kClassWarrior; + // 16959..16966 are the canonical 8 piece IDs. + e.itemIds[0] = 16959; e.itemIds[1] = 16960; + e.itemIds[2] = 16961; e.itemIds[3] = 16962; + e.itemIds[4] = 16963; e.itemIds[5] = 16964; + e.itemIds[6] = 16965; e.itemIds[7] = 16966; + // 3-piece, 5-piece, 8-piece set bonuses. + e.bonusCount = 3; + e.bonusThresholds[0] = 3; e.bonusSpellIds[0] = 23687; + e.bonusThresholds[1] = 5; e.bonusSpellIds[1] = 23689; + e.bonusThresholds[2] = 8; e.bonusSpellIds[2] = 23690; + c.entries.push_back(e); + } + { + // Stormrage Raiment — Druid tier-2 (8 pieces). + WoweeItemSet::Entry e; + e.setId = 2; e.name = "Stormrage Raiment"; + e.description = "Druid tier-2 leather set — Onyxia / BWL."; + e.pieceCount = 8; + e.requiredClassMask = WoweeItemSet::kClassDruid; + e.itemIds[0] = 16897; e.itemIds[1] = 16898; + e.itemIds[2] = 16899; e.itemIds[3] = 16900; + e.itemIds[4] = 16901; e.itemIds[5] = 16902; + e.itemIds[6] = 16903; e.itemIds[7] = 16904; + e.bonusCount = 3; + e.bonusThresholds[0] = 3; e.bonusSpellIds[0] = 23734; + e.bonusThresholds[1] = 5; e.bonusSpellIds[1] = 23737; + e.bonusThresholds[2] = 8; e.bonusSpellIds[2] = 23738; + c.entries.push_back(e); + } + return c; +} + +WoweeItemSet WoweeItemSetLoader::makeTier( + const std::string& catalogName) { + WoweeItemSet c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t classMask, + uint32_t baseItemId, uint32_t bonusSpell2, + uint32_t bonusSpell4, uint32_t bonusSpell6, + const char* desc) { + WoweeItemSet::Entry e; + e.setId = id; e.name = name; e.description = desc; + e.requiredClassMask = classMask; + e.pieceCount = 8; + for (size_t k = 0; k < 8; ++k) { + e.itemIds[k] = baseItemId + static_cast(k); + } + // Standard 2 / 4 / 6-piece progression. + e.bonusCount = 3; + e.bonusThresholds[0] = 2; e.bonusSpellIds[0] = bonusSpell2; + e.bonusThresholds[1] = 4; e.bonusSpellIds[1] = bonusSpell4; + e.bonusThresholds[2] = 6; e.bonusSpellIds[2] = bonusSpell6; + c.entries.push_back(e); + }; + add(100, "Tier1Warrior", WoweeItemSet::kClassWarrior, + 16451, 26460, 26461, 26462, + "Warrior tier-1 plate (Molten Core)."); + add(101, "Tier1Mage", WoweeItemSet::kClassMage, + 16811, 26467, 26468, 26469, + "Mage tier-1 cloth (Molten Core)."); + add(102, "Tier1Rogue", WoweeItemSet::kClassRogue, + 16723, 26482, 26483, 26484, + "Rogue tier-1 leather (Molten Core)."); + add(103, "Tier1Paladin", WoweeItemSet::kClassPaladin, + 16927, 26471, 26472, 26473, + "Paladin tier-1 holy plate (Molten Core)."); + return c; +} + +WoweeItemSet WoweeItemSetLoader::makePvP( + const std::string& catalogName) { + WoweeItemSet c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t classMask, + uint16_t skillId, uint16_t skillRank, + uint32_t baseItemId, uint32_t bonus2, + uint32_t bonus4, const char* desc) { + WoweeItemSet::Entry e; + e.setId = id; e.name = name; e.description = desc; + e.requiredClassMask = classMask; + e.requiredSkillId = skillId; + e.requiredSkillRank = skillRank; + // PvP sets typically have 5 pieces (head/shoulder/chest + // /legs/gloves) — leave slots 5-7 empty. + e.pieceCount = 5; + for (size_t k = 0; k < 5; ++k) { + e.itemIds[k] = baseItemId + static_cast(k); + } + // 2-piece + 4-piece bonuses. + e.bonusCount = 2; + e.bonusThresholds[0] = 2; e.bonusSpellIds[0] = bonus2; + e.bonusThresholds[1] = 4; e.bonusSpellIds[1] = bonus4; + c.entries.push_back(e); + }; + // skillId 162 = Unarmed; here we use a hypothetical + // PvP-ranking skill check (1850 honor / 5000 honor). + // requiredSkillRank values represent honor thresholds. + add(200, "GladiatorVindication", + WoweeItemSet::kClassPlate, 599, 1850, 41868, 35114, 35116, + "Gladiator's plate set — requires 1850 honor rank."); + add(201, "Doomcaller", + WoweeItemSet::kClassMage, 599, 1850, 41888, 35124, 35126, + "Mage doomcaller PvP set — requires 1850 honor rank."); + add(202, "Predatory", + WoweeItemSet::kClassRogue, 599, 1500, 41878, 35134, 35136, + "Rogue predatory PvP set — requires 1500 honor rank."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index fb104cba..931a6d14 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -154,6 +154,8 @@ const char* const kArgRequired[] = { "--gen-ceq", "--gen-ceq-bosses", "--gen-ceq-ranged", "--info-wceq", "--validate-wceq", "--export-wceq-json", "--import-wceq-json", + "--gen-itset", "--gen-itset-tier", "--gen-itset-pvp", + "--info-wset", "--validate-wset", "--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 09995376..07b5b4b1 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -83,6 +83,7 @@ #include "cli_player_conditions_catalog.hpp" #include "cli_trade_skills_catalog.hpp" #include "cli_creature_equipment_catalog.hpp" +#include "cli_item_sets_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -207,6 +208,7 @@ constexpr DispatchFn kDispatchTable[] = { handlePlayerConditionsCatalog, handleTradeSkillsCatalog, handleCreatureEquipmentCatalog, + handleItemSetsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index e091d6cc..d3d5eb53 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -53,6 +53,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','P','C','N'}, ".wpcn", "logic", "--info-wpcn", "Player condition catalog"}, {{'W','T','S','K'}, ".wtsk", "crafting", "--info-wtsk", "Trade skill recipe catalog"}, {{'W','C','E','Q'}, ".wceq", "creatures", "--info-wceq", "Creature equipment loadout catalog"}, + {{'W','S','E','T'}, ".wset", "items", "--info-wset", "Item set / tier-bonus 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 7c87f39d..21310426 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1441,6 +1441,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wceq to a human-editable JSON sidecar (defaults to .wceq.json)\n"); std::printf(" --import-wceq-json [out-base]\n"); std::printf(" Import a .wceq.json sidecar back into binary .wceq (slot fields default to canonical 16/17/18 if omitted)\n"); + std::printf(" --gen-itset [name]\n"); + std::printf(" Emit .wset starter: 2 raid sets (Battlegear of Wrath / Stormrage Raiment) — 8-piece tier-2 layouts\n"); + std::printf(" --gen-itset-tier [name]\n"); + std::printf(" Emit .wset 4 progression tier-1 sets (warrior plate / mage cloth / rogue leather / paladin holy plate) with 2/4/6 bonuses\n"); + std::printf(" --gen-itset-pvp [name]\n"); + std::printf(" Emit .wset 3 PvP gladiator sets (5-piece) with 2/4 bonuses and honor-rank skill thresholds\n"); + std::printf(" --info-wset [--json]\n"); + std::printf(" Print WSET entries (id / pieces / bonuses / classMask / skill+rank gates / first item ID / name)\n"); + std::printf(" --validate-wset [--json]\n"); + std::printf(" Static checks: id+name+pieceCount required, piece/bonus arrays match counts, monotonic bonus thresholds within pieceCount\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"); diff --git a/tools/editor/cli_item_sets_catalog.cpp b/tools/editor/cli_item_sets_catalog.cpp new file mode 100644 index 00000000..16fb96bc --- /dev/null +++ b/tools/editor/cli_item_sets_catalog.cpp @@ -0,0 +1,293 @@ +#include "cli_item_sets_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_item_sets.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWsetExt(std::string base) { + stripExt(base, ".wset"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeItemSet& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeItemSetLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wset\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeItemSet& c, + const std::string& base) { + std::printf("Wrote %s.wset\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" sets : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterItemSets"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsetExt(base); + auto c = wowee::pipeline::WoweeItemSetLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-itset")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenTier(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "TierItemSets"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsetExt(base); + auto c = wowee::pipeline::WoweeItemSetLoader::makeTier(name); + if (!saveOrError(c, base, "gen-itset-tier")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenPvP(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "PvPItemSets"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsetExt(base); + auto c = wowee::pipeline::WoweeItemSetLoader::makePvP(name); + if (!saveOrError(c, base, "gen-itset-pvp")) return 1; + printGenSummary(c, base); + return 0; +} + +void appendEntryJson(nlohmann::json& arr, + const wowee::pipeline::WoweeItemSet::Entry& e) { + nlohmann::json items = nlohmann::json::array(); + for (size_t k = 0; k < e.pieceCount && + k < wowee::pipeline::WoweeItemSet::kMaxPieces; ++k) { + items.push_back(e.itemIds[k]); + } + nlohmann::json bonuses = nlohmann::json::array(); + for (size_t k = 0; k < e.bonusCount && + k < wowee::pipeline::WoweeItemSet::kMaxBonuses; ++k) { + bonuses.push_back({ + {"threshold", e.bonusThresholds[k]}, + {"spellId", e.bonusSpellIds[k]}, + }); + } + arr.push_back({ + {"setId", e.setId}, + {"name", e.name}, + {"description", e.description}, + {"pieceCount", e.pieceCount}, + {"bonusCount", e.bonusCount}, + {"requiredClassMask", e.requiredClassMask}, + {"requiredSkillId", e.requiredSkillId}, + {"requiredSkillRank", e.requiredSkillRank}, + {"itemIds", items}, + {"bonuses", bonuses}, + }); +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWsetExt(base); + if (!wowee::pipeline::WoweeItemSetLoader::exists(base)) { + std::fprintf(stderr, "WSET not found: %s.wset\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemSetLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wset"] = base + ".wset"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) appendEntryJson(arr, e); + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSET: %s.wset\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" sets : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id pieces bonuses classMask skill rank first-itemId name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %3u %3u 0x%02x %5u %5u %12u %s\n", + e.setId, e.pieceCount, e.bonusCount, + e.requiredClassMask, e.requiredSkillId, + e.requiredSkillRank, e.itemIds[0], + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWsetExt(base); + if (!wowee::pipeline::WoweeItemSetLoader::exists(base)) { + std::fprintf(stderr, + "validate-wset: WSET not found: %s.wset\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemSetLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + 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.setId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.setId == 0) + errors.push_back(ctx + ": setId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.pieceCount == 0) + errors.push_back(ctx + + ": pieceCount=0 (set has no items)"); + if (e.pieceCount > wowee::pipeline::WoweeItemSet::kMaxPieces) { + errors.push_back(ctx + ": pieceCount " + + std::to_string(e.pieceCount) + " exceeds kMaxPieces (8)"); + } + if (e.bonusCount > wowee::pipeline::WoweeItemSet::kMaxBonuses) { + errors.push_back(ctx + ": bonusCount " + + std::to_string(e.bonusCount) + " exceeds kMaxBonuses (4)"); + } + // Verify the populated piece slots are non-zero and + // the unpopulated tail is zero — drift between + // pieceCount and the actual slot data confuses the + // runtime resolver. + for (size_t p = 0; p < wowee::pipeline::WoweeItemSet::kMaxPieces; + ++p) { + if (p < e.pieceCount) { + if (e.itemIds[p] == 0) { + errors.push_back(ctx + ": piece slot " + + std::to_string(p) + " is 0 but pieceCount=" + + std::to_string(e.pieceCount)); + } + } else { + if (e.itemIds[p] != 0) { + warnings.push_back(ctx + ": piece slot " + + std::to_string(p) + " has itemId=" + + std::to_string(e.itemIds[p]) + + " but is past pieceCount " + + std::to_string(e.pieceCount) + + " (silently ignored at runtime)"); + } + } + } + // Bonus thresholds must be ascending and within + // pieceCount — a bonus that requires more pieces + // than the set has can never trigger. + uint8_t prevThreshold = 0; + for (size_t b = 0; b < e.bonusCount; ++b) { + uint8_t t = e.bonusThresholds[b]; + uint32_t s = e.bonusSpellIds[b]; + if (t == 0) { + errors.push_back(ctx + ": bonus " + + std::to_string(b) + " has threshold=0"); + } + if (t > e.pieceCount) { + errors.push_back(ctx + ": bonus " + + std::to_string(b) + " threshold " + + std::to_string(t) + " > pieceCount " + + std::to_string(e.pieceCount) + + " (bonus can never trigger)"); + } + if (s == 0) { + errors.push_back(ctx + ": bonus " + + std::to_string(b) + + " threshold set but spellId=0 " + "(bonus has no aura)"); + } + if (b > 0 && t <= prevThreshold) { + errors.push_back(ctx + ": bonus " + + std::to_string(b) + " threshold " + + std::to_string(t) + + " not strictly greater than previous " + + std::to_string(prevThreshold)); + } + prevThreshold = t; + } + for (uint32_t prev : idsSeen) { + if (prev == e.setId) { + errors.push_back(ctx + ": duplicate setId"); + break; + } + } + idsSeen.push_back(e.setId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wset"] = base + ".wset"; + 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-wset: %s.wset\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu sets, all setIds unique, all bonus thresholds reachable\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 handleItemSetsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-itset") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-itset-tier") == 0 && i + 1 < argc) { + outRc = handleGenTier(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-itset-pvp") == 0 && i + 1 < argc) { + outRc = handleGenPvP(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wset") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wset") == 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_item_sets_catalog.hpp b/tools/editor/cli_item_sets_catalog.hpp new file mode 100644 index 00000000..5c42e785 --- /dev/null +++ b/tools/editor/cli_item_sets_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleItemSetsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 5688c7ec..f3f92b60 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -75,6 +75,7 @@ constexpr FormatRow kFormats[] = { {"WPCN", ".wpcn", "logic", "PlayerCondition.dbc + conditions", "Player condition (gates, AND/OR/NOT chains)"}, {"WTSK", ".wtsk", "crafting", "SkillLineAbility.dbc + recipes", "Trade skill recipes (per-profession crafts)"}, {"WCEQ", ".wceq", "creatures", "creature_equip_template", "Creature equipment loadout (visible weapons)"}, + {"WSET", ".wset", "items", "ItemSet.dbc + ItemSetSpell.dbc", "Item set + tier-bonus catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine