diff --git a/CMakeLists.txt b/CMakeLists.txt index aa720b1c..17c22a0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -672,6 +672,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_achievement_criteria.cpp src/pipeline/wowee_spell_effect_types.cpp src/pipeline/wowee_spell_aura_types.cpp + src/pipeline/wowee_item_qualities.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1504,6 +1505,7 @@ add_executable(wowee_editor tools/editor/cli_achievement_criteria_catalog.cpp tools/editor/cli_spell_effect_types_catalog.cpp tools/editor/cli_spell_aura_types_catalog.cpp + tools/editor/cli_item_qualities_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1654,6 +1656,7 @@ add_executable(wowee_editor src/pipeline/wowee_achievement_criteria.cpp src/pipeline/wowee_spell_effect_types.cpp src/pipeline/wowee_spell_aura_types.cpp + src/pipeline/wowee_item_qualities.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_item_qualities.hpp b/include/pipeline/wowee_item_qualities.hpp new file mode 100644 index 00000000..b2e1bce6 --- /dev/null +++ b/include/pipeline/wowee_item_qualities.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Item Quality catalog (.wiqr) — novel +// replacement for the hardcoded item quality tiers in +// the WoW client (Poor / Common / Uncommon / Rare / Epic +// / Legendary / Artifact / Heirloom). Defines each tier's +// tooltip text color, inventory slot border color, +// vendor price multiplier, drop-level gating, and +// disenchant eligibility. +// +// The hardcoded client uses a static color table: +// Poor = gray #9d9d9d +// Common = white #ffffff +// Uncommon = green #1eff00 +// Rare = blue #0070dd +// Epic = purple #a335ee +// Legendary = orange #ff8000 +// Artifact = red #e6cc80 +// Heirloom = gold #00ccff +// +// This catalog lets server admins: +// - retune the colors (rename "Epic" to "Tier 1" with a +// custom orange-red, etc.) +// - add server-custom tiers above Heirloom +// (Donator / Weekly / Anniversary) +// - change vendor markup per tier (legendary sells for +// 20x base price) +// - gate quality drops by character level (Heirlooms +// unlock at lvl 80) +// +// Cross-references with previously-added formats: +// WIT: item entries reference qualityId here for +// tooltip color and sort order. +// +// Binary layout (little-endian): +// magic[4] = "WIQR" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// qualityId (uint32) +// nameLen + name +// descLen + description +// nameColorRGBA (uint32) +// borderColorRGBA (uint32) +// vendorPriceMultiplier (float) +// minLevelToDrop (uint8) / maxLevelToDrop (uint8) +// canBeDisenchanted (uint8) / pad (uint8) +// borderTexLen + inventoryBorderTexture +struct WoweeItemQuality { + struct Entry { + uint32_t qualityId = 0; + std::string name; + std::string description; + uint32_t nameColorRGBA = 0xFFFFFFFFu; + uint32_t borderColorRGBA = 0xFFFFFFFFu; + float vendorPriceMultiplier = 1.0f; + uint8_t minLevelToDrop = 1; + uint8_t maxLevelToDrop = 0; // 0 = no max + uint8_t canBeDisenchanted = 0; // 0/1 bool + uint8_t pad0 = 0; + std::string inventoryBorderTexture; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t qualityId) const; + + // Returns true if an item of this quality can drop + // for a character of the given level (gated by + // [minLevelToDrop, maxLevelToDrop] when maxLevelToDrop + // is non-zero). + bool canDropAtLevel(uint32_t qualityId, + uint8_t characterLevel) const; +}; + +class WoweeItemQualityLoader { +public: + static bool save(const WoweeItemQuality& cat, + const std::string& basePath); + static WoweeItemQuality load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-iqr* variants. + // + // makeStandard — 8 canonical WoW item quality + // tiers (Poor through Heirloom) + // with their standard hex colors + // and disenchant rules. Heirloom + // gated to lvl 80. + // makeServerCustom — 4 server-custom tiers + // (Junk / Weekly / QuestLocked / + // Donator) with custom colors and + // non-standard vendor multipliers. + // makeRaidTiers — 4 raid progression tiers + // (T1 lvl 60 / T2 lvl 60 / T3 + // lvl 60 / Legendary lvl 60+) + // gated by minLevelToDrop and + // priced for server economy. + static WoweeItemQuality makeStandard(const std::string& catalogName); + static WoweeItemQuality makeServerCustom(const std::string& catalogName); + static WoweeItemQuality makeRaidTiers(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_item_qualities.cpp b/src/pipeline/wowee_item_qualities.cpp new file mode 100644 index 00000000..757b4340 --- /dev/null +++ b/src/pipeline/wowee_item_qualities.cpp @@ -0,0 +1,263 @@ +#include "pipeline/wowee_item_qualities.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'I', 'Q', 'R'}; +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) != ".wiqr") { + base += ".wiqr"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeItemQuality::Entry* +WoweeItemQuality::findById(uint32_t qualityId) const { + for (const auto& e : entries) + if (e.qualityId == qualityId) return &e; + return nullptr; +} + +bool WoweeItemQuality::canDropAtLevel(uint32_t qualityId, + uint8_t characterLevel) const { + const Entry* e = findById(qualityId); + if (!e) return false; + if (characterLevel < e->minLevelToDrop) return false; + if (e->maxLevelToDrop != 0 && characterLevel > e->maxLevelToDrop) + return false; + return true; +} + +bool WoweeItemQualityLoader::save(const WoweeItemQuality& 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.qualityId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.nameColorRGBA); + writePOD(os, e.borderColorRGBA); + writePOD(os, e.vendorPriceMultiplier); + writePOD(os, e.minLevelToDrop); + writePOD(os, e.maxLevelToDrop); + writePOD(os, e.canBeDisenchanted); + writePOD(os, e.pad0); + writeStr(os, e.inventoryBorderTexture); + } + return os.good(); +} + +WoweeItemQuality WoweeItemQualityLoader::load( + const std::string& basePath) { + WoweeItemQuality 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.qualityId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.nameColorRGBA) || + !readPOD(is, e.borderColorRGBA) || + !readPOD(is, e.vendorPriceMultiplier) || + !readPOD(is, e.minLevelToDrop) || + !readPOD(is, e.maxLevelToDrop) || + !readPOD(is, e.canBeDisenchanted) || + !readPOD(is, e.pad0)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.inventoryBorderTexture)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeItemQualityLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeItemQuality WoweeItemQualityLoader::makeStandard( + const std::string& catalogName) { + using Q = WoweeItemQuality; + WoweeItemQuality c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t r, uint8_t g, uint8_t b, + float vendorMul, uint8_t minLvl, uint8_t maxLvl, + uint8_t disenchant, const char* texture, + const char* desc) { + Q::Entry e; + e.qualityId = id; e.name = name; e.description = desc; + e.nameColorRGBA = packRgba(r, g, b); + e.borderColorRGBA = packRgba(r, g, b); + e.vendorPriceMultiplier = vendorMul; + e.minLevelToDrop = minLvl; + e.maxLevelToDrop = maxLvl; + e.canBeDisenchanted = disenchant; + e.inventoryBorderTexture = texture; + c.entries.push_back(e); + }; + // The canonical WoW item quality scale, with hex colors + // matching the live client. Heirlooms (id 7) are gated + // to character level 80 in WotLK. + add(0, "Poor", 0x9d, 0x9d, 0x9d, 0.5f, 1, 0, 0, + "", "Poor (gray) — junk loot; vendor sells at half price."); + add(1, "Common", 0xff, 0xff, 0xff, 1.0f, 1, 0, 1, + "", "Common (white) — basic gear; standard vendor pricing."); + add(2, "Uncommon", 0x1e, 0xff, 0x00, 1.5f, 1, 0, 1, + "Border-Uncommon", + "Uncommon (green) — early-tier quest reward; 50% markup."); + add(3, "Rare", 0x00, 0x70, 0xdd, 2.0f, 1, 0, 1, + "Border-Rare", + "Rare (blue) — dungeon-tier; 2x markup, can be disenchanted."); + add(4, "Epic", 0xa3, 0x35, 0xee, 4.0f, 60, 0, 1, + "Border-Epic", + "Epic (purple) — raid-tier; 4x markup, disenchants to high-tier dust."); + add(5, "Legendary", 0xff, 0x80, 0x00, 8.0f, 60, 0, 0, + "Border-Legendary", + "Legendary (orange) — extremely rare; cannot be disenchanted."); + add(6, "Artifact", 0xe6, 0xcc, 0x80, 16.0f, 80, 0, 0, + "Border-Artifact", + "Artifact (red-gold) — unique, account-bound."); + add(7, "Heirloom", 0x00, 0xcc, 0xff, 1.0f, 80, 0, 0, + "Border-Heirloom", + "Heirloom (cyan) — scales to character level, lvl 80+ unlock."); + return c; +} + +WoweeItemQuality WoweeItemQualityLoader::makeServerCustom( + const std::string& catalogName) { + using Q = WoweeItemQuality; + WoweeItemQuality c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t r, uint8_t g, uint8_t b, + float vendorMul, uint8_t disenchant, + const char* desc) { + Q::Entry e; + e.qualityId = id; e.name = name; e.description = desc; + e.nameColorRGBA = packRgba(r, g, b); + e.borderColorRGBA = packRgba(r, g, b); + e.vendorPriceMultiplier = vendorMul; + e.canBeDisenchanted = disenchant; + c.entries.push_back(e); + }; + // 4 server-custom tiers above the standard 0..7 range. + add(100, "Junk", 0x33, 0x33, 0x33, 0.1f, 0, + "Server-custom: cosmetic junk, near-zero vendor price."); + add(101, "Weekly", 0x80, 0xff, 0x80, 5.0f, 0, + "Server-custom: drops only from weekly raids, " + "premium pricing."); + add(102, "QuestLocked",0xff, 0xff, 0x40, 0.0f, 0, + "Server-custom: quest-bound, cannot be sold."); + add(103, "Donator", 0xff, 0x40, 0xff, 0.0f, 0, + "Server-custom: donor reward, soulbound, unsellable."); + return c; +} + +WoweeItemQuality WoweeItemQualityLoader::makeRaidTiers( + const std::string& catalogName) { + using Q = WoweeItemQuality; + WoweeItemQuality c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t r, uint8_t g, uint8_t b, + float vendorMul, uint8_t minLvl, + const char* desc) { + Q::Entry e; + e.qualityId = id; e.name = name; e.description = desc; + e.nameColorRGBA = packRgba(r, g, b); + e.borderColorRGBA = packRgba(r, g, b); + e.vendorPriceMultiplier = vendorMul; + e.minLevelToDrop = minLvl; + e.canBeDisenchanted = 1; + c.entries.push_back(e); + }; + // Vanilla raid progression tiers as alternative quality + // markers — each tier gates at a higher minLevelToDrop + // and commands a higher vendor multiplier. + add(200, "Tier1Raid", 0xa3, 0x35, 0xee, 4.0f, 60, + "Tier 1 raid set (MC / Onyxia) — Epic-color, lvl 60."); + add(201, "Tier2Raid", 0xc8, 0x4c, 0xff, 6.0f, 60, + "Tier 2 raid set (BWL) — slightly brighter purple, " + "premium pricing."); + add(202, "Tier3Raid", 0xff, 0x80, 0xc8, 10.0f, 60, + "Tier 3 raid set (Naxx pre-WotLK) — pink-orange, " + "rarest pre-TBC tier."); + add(203, "Legendary", 0xff, 0x80, 0x00, 50.0f, 60, + "Tier-equivalent legendary (Thunderfury / Sulfuras / " + "Atiesh) — premium economy pricing."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 9d289526..18c39c30 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -257,6 +257,8 @@ const char* const kArgRequired[] = { "--gen-aur", "--gen-aur-stats", "--gen-aur-movement", "--info-waur", "--validate-waur", "--export-waur-json", "--import-waur-json", + "--gen-iqr", "--gen-iqr-server", "--gen-iqr-raid", + "--info-wiqr", "--validate-wiqr", "--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 eb8f7c4e..fa0a2b17 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -125,6 +125,7 @@ #include "cli_achievement_criteria_catalog.hpp" #include "cli_spell_effect_types_catalog.hpp" #include "cli_spell_aura_types_catalog.hpp" +#include "cli_item_qualities_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -291,6 +292,7 @@ constexpr DispatchFn kDispatchTable[] = { handleAchievementCriteriaCatalog, handleSpellEffectTypesCatalog, handleSpellAuraTypesCatalog, + handleItemQualitiesCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 1657d5fa..06b6c3a3 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -86,6 +86,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','A','C','R'}, ".wacr", "achieve", "--info-wacr", "Achievement criteria catalog"}, {{'W','S','E','F'}, ".wsef", "spells", "--info-wsef", "Spell effect type catalog"}, {{'W','A','U','R'}, ".waur", "spells", "--info-waur", "Spell aura type catalog"}, + {{'W','I','Q','R'}, ".wiqr", "items", "--info-wiqr", "Item quality tier 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 f49bf280..a9efcdb7 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1923,6 +1923,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .waur to a human-editable JSON sidecar (defaults to .waur.json)\n"); std::printf(" --import-waur-json [out-base]\n"); std::printf(" Import a .waur.json sidecar back into binary .waur (accepts auraKind/targetingHint int OR name; isStackable bool OR int)\n"); + std::printf(" --gen-iqr [name]\n"); + std::printf(" Emit .wiqr 8 standard quality tiers (Poor through Heirloom) with canonical hex colors and disenchant rules\n"); + std::printf(" --gen-iqr-server [name]\n"); + std::printf(" Emit .wiqr 4 server-custom tiers (Junk / Weekly / QuestLocked / Donator) with custom colors and non-standard markups\n"); + std::printf(" --gen-iqr-raid [name]\n"); + std::printf(" Emit .wiqr 4 raid progression tiers (T1/T2/T3/Legendary lvl 60+) gated by minLevelToDrop and priced for server economy\n"); + std::printf(" --info-wiqr [--json]\n"); + std::printf(" Print WIQR entries (id / name / nameColor RGBA / vendor multiplier / minLevel / maxLevel / disenchant flag / border texture)\n"); + std::printf(" --validate-wiqr [--json]\n"); + std::printf(" Static checks: name required, no duplicate ids, vendor>=0, min<=max; warns on lvl>80 (unreachable), vendor>100x (sanity), alpha=0 nameColor (invisible)\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_qualities_catalog.cpp b/tools/editor/cli_item_qualities_catalog.cpp new file mode 100644 index 00000000..be2bc26c --- /dev/null +++ b/tools/editor/cli_item_qualities_catalog.cpp @@ -0,0 +1,242 @@ +#include "cli_item_qualities_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_item_qualities.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWiqrExt(std::string base) { + stripExt(base, ".wiqr"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeItemQuality& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeItemQualityLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wiqr\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeItemQuality& c, + const std::string& base) { + std::printf("Wrote %s.wiqr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tiers : %zu\n", c.entries.size()); +} + +int handleGenStandard(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StandardQualities"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWiqrExt(base); + auto c = wowee::pipeline::WoweeItemQualityLoader::makeStandard(name); + if (!saveOrError(c, base, "gen-iqr")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenServerCustom(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ServerCustomQualities"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWiqrExt(base); + auto c = wowee::pipeline::WoweeItemQualityLoader::makeServerCustom(name); + if (!saveOrError(c, base, "gen-iqr-server")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRaidTiers(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RaidProgressionQualities"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWiqrExt(base); + auto c = wowee::pipeline::WoweeItemQualityLoader::makeRaidTiers(name); + if (!saveOrError(c, base, "gen-iqr-raid")) 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 = stripWiqrExt(base); + if (!wowee::pipeline::WoweeItemQualityLoader::exists(base)) { + std::fprintf(stderr, "WIQR not found: %s.wiqr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemQualityLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wiqr"] = base + ".wiqr"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"qualityId", e.qualityId}, + {"name", e.name}, + {"description", e.description}, + {"nameColorRGBA", e.nameColorRGBA}, + {"borderColorRGBA", e.borderColorRGBA}, + {"vendorPriceMultiplier", e.vendorPriceMultiplier}, + {"minLevelToDrop", e.minLevelToDrop}, + {"maxLevelToDrop", e.maxLevelToDrop}, + {"canBeDisenchanted", e.canBeDisenchanted != 0}, + {"inventoryBorderTexture", e.inventoryBorderTexture}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WIQR: %s.wiqr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tiers : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id name nameColor vendorMul minLvl maxLvl DE border\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-10s 0x%08x %6.2fx %4u %4u %s %s\n", + e.qualityId, e.name.c_str(), + e.nameColorRGBA, e.vendorPriceMultiplier, + e.minLevelToDrop, e.maxLevelToDrop, + e.canBeDisenchanted ? "yes" : "no ", + e.inventoryBorderTexture.empty() ? "(none)" : + e.inventoryBorderTexture.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWiqrExt(base); + if (!wowee::pipeline::WoweeItemQualityLoader::exists(base)) { + std::fprintf(stderr, + "validate-wiqr: WIQR not found: %s.wiqr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeItemQualityLoader::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.qualityId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.vendorPriceMultiplier < 0.0f) { + errors.push_back(ctx + + ": vendorPriceMultiplier < 0 — vendor would " + "pay the player to take items"); + } + if (e.maxLevelToDrop != 0 && + e.minLevelToDrop > e.maxLevelToDrop) { + errors.push_back(ctx + ": minLevelToDrop " + + std::to_string(e.minLevelToDrop) + + " > maxLevelToDrop " + + std::to_string(e.maxLevelToDrop) + + " — quality will never drop"); + } + if (e.minLevelToDrop > 80) { + warnings.push_back(ctx + + ": minLevelToDrop " + + std::to_string(e.minLevelToDrop) + + " > 80 — quality unreachable at WotLK cap"); + } + if (e.vendorPriceMultiplier > 100.0f) { + warnings.push_back(ctx + + ": vendorPriceMultiplier " + + std::to_string(e.vendorPriceMultiplier) + + "x is very high — sanity check the economy"); + } + // Pure transparent color is suspicious (alpha=0). + if ((e.nameColorRGBA & 0xFF000000u) == 0) { + warnings.push_back(ctx + + ": nameColorRGBA has alpha=0 — text will be " + "invisible in tooltips"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.qualityId) { + errors.push_back(ctx + ": duplicate qualityId"); + break; + } + } + idsSeen.push_back(e.qualityId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wiqr"] = base + ".wiqr"; + 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-wiqr: %s.wiqr\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu tiers, all qualityIds 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 handleItemQualitiesCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-iqr") == 0 && i + 1 < argc) { + outRc = handleGenStandard(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-iqr-server") == 0 && i + 1 < argc) { + outRc = handleGenServerCustom(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-iqr-raid") == 0 && i + 1 < argc) { + outRc = handleGenRaidTiers(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wiqr") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wiqr") == 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_qualities_catalog.hpp b/tools/editor/cli_item_qualities_catalog.hpp new file mode 100644 index 00000000..ede7e6a3 --- /dev/null +++ b/tools/editor/cli_item_qualities_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleItemQualitiesCatalog(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 90c223ac..5e7fd931 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -108,6 +108,7 @@ constexpr FormatRow kFormats[] = { {"WACR", ".wacr", "achieve", "Achievement_Criteria.dbc", "Achievement criteria catalog"}, {"WSEF", ".wsef", "spells", "SpellEffect.Effect dispatch", "Spell effect type catalog"}, {"WAUR", ".waur", "spells", "SpellEffect.EffectAuraType", "Spell aura type catalog"}, + {"WIQR", ".wiqr", "items", "Item quality tier colors+rules", "Item quality tier catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine