From 48c770f5ead0567e861f4f32d9587230d83def14 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 22:05:05 -0700 Subject: [PATCH] feat(editor): add WGFS (Glyph Slot) open catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for Blizzard's GlyphSlot.dbc. Defines the per-class glyph slot layout: which slots a class has (Major / Minor / Prime), in which display order they appear in the spellbook UI, and at which character level each slot becomes available for use. Distinct from WGLY (GlyphProperties) which defines the individual glyphs themselves. WGLY says "Glyph of Polymorph exists, costs 1 inscription dust, modifies Polymorph"; WGFS says "the slot that holds Glyph of Polymorph is the second Major Glyph Slot, unlocks at level 25, and only Mages have it". Layout grew across expansions, captured by the three presets: - --gen-gfs — 6 slots: 3 Major + 3 Minor all-class baseline (25/50/75 each) - --gen-gfs-wotlk — 6 slots: 3 Major (15/30/50) + 3 Minor (15/50/70) matching WotLK 3.3.5a - --gen-gfs-cata — 9 slots: 3 Prime + 3 Major + 3 Minor matching Cataclysm Cross-references back to WGLY (glyphs reference slotKind to constrain which glyph fits which slot) and WCHC (requiredClassMask uses the same bit layout as WCHC class IDs). Validation enforces id+name+classMask presence (classMask=0 means no class can use the slot — usually a config bug), slotKind 0..2, no duplicate ids; warns on minLevelToUnlock>80 (would never unlock at WotLK cap), displayOrder>4 (UI typically shows 3-4), and (kind+order) collisions for overlapping classMask (two slots claiming the same UI position would render on top of each other). isUnlockedFor(id, classBit, level) is the engine helper. Wired through the cross-format table; WGFS appears automatically in all 11 cross-format utilities. Format count 73 -> 74; CLI flag count 929 -> 934. --- CMakeLists.txt | 3 + include/pipeline/wowee_glyph_slots.hpp | 112 ++++++++++ src/pipeline/wowee_glyph_slots.cpp | 246 ++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_glyph_slots_catalog.cpp | 254 +++++++++++++++++++++++ tools/editor/cli_glyph_slots_catalog.hpp | 12 ++ tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 643 insertions(+) create mode 100644 include/pipeline/wowee_glyph_slots.hpp create mode 100644 src/pipeline/wowee_glyph_slots.cpp create mode 100644 tools/editor/cli_glyph_slots_catalog.cpp create mode 100644 tools/editor/cli_glyph_slots_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c634f35e..fae3ea68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -662,6 +662,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spell_cooldowns.cpp src/pipeline/wowee_creature_families.cpp src/pipeline/wowee_spell_power_costs.cpp + src/pipeline/wowee_glyph_slots.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1481,6 +1482,7 @@ add_executable(wowee_editor tools/editor/cli_spell_cooldowns_catalog.cpp tools/editor/cli_creature_families_catalog.cpp tools/editor/cli_spell_power_costs_catalog.cpp + tools/editor/cli_glyph_slots_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1621,6 +1623,7 @@ add_executable(wowee_editor src/pipeline/wowee_spell_cooldowns.cpp src/pipeline/wowee_creature_families.cpp src/pipeline/wowee_spell_power_costs.cpp + src/pipeline/wowee_glyph_slots.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_glyph_slots.hpp b/include/pipeline/wowee_glyph_slots.hpp new file mode 100644 index 00000000..e538e9a8 --- /dev/null +++ b/include/pipeline/wowee_glyph_slots.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Glyph Slot catalog (.wgfs) — novel +// replacement for Blizzard's GlyphSlot.dbc. Defines the +// per-class glyph slot layout: which slots a class +// has (Major / Minor / Prime), in which display order +// they appear in the spellbook UI, and at which character +// level each slot becomes available for use. +// +// Distinct from WGLY (GlyphProperties), which defines the +// individual glyphs themselves. WGLY says "Glyph of +// Polymorph exists, costs 1 inscription dust, modifies +// Polymorph"; WGFS says "the slot that holds Glyph of +// Polymorph is the second Major Glyph Slot, unlocks at +// level 25, and only Mages have it". +// +// Layout grew across expansions: +// Wrath of the Lich King — 3 Major + 3 Minor (6 slots) +// Cataclysm — 3 Prime + 3 Major + 3 Minor (9 slots) +// The presets cover both, plus a starter Classic-style +// "any class" 6-slot layout for the simplest case. +// +// Cross-references with previously-added formats: +// WGLY: glyph entries reference slotKind to constrain +// which glyph fits which slot kind. +// WCHC: requiredClassMask uses the same bit layout as +// WCHC class IDs (Warrior=0x01, Paladin=0x02, ...). +// +// Binary layout (little-endian): +// magic[4] = "WGFS" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// slotId (uint32) +// nameLen + name +// descLen + description +// slotKind (uint8) / displayOrder (uint8) +// minLevelToUnlock (uint8) / pad (uint8) +// requiredClassMask (uint32) +// iconColorRGBA (uint32) +struct WoweeGlyphSlot { + enum SlotKind : uint8_t { + Major = 0, // class-defining utility / mechanic glyphs + Minor = 1, // cosmetic / convenience glyphs + Prime = 2, // damage-output glyphs (Cata+) + }; + + struct Entry { + uint32_t slotId = 0; + std::string name; + std::string description; + uint8_t slotKind = Major; + uint8_t displayOrder = 0; + uint8_t minLevelToUnlock = 25; + uint8_t pad0 = 0; + uint32_t requiredClassMask = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t slotId) const; + + // Returns true if a character of the given class+level + // has access to this slot. classBit is one of the WCHC + // class-bit flags (Warrior=0x01, Paladin=0x02, ...). + bool isUnlockedFor(uint32_t slotId, + uint32_t classBit, + uint8_t characterLevel) const; + + static const char* slotKindName(uint8_t k); +}; + +class WoweeGlyphSlotLoader { +public: + static bool save(const WoweeGlyphSlot& cat, + const std::string& basePath); + static WoweeGlyphSlot load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-gfs* variants. + // + // makeStarter — 6 slots: 3 Major + 3 Minor available + // to every class (classMask=0xFFFFFFFF), + // unlocking at 25/50/75 for each kind. + // Simplest baseline layout. + // makeWotlk — 6 slots: 3 Major + 3 Minor matching + // the WotLK 3.3.5a layout (any class). + // Major unlocks at 15/30/50, Minor at + // 15/50/70. + // makeCata — 9 slots: 3 Prime + 3 Major + 3 Minor + // matching the Cataclysm layout. Prime + // unlocks at 25/50/75, Major at 25/50/75, + // Minor at 25/50/75. + static WoweeGlyphSlot makeStarter(const std::string& catalogName); + static WoweeGlyphSlot makeWotlk(const std::string& catalogName); + static WoweeGlyphSlot makeCata(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_glyph_slots.cpp b/src/pipeline/wowee_glyph_slots.cpp new file mode 100644 index 00000000..dcdf2a1d --- /dev/null +++ b/src/pipeline/wowee_glyph_slots.cpp @@ -0,0 +1,246 @@ +#include "pipeline/wowee_glyph_slots.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'G', 'F', 'S'}; +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) != ".wgfs") { + base += ".wgfs"; + } + 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 WoweeGlyphSlot::Entry* +WoweeGlyphSlot::findById(uint32_t slotId) const { + for (const auto& e : entries) + if (e.slotId == slotId) return &e; + return nullptr; +} + +bool WoweeGlyphSlot::isUnlockedFor(uint32_t slotId, + uint32_t classBit, + uint8_t characterLevel) const { + const Entry* e = findById(slotId); + if (!e) return false; + if ((e->requiredClassMask & classBit) == 0) return false; + return characterLevel >= e->minLevelToUnlock; +} + +const char* WoweeGlyphSlot::slotKindName(uint8_t k) { + switch (k) { + case Major: return "major"; + case Minor: return "minor"; + case Prime: return "prime"; + default: return "unknown"; + } +} + +bool WoweeGlyphSlotLoader::save(const WoweeGlyphSlot& 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.slotId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.slotKind); + writePOD(os, e.displayOrder); + writePOD(os, e.minLevelToUnlock); + writePOD(os, e.pad0); + writePOD(os, e.requiredClassMask); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeGlyphSlot WoweeGlyphSlotLoader::load( + const std::string& basePath) { + WoweeGlyphSlot 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.slotId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.slotKind) || + !readPOD(is, e.displayOrder) || + !readPOD(is, e.minLevelToUnlock) || + !readPOD(is, e.pad0) || + !readPOD(is, e.requiredClassMask) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeGlyphSlotLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeGlyphSlot WoweeGlyphSlotLoader::makeStarter( + const std::string& catalogName) { + using G = WoweeGlyphSlot; + WoweeGlyphSlot c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t order, uint8_t lvl, + uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + G::Entry e; + e.slotId = id; e.name = name; e.description = desc; + e.slotKind = kind; + e.displayOrder = order; + e.minLevelToUnlock = lvl; + e.requiredClassMask = 0xFFFFFFFFu; // all classes + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + // Three Major + three Minor, all-class baseline. + add(1, "MajorSlot1", G::Major, 0, 25, 240, 200, 100, + "Major glyph slot 1 — unlocks at level 25."); + add(2, "MajorSlot2", G::Major, 1, 50, 240, 200, 100, + "Major glyph slot 2 — unlocks at level 50."); + add(3, "MajorSlot3", G::Major, 2, 75, 240, 200, 100, + "Major glyph slot 3 — unlocks at level 75."); + add(4, "MinorSlot1", G::Minor, 0, 25, 150, 200, 240, + "Minor glyph slot 1 — unlocks at level 25."); + add(5, "MinorSlot2", G::Minor, 1, 50, 150, 200, 240, + "Minor glyph slot 2 — unlocks at level 50."); + add(6, "MinorSlot3", G::Minor, 2, 75, 150, 200, 240, + "Minor glyph slot 3 — unlocks at level 75."); + return c; +} + +WoweeGlyphSlot WoweeGlyphSlotLoader::makeWotlk( + const std::string& catalogName) { + using G = WoweeGlyphSlot; + WoweeGlyphSlot c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t order, uint8_t lvl, const char* desc) { + G::Entry e; + e.slotId = id; e.name = name; e.description = desc; + e.slotKind = kind; + e.displayOrder = order; + e.minLevelToUnlock = lvl; + e.requiredClassMask = 0xFFFFFFFFu; + // Color by kind so the UI distinguishes them. + e.iconColorRGBA = (kind == G::Major) ? packRgba(240, 200, 100) + : packRgba(150, 200, 240); + c.entries.push_back(e); + }; + // WotLK 3.3.5a: 3 Major + 3 Minor with staggered + // unlocks 15/30/50 and 15/50/70. + add(100, "WotlkMajor1", G::Major, 0, 15, "Major slot 1 — unlocks at 15."); + add(101, "WotlkMajor2", G::Major, 1, 30, "Major slot 2 — unlocks at 30."); + add(102, "WotlkMajor3", G::Major, 2, 50, "Major slot 3 — unlocks at 50."); + add(103, "WotlkMinor1", G::Minor, 0, 15, "Minor slot 1 — unlocks at 15."); + add(104, "WotlkMinor2", G::Minor, 1, 50, "Minor slot 2 — unlocks at 50."); + add(105, "WotlkMinor3", G::Minor, 2, 70, "Minor slot 3 — unlocks at 70."); + return c; +} + +WoweeGlyphSlot WoweeGlyphSlotLoader::makeCata( + const std::string& catalogName) { + using G = WoweeGlyphSlot; + WoweeGlyphSlot c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t order, uint8_t lvl, const char* desc) { + G::Entry e; + e.slotId = id; e.name = name; e.description = desc; + e.slotKind = kind; + e.displayOrder = order; + e.minLevelToUnlock = lvl; + e.requiredClassMask = 0xFFFFFFFFu; + // Color by kind: prime=red, major=gold, minor=blue. + if (kind == G::Prime) e.iconColorRGBA = packRgba(240, 100, 100); + else if (kind == G::Major) e.iconColorRGBA = packRgba(240, 200, 100); + else e.iconColorRGBA = packRgba(150, 200, 240); + c.entries.push_back(e); + }; + // Cataclysm layout: 3 Prime + 3 Major + 3 Minor. + add(200, "CataPrime1", G::Prime, 0, 25, "Prime slot 1 — unlocks at 25."); + add(201, "CataPrime2", G::Prime, 1, 50, "Prime slot 2 — unlocks at 50."); + add(202, "CataPrime3", G::Prime, 2, 75, "Prime slot 3 — unlocks at 75."); + add(203, "CataMajor1", G::Major, 0, 25, "Major slot 1 — unlocks at 25."); + add(204, "CataMajor2", G::Major, 1, 50, "Major slot 2 — unlocks at 50."); + add(205, "CataMajor3", G::Major, 2, 75, "Major slot 3 — unlocks at 75."); + add(206, "CataMinor1", G::Minor, 0, 25, "Minor slot 1 — unlocks at 25."); + add(207, "CataMinor2", G::Minor, 1, 50, "Minor slot 2 — unlocks at 50."); + add(208, "CataMinor3", G::Minor, 2, 75, "Minor slot 3 — unlocks at 75."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index b8e331b2..54e096da 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -225,6 +225,8 @@ const char* const kArgRequired[] = { "--gen-spc", "--gen-spc-rage", "--gen-spc-mixed", "--info-wspc", "--validate-wspc", "--export-wspc-json", "--import-wspc-json", + "--gen-gfs", "--gen-gfs-wotlk", "--gen-gfs-cata", + "--info-wgfs", "--validate-wgfs", "--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 a5831d66..4edbb03d 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -112,6 +112,7 @@ #include "cli_spell_cooldowns_catalog.hpp" #include "cli_creature_families_catalog.hpp" #include "cli_spell_power_costs_catalog.hpp" +#include "cli_glyph_slots_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -265,6 +266,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellCooldownsCatalog, handleCreatureFamiliesCatalog, handleSpellPowerCostsCatalog, + handleGlyphSlotsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index a0d4c204..25446ba6 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -76,6 +76,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','C','D'}, ".wscd", "spells", "--info-wscd", "Spell cooldown category catalog"}, {{'W','C','E','F'}, ".wcef", "creatures", "--info-wcef", "Creature / pet family catalog"}, {{'W','S','P','C'}, ".wspc", "spells", "--info-wspc", "Spell power cost bucket catalog"}, + {{'W','G','F','S'}, ".wgfs", "glyphs", "--info-wgfs", "Glyph slot layout 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_glyph_slots_catalog.cpp b/tools/editor/cli_glyph_slots_catalog.cpp new file mode 100644 index 00000000..eb824fff --- /dev/null +++ b/tools/editor/cli_glyph_slots_catalog.cpp @@ -0,0 +1,254 @@ +#include "cli_glyph_slots_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_glyph_slots.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWgfsExt(std::string base) { + stripExt(base, ".wgfs"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeGlyphSlot& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeGlyphSlotLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wgfs\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeGlyphSlot& c, + const std::string& base) { + std::printf("Wrote %s.wgfs\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" slots : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterGlyphSlots"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgfsExt(base); + auto c = wowee::pipeline::WoweeGlyphSlotLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-gfs")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWotlk(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WotlkGlyphSlots"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgfsExt(base); + auto c = wowee::pipeline::WoweeGlyphSlotLoader::makeWotlk(name); + if (!saveOrError(c, base, "gen-gfs-wotlk")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCata(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CataclysmGlyphSlots"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWgfsExt(base); + auto c = wowee::pipeline::WoweeGlyphSlotLoader::makeCata(name); + if (!saveOrError(c, base, "gen-gfs-cata")) 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 = stripWgfsExt(base); + if (!wowee::pipeline::WoweeGlyphSlotLoader::exists(base)) { + std::fprintf(stderr, "WGFS not found: %s.wgfs\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGlyphSlotLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wgfs"] = base + ".wgfs"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"slotId", e.slotId}, + {"name", e.name}, + {"description", e.description}, + {"slotKind", e.slotKind}, + {"slotKindName", wowee::pipeline::WoweeGlyphSlot::slotKindName(e.slotKind)}, + {"displayOrder", e.displayOrder}, + {"minLevelToUnlock", e.minLevelToUnlock}, + {"requiredClassMask", e.requiredClassMask}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WGFS: %s.wgfs\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" slots : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind ord lvl classMask name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-7s %u %3u 0x%08x %s\n", + e.slotId, + wowee::pipeline::WoweeGlyphSlot::slotKindName(e.slotKind), + e.displayOrder, + e.minLevelToUnlock, + e.requiredClassMask, + 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 = stripWgfsExt(base); + if (!wowee::pipeline::WoweeGlyphSlotLoader::exists(base)) { + std::fprintf(stderr, + "validate-wgfs: WGFS not found: %s.wgfs\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeGlyphSlotLoader::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.slotId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.slotId == 0) + errors.push_back(ctx + ": slotId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.slotKind > wowee::pipeline::WoweeGlyphSlot::Prime) { + errors.push_back(ctx + ": slotKind " + + std::to_string(e.slotKind) + " not in 0..2"); + } + if (e.requiredClassMask == 0) { + errors.push_back(ctx + + ": requiredClassMask is 0 — no class can use " + "this slot"); + } + if (e.minLevelToUnlock > 80) { + warnings.push_back(ctx + + ": minLevelToUnlock " + + std::to_string(e.minLevelToUnlock) + + " > 80 — slot will never unlock at WotLK cap"); + } + if (e.displayOrder > 4) { + warnings.push_back(ctx + + ": displayOrder " + + std::to_string(e.displayOrder) + + " > 4 — UI typically shows only 3-4 slots per kind"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.slotId) { + errors.push_back(ctx + ": duplicate slotId"); + break; + } + } + idsSeen.push_back(e.slotId); + } + // Cross-entry check: detect overlapping (kind,order) + // pairs within the same class — two slots claiming the + // same UI position would collide. + for (size_t a = 0; a < c.entries.size(); ++a) { + for (size_t b = a + 1; b < c.entries.size(); ++b) { + const auto& ea = c.entries[a]; + const auto& eb = c.entries[b]; + if (ea.slotKind != eb.slotKind) continue; + if (ea.displayOrder != eb.displayOrder) continue; + if ((ea.requiredClassMask & eb.requiredClassMask) == 0) + continue; + warnings.push_back( + "entries " + std::to_string(a) + " (" + + ea.name + ") and " + std::to_string(b) + + " (" + eb.name + ") share " + + wowee::pipeline::WoweeGlyphSlot::slotKindName(ea.slotKind) + + " kind + displayOrder " + + std::to_string(ea.displayOrder) + + " for overlapping classMask — UI position collision"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wgfs"] = base + ".wgfs"; + 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-wgfs: %s.wgfs\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu slots, all slotIds unique, no UI overlaps\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 handleGlyphSlotsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-gfs") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gfs-wotlk") == 0 && i + 1 < argc) { + outRc = handleGenWotlk(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-gfs-cata") == 0 && i + 1 < argc) { + outRc = handleGenCata(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wgfs") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wgfs") == 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_glyph_slots_catalog.hpp b/tools/editor/cli_glyph_slots_catalog.hpp new file mode 100644 index 00000000..542e740c --- /dev/null +++ b/tools/editor/cli_glyph_slots_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleGlyphSlotsCatalog(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 375647a2..d9e11e54 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1775,6 +1775,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wspc to a human-editable JSON sidecar (defaults to .wspc.json)\n"); std::printf(" --import-wspc-json [out-base]\n"); std::printf(" Import a .wspc.json sidecar back into binary .wspc (accepts powerType int OR name; costFlags int OR pipe-separated label string)\n"); + std::printf(" --gen-gfs [name]\n"); + std::printf(" Emit .wgfs starter: 6 baseline glyph slots (3 Major + 3 Minor) all-class, unlocking at 25/50/75 each\n"); + std::printf(" --gen-gfs-wotlk [name]\n"); + std::printf(" Emit .wgfs WotLK 3.3.5a layout: 3 Major (15/30/50) + 3 Minor (15/50/70), all-class\n"); + std::printf(" --gen-gfs-cata [name]\n"); + std::printf(" Emit .wgfs Cataclysm layout: 3 Prime + 3 Major + 3 Minor (9 slots), all unlocking at 25/50/75\n"); + std::printf(" --info-wgfs [--json]\n"); + std::printf(" Print WGFS entries (id / kind / displayOrder / minLevelToUnlock / classMask / name)\n"); + std::printf(" --validate-wgfs [--json]\n"); + std::printf(" Static checks: id+name+classMask required, slotKind 0..2, no duplicate ids; warns on lvl>80, displayOrder>4, and (kind+order) collisions for overlapping classMask\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_list_formats.cpp b/tools/editor/cli_list_formats.cpp index d655ea0c..87591a9c 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -98,6 +98,7 @@ constexpr FormatRow kFormats[] = { {"WSCD", ".wscd", "spells", "SpellCooldown.dbc + shared cd grp","Spell cooldown category catalog"}, {"WCEF", ".wcef", "creatures", "CreatureFamily.dbc + pet trees", "Creature / pet family catalog"}, {"WSPC", ".wspc", "spells", "Spell.dbc power-cost fields", "Spell power cost bucket catalog"}, + {"WGFS", ".wgfs", "glyphs", "GlyphSlot.dbc", "Glyph slot layout catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine