diff --git a/CMakeLists.txt b/CMakeLists.txt index 9834279b..288e8c03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -622,6 +622,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_mounts.cpp src/pipeline/wowee_battlegrounds.cpp src/pipeline/wowee_mail.cpp + src/pipeline/wowee_gems.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1390,6 +1391,7 @@ add_executable(wowee_editor tools/editor/cli_mounts_catalog.cpp tools/editor/cli_battlegrounds_catalog.cpp tools/editor/cli_mail_catalog.cpp + tools/editor/cli_gems_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1490,6 +1492,7 @@ add_executable(wowee_editor src/pipeline/wowee_mounts.cpp src/pipeline/wowee_battlegrounds.cpp src/pipeline/wowee_mail.cpp + src/pipeline/wowee_gems.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_gems.hpp b/include/pipeline/wowee_gems.hpp new file mode 100644 index 00000000..17498d0b --- /dev/null +++ b/include/pipeline/wowee_gems.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Gem / Enchantment catalog (.wgem) — novel +// replacement for Blizzard's ItemEnchantment.dbc + +// GemProperties.dbc + SpellItemEnchantment.dbc. The 35th +// open format added to the editor. +// +// Defines two related kinds of item enhancement: +// • Gems — socketable jewelry pieces (red / blue / +// yellow / meta colors) that fit into +// gear sockets +// • Enchantments — persistent buffs applied to weapon / +// armor pieces, either by an enchanter +// spell or by an item proc +// +// Cross-references with previously-added formats: +// WGEM.gem.itemIdToInsert → WIT.entry.itemId +// WGEM.gem.spellId → WSPL.entry.spellId +// WGEM.enchantment.spellId → WSPL.entry.spellId +// +// Binary layout (little-endian): +// magic[4] = "WGEM" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// gemCount (uint32) +// gems (each): +// gemId (uint32) +// itemIdToInsert (uint32) +// nameLen + name +// color (uint8) / statType (uint8) / +// requiredItemQuality (uint8) / pad[1] +// statValue (int16) / pad[2] +// spellId (uint32) +// enchantCount (uint32) +// enchantments (each): +// enchantId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// enchantSlot (uint8) / statType (uint8) / pad[2] +// statValue (int16) / pad[2] +// spellId (uint32) +// durationSeconds (uint32) +// chargeCount (uint16) / pad[2] +struct WoweeGem { + enum Color : uint8_t { + Meta = 0, + Red = 1, + Yellow = 2, + Blue = 3, + Purple = 4, // red + blue + Green = 5, // blue + yellow + Orange = 6, // red + yellow + Prismatic = 7, // matches any color socket + }; + + // Stat types match WIT.StatType (Stamina / Strength / + // Intellect / Spirit / Agility / Defense / etc). + struct GemEntry { + uint32_t gemId = 0; + uint32_t itemIdToInsert = 0; + std::string name; + uint8_t color = Red; + uint8_t statType = 0; + uint8_t requiredItemQuality = 0; // 0 = any quality + int16_t statValue = 0; + uint32_t spellId = 0; // 0 = none (stat-only gem) + }; + + enum EnchantSlot : uint8_t { + Permanent = 0, // weapon enchant by enchanter + Temporary = 1, // poison / oil + SocketColor = 2, // socket recolor + Ring = 3, + Cloak = 4, + }; + + struct EnchantEntry { + uint32_t enchantId = 0; + std::string name; + std::string description; + std::string iconPath; + uint8_t enchantSlot = Permanent; + uint8_t statType = 0; + int16_t statValue = 0; + uint32_t spellId = 0; // 0 = stat-only + uint32_t durationSeconds = 0; // 0 = permanent + uint16_t chargeCount = 0; // 0 = unlimited + }; + + std::string name; + std::vector gems; + std::vector enchantments; + + bool isValid() const { return !gems.empty() || !enchantments.empty(); } + + const GemEntry* findGem(uint32_t gemId) const; + const EnchantEntry* findEnchant(uint32_t enchantId) const; + + static const char* colorName(uint8_t c); + static const char* enchantSlotName(uint8_t s); +}; + +class WoweeGemLoader { +public: + static bool save(const WoweeGem& cat, + const std::string& basePath); + static WoweeGem load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-gems* variants. + // + // makeStarter — 3 gems (one per primary color) + 2 + // enchantments (one weapon proc + one + // armor stat). + // makeGemSet — 6 gems covering all primary + secondary + // colors (red/yellow/blue/purple/green/ + // orange). + // makeEnchants — 5 enchantment variants spanning slots + // (permanent stat / temporary poison / + // ring / cloak / weapon proc). + static WoweeGem makeStarter(const std::string& catalogName); + static WoweeGem makeGemSet(const std::string& catalogName); + static WoweeGem makeEnchants(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_gems.cpp b/src/pipeline/wowee_gems.cpp new file mode 100644 index 00000000..58ede492 --- /dev/null +++ b/src/pipeline/wowee_gems.cpp @@ -0,0 +1,330 @@ +#include "pipeline/wowee_gems.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'E', 'M'}; +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) != ".wgem") { + base += ".wgem"; + } + return base; +} + +} // namespace + +const WoweeGem::GemEntry* WoweeGem::findGem(uint32_t gemId) const { + for (const auto& g : gems) if (g.gemId == gemId) return &g; + return nullptr; +} + +const WoweeGem::EnchantEntry* WoweeGem::findEnchant(uint32_t enchantId) const { + for (const auto& e : enchantments) if (e.enchantId == enchantId) return &e; + return nullptr; +} + +const char* WoweeGem::colorName(uint8_t c) { + switch (c) { + case Meta: return "meta"; + case Red: return "red"; + case Yellow: return "yellow"; + case Blue: return "blue"; + case Purple: return "purple"; + case Green: return "green"; + case Orange: return "orange"; + case Prismatic: return "prismatic"; + default: return "unknown"; + } +} + +const char* WoweeGem::enchantSlotName(uint8_t s) { + switch (s) { + case Permanent: return "permanent"; + case Temporary: return "temporary"; + case SocketColor: return "socket"; + case Ring: return "ring"; + case Cloak: return "cloak"; + default: return "unknown"; + } +} + +bool WoweeGemLoader::save(const WoweeGem& 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 gemCount = static_cast(cat.gems.size()); + writePOD(os, gemCount); + for (const auto& g : cat.gems) { + writePOD(os, g.gemId); + writePOD(os, g.itemIdToInsert); + writeStr(os, g.name); + writePOD(os, g.color); + writePOD(os, g.statType); + writePOD(os, g.requiredItemQuality); + uint8_t pad = 0; + writePOD(os, pad); + writePOD(os, g.statValue); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, g.spellId); + } + uint32_t enchCount = static_cast(cat.enchantments.size()); + writePOD(os, enchCount); + for (const auto& e : cat.enchantments) { + writePOD(os, e.enchantId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writePOD(os, e.enchantSlot); + writePOD(os, e.statType); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.statValue); + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.spellId); + writePOD(os, e.durationSeconds); + writePOD(os, e.chargeCount); + os.write(reinterpret_cast(pad2), 2); + } + return os.good(); +} + +WoweeGem WoweeGemLoader::load(const std::string& basePath) { + WoweeGem 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 gemCount = 0; + if (!readPOD(is, gemCount)) return out; + if (gemCount > (1u << 20)) return out; + out.gems.resize(gemCount); + for (auto& g : out.gems) { + if (!readPOD(is, g.gemId) || + !readPOD(is, g.itemIdToInsert)) { + out.gems.clear(); return out; + } + if (!readStr(is, g.name)) { + out.gems.clear(); return out; + } + if (!readPOD(is, g.color) || + !readPOD(is, g.statType) || + !readPOD(is, g.requiredItemQuality)) { + out.gems.clear(); return out; + } + uint8_t pad = 0; + if (!readPOD(is, pad)) { + out.gems.clear(); return out; + } + if (!readPOD(is, g.statValue)) { + out.gems.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.gems.clear(); return out; } + if (!readPOD(is, g.spellId)) { + out.gems.clear(); return out; + } + } + uint32_t enchCount = 0; + if (!readPOD(is, enchCount)) { + out.gems.clear(); return out; + } + if (enchCount > (1u << 20)) { + out.gems.clear(); return out; + } + out.enchantments.resize(enchCount); + for (auto& e : out.enchantments) { + if (!readPOD(is, e.enchantId)) { + out.gems.clear(); out.enchantments.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath)) { + out.gems.clear(); out.enchantments.clear(); return out; + } + if (!readPOD(is, e.enchantSlot) || + !readPOD(is, e.statType)) { + out.gems.clear(); out.enchantments.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { + out.gems.clear(); out.enchantments.clear(); return out; + } + if (!readPOD(is, e.statValue)) { + out.gems.clear(); out.enchantments.clear(); return out; + } + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { + out.gems.clear(); out.enchantments.clear(); return out; + } + if (!readPOD(is, e.spellId) || + !readPOD(is, e.durationSeconds) || + !readPOD(is, e.chargeCount)) { + out.gems.clear(); out.enchantments.clear(); return out; + } + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { + out.gems.clear(); out.enchantments.clear(); return out; + } + } + return out; +} + +bool WoweeGemLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeGem WoweeGemLoader::makeStarter(const std::string& catalogName) { + WoweeGem c; + c.name = catalogName; + { + WoweeGem::GemEntry g; + g.gemId = 1; g.itemIdToInsert = 23436; + g.name = "Bold Living Ruby"; + g.color = WoweeGem::Red; + // statType=4 matches WIT::StatStrength. + g.statType = 4; g.statValue = 8; + c.gems.push_back(g); + } + { + WoweeGem::GemEntry g; + g.gemId = 2; g.itemIdToInsert = 23437; + g.name = "Brilliant Dawnstone"; + g.color = WoweeGem::Yellow; + // statType=5 matches WIT::StatIntellect. + g.statType = 5; g.statValue = 8; + c.gems.push_back(g); + } + { + WoweeGem::GemEntry g; + g.gemId = 3; g.itemIdToInsert = 23438; + g.name = "Solid Star of Elune"; + g.color = WoweeGem::Blue; + // statType=7 matches WIT::StatStamina. + g.statType = 7; g.statValue = 12; + c.gems.push_back(g); + } + { + WoweeGem::EnchantEntry e; + e.enchantId = 1; e.name = "Crusader"; + e.description = "Chance on hit to heal you and increase strength."; + e.enchantSlot = WoweeGem::Permanent; + e.spellId = 20007; // canonical enchant proc + c.enchantments.push_back(e); + } + { + WoweeGem::EnchantEntry e; + e.enchantId = 2; e.name = "Greater Stats"; + e.description = "+8 to all stats."; + e.enchantSlot = WoweeGem::Permanent; + e.statType = 7; e.statValue = 8; // stamina + c.enchantments.push_back(e); + } + return c; +} + +WoweeGem WoweeGemLoader::makeGemSet(const std::string& catalogName) { + WoweeGem c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t color, + uint8_t stat, int16_t value) { + WoweeGem::GemEntry g; + g.gemId = id; g.name = name; + g.color = color; g.statType = stat; g.statValue = value; + c.gems.push_back(g); + }; + // Primary colors. + add(101, "Bold Crimson Spinel", WoweeGem::Red, 4, 14); // STR + add(102, "Brilliant Lionseye", WoweeGem::Yellow, 5, 14); // INT + add(103, "Solid Empyrean Sapphire", WoweeGem::Blue, 7, 21); // STA + // Secondary colors (combinations). + add(201, "Sovereign Shadowsong Amethyst", WoweeGem::Purple, 4, 7); + add(202, "Forceful Earthsiege Diamond", WoweeGem::Green, 5, 7); + add(203, "Inscribed Pyrestone", WoweeGem::Orange, 4, 7); + return c; +} + +WoweeGem WoweeGemLoader::makeEnchants(const std::string& catalogName) { + WoweeGem c; + c.name = catalogName; + // Lambda parameters in order: id, name, slot, stat type, + // stat value, durSec, charges, spellId, description. + // Proc-only enchants pass spellId = a placeholder in the + // 28000-29000 range (outside WSPL preset spellIds; the + // runtime resolves these to real proc spells when WSPL is + // extended with enchant procs). + auto add = [&](uint32_t id, const char* name, uint8_t slot, + uint8_t stat, int16_t value, uint32_t durSec, + uint16_t charges, uint32_t spellId, + const char* desc) { + WoweeGem::EnchantEntry e; + e.enchantId = id; e.name = name; e.description = desc; + e.enchantSlot = slot; e.statType = stat; e.statValue = value; + e.durationSeconds = durSec; e.chargeCount = charges; + e.spellId = spellId; + c.enchantments.push_back(e); + }; + add(300, "Mongoose", WoweeGem::Permanent, 3, 0, 0, 0, 28100, + "Chance on hit: +120 agility for 15 seconds."); + add(301, "Deadly Poison", WoweeGem::Temporary, 0, 0, 3600, 60, 28200, + "Each hit applies a poison stack."); + add(302, "Greater Stats Ring", WoweeGem::Ring, 7, 4, 0, 0, 0, + "+4 to all stats."); + add(303, "Major Agility Cloak", WoweeGem::Cloak, 3, 16, 0, 0, 0, + "+16 agility."); + add(304, "Berserking", WoweeGem::Permanent, 0, 0, 0, 0, 28300, + "Chance on hit: temporarily increase haste."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8d67556a..2762b09d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -100,6 +100,8 @@ const char* const kArgRequired[] = { "--export-wbgd-json", "--import-wbgd-json", "--gen-mail", "--gen-mail-holiday", "--gen-mail-auction", "--info-wmal", "--validate-wmal", + "--gen-gems", "--gen-gems-set", "--gen-gems-enchants", + "--info-wgem", "--validate-wgem", "--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 6b6b9310..1a60b03a 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -62,6 +62,7 @@ #include "cli_mounts_catalog.hpp" #include "cli_battlegrounds_catalog.hpp" #include "cli_mail_catalog.hpp" +#include "cli_gems_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -165,6 +166,7 @@ constexpr DispatchFn kDispatchTable[] = { handleMountsCatalog, handleBattlegroundsCatalog, handleMailCatalog, + handleGemsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_gems_catalog.cpp b/tools/editor/cli_gems_catalog.cpp new file mode 100644 index 00000000..fee3dfd8 --- /dev/null +++ b/tools/editor/cli_gems_catalog.cpp @@ -0,0 +1,281 @@ +#include "cli_gems_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_gems.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgemExt(std::string base) { + stripExt(base, ".wgem"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeGem& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGemLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgem\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGem& c, + const std::string& base) { + std::printf("Wrote %s.wgem\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" gems : %zu\n", c.gems.size()); + std::printf(" enchantments : %zu\n", c.enchantments.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterGems"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgemExt(base); + auto c = wowee::pipeline::WoweeGemLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-gems")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenGemSet(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FullGemSet"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgemExt(base); + auto c = wowee::pipeline::WoweeGemLoader::makeGemSet(name); + if (!saveOrError(c, base, "gen-gems-set")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenEnchants(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "EnchantSet"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgemExt(base); + auto c = wowee::pipeline::WoweeGemLoader::makeEnchants(name); + if (!saveOrError(c, base, "gen-gems-enchants")) 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 = stripWgemExt(base); + if (!wowee::pipeline::WoweeGemLoader::exists(base)) { + std::fprintf(stderr, "WGEM not found: %s.wgem\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGemLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgem"] = base + ".wgem"; + j["name"] = c.name; + j["gemCount"] = c.gems.size(); + j["enchantCount"] = c.enchantments.size(); + nlohmann::json ga = nlohmann::json::array(); + for (const auto& g : c.gems) { + ga.push_back({ + {"gemId", g.gemId}, + {"itemIdToInsert", g.itemIdToInsert}, + {"name", g.name}, + {"color", g.color}, + {"colorName", wowee::pipeline::WoweeGem::colorName(g.color)}, + {"statType", g.statType}, + {"statValue", g.statValue}, + {"requiredItemQuality", g.requiredItemQuality}, + {"spellId", g.spellId}, + }); + } + j["gems"] = ga; + nlohmann::json ea = nlohmann::json::array(); + for (const auto& e : c.enchantments) { + ea.push_back({ + {"enchantId", e.enchantId}, + {"name", e.name}, + {"description", e.description}, + {"iconPath", e.iconPath}, + {"enchantSlot", e.enchantSlot}, + {"enchantSlotName", wowee::pipeline::WoweeGem::enchantSlotName(e.enchantSlot)}, + {"statType", e.statType}, + {"statValue", e.statValue}, + {"spellId", e.spellId}, + {"durationSeconds", e.durationSeconds}, + {"chargeCount", e.chargeCount}, + }); + } + j["enchantments"] = ea; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGEM: %s.wgem\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" gems : %zu\n", c.gems.size()); + std::printf(" enchantments : %zu\n", c.enchantments.size()); + if (!c.gems.empty()) { + std::printf("\n Gems:\n"); + std::printf(" id color stat/value itemId name\n"); + for (const auto& g : c.gems) { + std::printf(" %4u %-10s %3u/%-5d %5u %s\n", + g.gemId, + wowee::pipeline::WoweeGem::colorName(g.color), + g.statType, g.statValue, + g.itemIdToInsert, g.name.c_str()); + } + } + if (!c.enchantments.empty()) { + std::printf("\n Enchantments:\n"); + std::printf(" id slot stat/value dur(s) chg name\n"); + for (const auto& e : c.enchantments) { + std::printf(" %4u %-10s %3u/%-5d %5u %3u %s\n", + e.enchantId, + wowee::pipeline::WoweeGem::enchantSlotName(e.enchantSlot), + e.statType, e.statValue, + e.durationSeconds, e.chargeCount, + 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 = stripWgemExt(base); + if (!wowee::pipeline::WoweeGemLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgem: WGEM not found: %s.wgem\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGemLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.gems.empty() && c.enchantments.empty()) { + warnings.push_back("catalog has zero gems and zero enchantments"); + } + std::vector gemIdsSeen; + for (size_t k = 0; k < c.gems.size(); ++k) { + const auto& g = c.gems[k]; + std::string ctx = "gem " + std::to_string(k) + + " (id=" + std::to_string(g.gemId); + if (!g.name.empty()) ctx += " " + g.name; + ctx += ")"; + if (g.gemId == 0) errors.push_back(ctx + ": gemId is 0"); + if (g.name.empty()) errors.push_back(ctx + ": name is empty"); + if (g.color > wowee::pipeline::WoweeGem::Prismatic) { + errors.push_back(ctx + ": color " + + std::to_string(g.color) + " not in 0..7"); + } + // Stat-only gems (spellId=0) need statValue != 0. + if (g.spellId == 0 && g.statValue == 0) { + warnings.push_back(ctx + + ": no spell + statValue=0 (gem provides nothing)"); + } + for (uint32_t prev : gemIdsSeen) { + if (prev == g.gemId) { + errors.push_back(ctx + ": duplicate gemId"); + break; + } + } + gemIdsSeen.push_back(g.gemId); + } + std::vector enchIdsSeen; + for (size_t k = 0; k < c.enchantments.size(); ++k) { + const auto& e = c.enchantments[k]; + std::string ctx = "enchant " + std::to_string(k) + + " (id=" + std::to_string(e.enchantId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.enchantId == 0) errors.push_back(ctx + ": enchantId is 0"); + if (e.name.empty()) errors.push_back(ctx + ": name is empty"); + if (e.enchantSlot > wowee::pipeline::WoweeGem::Cloak) { + errors.push_back(ctx + ": enchantSlot " + + std::to_string(e.enchantSlot) + " not in 0..4"); + } + if (e.spellId == 0 && e.statValue == 0) { + warnings.push_back(ctx + + ": no spell + statValue=0 (enchant provides nothing)"); + } + // Charges only meaningful for Temporary enchants. + if (e.chargeCount > 0 && + e.enchantSlot != wowee::pipeline::WoweeGem::Temporary) { + warnings.push_back(ctx + + ": chargeCount > 0 on non-Temporary slot (charges ignored)"); + } + for (uint32_t prev : enchIdsSeen) { + if (prev == e.enchantId) { + errors.push_back(ctx + ": duplicate enchantId"); + break; + } + } + enchIdsSeen.push_back(e.enchantId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgem"] = base + ".wgem"; + 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-wgem: %s.wgem\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu gems, %zu enchantments, all IDs unique\n", + c.gems.size(), c.enchantments.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 handleGemsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-gems") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gems-set") == 0 && i + 1 < argc) { + outRc = handleGenGemSet(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gems-enchants") == 0 && i + 1 < argc) { + outRc = handleGenEnchants(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgem") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgem") == 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_gems_catalog.hpp b/tools/editor/cli_gems_catalog.hpp new file mode 100644 index 00000000..6dc1381d --- /dev/null +++ b/tools/editor/cli_gems_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleGemsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 94aa333c..6ae8d894 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1189,6 +1189,16 @@ void printUsage(const char* argv0) { std::printf(" Print WMAL templates (id / category / sender / subject + body / money + items / cod / expiry)\n"); std::printf(" --validate-wmal [--json]\n"); std::printf(" Static checks: id>0+unique, subject not empty, sender set, attachments valid, no money+no items info-only\n"); + std::printf(" --gen-gems [name]\n"); + std::printf(" Emit .wgem starter: 3 gems (red/yellow/blue) + 2 enchantments (Crusader proc + Greater Stats)\n"); + std::printf(" --gen-gems-set [name]\n"); + std::printf(" Emit .wgem 6-gem full color set (3 primary + 3 secondary purple/green/orange)\n"); + std::printf(" --gen-gems-enchants [name]\n"); + std::printf(" Emit .wgem 5 enchant variants (Mongoose / Deadly Poison / stats ring / cloak / Berserking proc)\n"); + std::printf(" --info-wgem [--json]\n"); + std::printf(" Print WGEM gems (id / color / stat / item) + enchantments (id / slot / stat / duration / charges)\n"); + std::printf(" --validate-wgem [--json]\n"); + std::printf(" Static checks: ids>0+unique, name not empty, color/slot in range, stat-only entries need non-zero value\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");