mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(editor): add WGFS (Glyph Slot) open catalog format
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.
This commit is contained in:
parent
47d8892f74
commit
48c770f5ea
10 changed files with 643 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
112
include/pipeline/wowee_glyph_slots.hpp
Normal file
112
include/pipeline/wowee_glyph_slots.hpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Entry> 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
|
||||
246
src/pipeline/wowee_glyph_slots.cpp
Normal file
246
src/pipeline/wowee_glyph_slots.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#include "pipeline/wowee_glyph_slots.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'G', 'F', 'S'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
template <typename T>
|
||||
void writePOD(std::ofstream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPOD(std::ifstream& is, T& v) {
|
||||
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
||||
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
||||
}
|
||||
|
||||
void writeStr(std::ofstream& os, const std::string& s) {
|
||||
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(a) << 24) |
|
||||
(static_cast<uint32_t>(b) << 16) |
|
||||
(static_cast<uint32_t>(g) << 8) |
|
||||
static_cast<uint32_t>(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<uint32_t>(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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
254
tools/editor/cli_glyph_slots_catalog.cpp
Normal file
254
tools/editor/cli_glyph_slots_catalog.cpp
Normal file
|
|
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> 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
|
||||
12
tools/editor/cli_glyph_slots_catalog.hpp
Normal file
12
tools/editor/cli_glyph_slots_catalog.hpp
Normal file
|
|
@ -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
|
||||
|
|
@ -1775,6 +1775,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wspc to a human-editable JSON sidecar (defaults to <base>.wspc.json)\n");
|
||||
std::printf(" --import-wspc-json <json-path> [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 <wgfs-base> [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 <wgfs-base> [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 <wgfs-base> [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 <wgfs-base> [--json]\n");
|
||||
std::printf(" Print WGFS entries (id / kind / displayOrder / minLevelToUnlock / classMask / name)\n");
|
||||
std::printf(" --validate-wgfs <wgfs-base> [--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 <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue