mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
feat(pipeline): WSPK spell pack catalog (126th open format)
Novel replacement for the implicit per-class spellbook layout
that vanilla WoW derived from SkillLineAbility.dbc + the hard-
coded per-spec tab order baked into the client UI. Each WSPK
entry binds one (classId, tabIndex) pair to an ordered list of
spellIds shown in that spellbook tab.
Three presets seeded with canonical vanilla low-rank spellIds:
--gen-spk-warrior 4 tabs (General + Arms/Fury/Protection)
including Charge, Mortal Strike,
Bloodthirst, Shield Block
--gen-spk-mage 4 tabs (General + Arcane/Fire/Frost)
including Frostbolt rank 1 (spellId 116)
— the canonical "every mage starts here"
--gen-spk-rogue 4 tabs (General + Assassination/Combat/
Subtlety) with poison + lethality picks
Validator catches: packId+tabName required, classId in 1..11,
tabIndex in 0..3, no duplicate packIds, no duplicate
(classId,tabIndex) pairs (spellbook UI dispatch tie), no zero
spellIds, no duplicate spellIds within any single tab (would
render twice in spellbook). Warns on classId 6 and 10 (vanilla
PlayerClass DBC gaps) and on empty tabs (player would see a
blank spellbook tab).
Format count 125 -> 126. CLI flag count 1328 -> 1335.
This commit is contained in:
parent
fa30db7ae1
commit
6d9d00fbb9
10 changed files with 712 additions and 0 deletions
|
|
@ -714,6 +714,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_localization.cpp
|
||||
src/pipeline/wowee_global_channels.cpp
|
||||
src/pipeline/wowee_addon_manifest.cpp
|
||||
src/pipeline/wowee_spell_pack.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1591,6 +1592,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_localization_catalog.cpp
|
||||
tools/editor/cli_global_channels_catalog.cpp
|
||||
tools/editor/cli_addon_manifest_catalog.cpp
|
||||
tools/editor/cli_spell_pack_catalog.cpp
|
||||
tools/editor/cli_catalog_pluck.cpp
|
||||
tools/editor/cli_catalog_find.cpp
|
||||
tools/editor/cli_catalog_by_name.cpp
|
||||
|
|
@ -1787,6 +1789,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_localization.cpp
|
||||
src/pipeline/wowee_global_channels.cpp
|
||||
src/pipeline/wowee_addon_manifest.cpp
|
||||
src/pipeline/wowee_spell_pack.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
100
include/pipeline/wowee_spell_pack.hpp
Normal file
100
include/pipeline/wowee_spell_pack.hpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Spell Pack catalog (.wspk) — novel
|
||||
// replacement for the implicit per-class spellbook
|
||||
// layout that vanilla WoW derived from
|
||||
// SkillLineAbility.dbc + SpellTabIcon mappings + the
|
||||
// hard-coded per-spec tab order baked into the client
|
||||
// UI. Each WSPK entry binds one (classId, tabIndex)
|
||||
// pair to an ordered list of spellIds shown in that
|
||||
// spellbook tab.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WSPL: spellIds in the ordered list are looked up
|
||||
// against WSPL spell catalog at runtime.
|
||||
// WCDB: classId references the playable-class
|
||||
// catalog (currently 1..11 in vanilla:
|
||||
// Warrior=1 ... Druid=11).
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WSPK"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// packId (uint32) — surrogate primary key
|
||||
// for cross-format
|
||||
// --catalog-find lookups
|
||||
// classId (uint8) — 1..11 vanilla class
|
||||
// tabIndex (uint8) — 0=General/3 spec tabs
|
||||
// iconIndex (uint8) — SpellIcon row id for
|
||||
// the tab header glyph
|
||||
// pad0 (uint8)
|
||||
// tabNameLen + tabName — display label for the
|
||||
// spellbook tab
|
||||
// spellCount (uint32)
|
||||
// spellIds (uint32 × count) — ordered display
|
||||
// list (top-to-
|
||||
// bottom in tab)
|
||||
struct WoweeSpellPack {
|
||||
struct Entry {
|
||||
uint32_t packId = 0;
|
||||
uint8_t classId = 0;
|
||||
uint8_t tabIndex = 0;
|
||||
uint8_t iconIndex = 0;
|
||||
uint8_t pad0 = 0;
|
||||
std::string tabName;
|
||||
std::vector<uint32_t> spellIds;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
const Entry* findById(uint32_t packId) const;
|
||||
const Entry* findByClassTab(uint8_t classId,
|
||||
uint8_t tabIndex) const;
|
||||
|
||||
// Returns all packs for a class (typically 4: General
|
||||
// + 3 spec tabs). Used by the spellbook-screen UI to
|
||||
// populate per-class tab order.
|
||||
std::vector<const Entry*> findByClass(uint8_t classId) const;
|
||||
};
|
||||
|
||||
class WoweeSpellPackLoader {
|
||||
public:
|
||||
static bool save(const WoweeSpellPack& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeSpellPack load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-spk* variants.
|
||||
//
|
||||
// makeWarriorPack — 4 tabs (General + 3 trees:
|
||||
// Arms/Fury/Protection). Each
|
||||
// tab seeded with canonical
|
||||
// vanilla spellIds.
|
||||
// makeMagePack — 4 tabs (General + Arcane/
|
||||
// Fire/Frost). Frost tab
|
||||
// includes Frostbolt rank-1
|
||||
// spellId 116 — the canonical
|
||||
// "every mage starts here" spell.
|
||||
// makeRoguePack — 4 tabs (General + Assassin/
|
||||
// Combat/Subtlety). Combat tab
|
||||
// seeded with poison-application
|
||||
// and lethality picks.
|
||||
static WoweeSpellPack makeWarriorPack(const std::string& catalogName);
|
||||
static WoweeSpellPack makeMagePack(const std::string& catalogName);
|
||||
static WoweeSpellPack makeRoguePack(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
290
src/pipeline/wowee_spell_pack.cpp
Normal file
290
src/pipeline/wowee_spell_pack.cpp
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#include "pipeline/wowee_spell_pack.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'S', 'P', 'K'};
|
||||
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;
|
||||
}
|
||||
|
||||
void writeU32Vec(std::ofstream& os,
|
||||
const std::vector<uint32_t>& v) {
|
||||
uint32_t n = static_cast<uint32_t>(v.size());
|
||||
writePOD(os, n);
|
||||
if (n > 0) {
|
||||
os.write(reinterpret_cast<const char*>(v.data()),
|
||||
static_cast<std::streamsize>(n * sizeof(uint32_t)));
|
||||
}
|
||||
}
|
||||
|
||||
bool readU32Vec(std::ifstream& is, std::vector<uint32_t>& v) {
|
||||
uint32_t n = 0;
|
||||
if (!readPOD(is, n)) return false;
|
||||
if (n > 4096) return false;
|
||||
v.resize(n);
|
||||
if (n > 0) {
|
||||
is.read(reinterpret_cast<char*>(v.data()),
|
||||
static_cast<std::streamsize>(n * sizeof(uint32_t)));
|
||||
if (is.gcount() !=
|
||||
static_cast<std::streamsize>(n * sizeof(uint32_t))) {
|
||||
v.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalizePath(std::string base) {
|
||||
if (base.size() < 5 || base.substr(base.size() - 5) != ".wspk") {
|
||||
base += ".wspk";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeSpellPack::Entry*
|
||||
WoweeSpellPack::findById(uint32_t packId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.packId == packId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const WoweeSpellPack::Entry*
|
||||
WoweeSpellPack::findByClassTab(uint8_t classId,
|
||||
uint8_t tabIndex) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.classId == classId && e.tabIndex == tabIndex)
|
||||
return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const WoweeSpellPack::Entry*>
|
||||
WoweeSpellPack::findByClass(uint8_t classId) const {
|
||||
std::vector<const Entry*> out;
|
||||
for (const auto& e : entries)
|
||||
if (e.classId == classId) out.push_back(&e);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeSpellPackLoader::save(const WoweeSpellPack& 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.packId);
|
||||
writePOD(os, e.classId);
|
||||
writePOD(os, e.tabIndex);
|
||||
writePOD(os, e.iconIndex);
|
||||
writePOD(os, e.pad0);
|
||||
writeStr(os, e.tabName);
|
||||
writeU32Vec(os, e.spellIds);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeSpellPack WoweeSpellPackLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweeSpellPack 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.packId) ||
|
||||
!readPOD(is, e.classId) ||
|
||||
!readPOD(is, e.tabIndex) ||
|
||||
!readPOD(is, e.iconIndex) ||
|
||||
!readPOD(is, e.pad0)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.tabName)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readU32Vec(is, e.spellIds)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeSpellPackLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper to build one tab entry. classId follows
|
||||
// vanilla DBC class IDs: Warrior=1, Mage=8, Rogue=4.
|
||||
struct TabSpec {
|
||||
uint32_t packId;
|
||||
uint8_t classId;
|
||||
uint8_t tabIndex;
|
||||
uint8_t iconIndex;
|
||||
const char* tabName;
|
||||
std::vector<uint32_t> spellIds;
|
||||
};
|
||||
|
||||
WoweeSpellPack makeFromTabs(const std::string& catalogName,
|
||||
std::vector<TabSpec> tabs) {
|
||||
using P = WoweeSpellPack;
|
||||
WoweeSpellPack c;
|
||||
c.name = catalogName;
|
||||
for (auto& t : tabs) {
|
||||
P::Entry e;
|
||||
e.packId = t.packId;
|
||||
e.classId = t.classId;
|
||||
e.tabIndex = t.tabIndex;
|
||||
e.iconIndex = t.iconIndex;
|
||||
e.tabName = t.tabName;
|
||||
e.spellIds = std::move(t.spellIds);
|
||||
c.entries.push_back(std::move(e));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WoweeSpellPack WoweeSpellPackLoader::makeWarriorPack(
|
||||
const std::string& catalogName) {
|
||||
// classId=1 (Warrior). Tab 0=General, 1=Arms,
|
||||
// 2=Fury, 3=Protection. SpellIds are canonical
|
||||
// vanilla low-rank picks: Charge=100, Heroic
|
||||
// Strike=78, Mortal Strike=12294, Bloodthirst=23881,
|
||||
// Shield Block=2565, etc.
|
||||
return makeFromTabs(catalogName, {
|
||||
{1001, 1, 0, 1, "General",
|
||||
{78, // Heroic Strike rank 1
|
||||
100, // Charge rank 1
|
||||
6673, // Battle Shout rank 1
|
||||
2457, // Battle Stance
|
||||
}},
|
||||
{1002, 1, 1, 30, "Arms",
|
||||
{12294, // Mortal Strike
|
||||
1680, // Whirlwind
|
||||
7384, // Overpower
|
||||
}},
|
||||
{1003, 1, 2, 31, "Fury",
|
||||
{23881, // Bloodthirst
|
||||
5308, // Execute
|
||||
1719, // Recklessness
|
||||
}},
|
||||
{1004, 1, 3, 32, "Protection",
|
||||
{2565, // Shield Block
|
||||
871, // Shield Wall
|
||||
355, // Taunt
|
||||
}},
|
||||
});
|
||||
}
|
||||
|
||||
WoweeSpellPack WoweeSpellPackLoader::makeMagePack(
|
||||
const std::string& catalogName) {
|
||||
// classId=8 (Mage). Frost tab includes Frostbolt
|
||||
// rank 1 (spellId 116) — the canonical "every
|
||||
// mage starts with this" spell.
|
||||
return makeFromTabs(catalogName, {
|
||||
{2001, 8, 0, 5, "General",
|
||||
{133, // Fireball rank 1
|
||||
168, // Frost Armor rank 1
|
||||
1459, // Arcane Intellect rank 1
|
||||
}},
|
||||
{2002, 8, 1, 50, "Arcane",
|
||||
{1449, // Arcane Explosion rank 1
|
||||
5143, // Arcane Missiles rank 1
|
||||
1953, // Blink
|
||||
}},
|
||||
{2003, 8, 2, 51, "Fire",
|
||||
{2120, // Flamestrike rank 1
|
||||
11366, // Pyroblast rank 1
|
||||
2948, // Scorch rank 1
|
||||
}},
|
||||
{2004, 8, 3, 52, "Frost",
|
||||
{116, // Frostbolt rank 1 — every mage
|
||||
// begins here
|
||||
122, // Frost Nova rank 1
|
||||
10, // Blizzard rank 1
|
||||
}},
|
||||
});
|
||||
}
|
||||
|
||||
WoweeSpellPack WoweeSpellPackLoader::makeRoguePack(
|
||||
const std::string& catalogName) {
|
||||
// classId=4 (Rogue). Combat tab seeded with
|
||||
// poison-application + lethality picks.
|
||||
return makeFromTabs(catalogName, {
|
||||
{3001, 4, 0, 7, "General",
|
||||
{1752, // Sinister Strike rank 1
|
||||
1784, // Stealth rank 1
|
||||
921, // Pickpocket
|
||||
}},
|
||||
{3002, 4, 1, 70, "Assassination",
|
||||
{703, // Garrote rank 1
|
||||
8676, // Ambush rank 1
|
||||
2098, // Eviscerate rank 1
|
||||
}},
|
||||
{3003, 4, 2, 71, "Combat",
|
||||
{2983, // Sprint rank 1
|
||||
1856, // Vanish rank 1
|
||||
8647, // Expose Armor rank 1
|
||||
}},
|
||||
{3004, 4, 3, 72, "Subtlety",
|
||||
{1857, // Vanish rank 2
|
||||
5277, // Evasion
|
||||
14185, // Preparation
|
||||
}},
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -386,6 +386,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-mod", "--gen-mod-ui", "--gen-mod-util",
|
||||
"--info-wmod", "--validate-wmod",
|
||||
"--export-wmod-json", "--import-wmod-json",
|
||||
"--gen-spk-warrior", "--gen-spk-mage", "--gen-spk-rogue",
|
||||
"--info-wspk", "--validate-wspk",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@
|
|||
#include "cli_localization_catalog.hpp"
|
||||
#include "cli_global_channels_catalog.hpp"
|
||||
#include "cli_addon_manifest_catalog.hpp"
|
||||
#include "cli_spell_pack_catalog.hpp"
|
||||
#include "cli_catalog_pluck.hpp"
|
||||
#include "cli_catalog_find.hpp"
|
||||
#include "cli_catalog_by_name.hpp"
|
||||
|
|
@ -385,6 +386,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleLocalizationCatalog,
|
||||
handleGlobalChannelsCatalog,
|
||||
handleAddonManifestCatalog,
|
||||
handleSpellPackCatalog,
|
||||
handleCatalogPluck,
|
||||
handleCatalogFind,
|
||||
handleCatalogByName,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ constexpr FormatMagicEntry kFormats[] = {
|
|||
{{'W','L','A','N'}, ".wlan", "i18n", "--info-wlan", "Localization catalog"},
|
||||
{{'W','G','C','H'}, ".wgch", "chat", "--info-wgch", "Global chat channel catalog"},
|
||||
{{'W','M','O','D'}, ".wmod", "addons", "--info-wmod", "Addon manifest catalog"},
|
||||
{{'W','S','P','K'}, ".wspk", "spells", "--info-wspk", "Spell pack 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"},
|
||||
|
|
|
|||
|
|
@ -2517,6 +2517,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wmod to a human-editable JSON sidecar (defaults to <base>.wmod.json; emits dependencies and optionalDependencies as JSON int arrays)\n");
|
||||
std::printf(" --import-wmod-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wmod.json sidecar back into binary .wmod (dependency arrays accept JSON int arrays — round-trips chained dep graphs byte-identical)\n");
|
||||
std::printf(" --gen-spk-warrior <wspk-base> [name]\n");
|
||||
std::printf(" Emit .wspk Warrior class spellbook layout — 4 tabs (General + Arms/Fury/Protection) with canonical vanilla low-rank spellIds\n");
|
||||
std::printf(" --gen-spk-mage <wspk-base> [name]\n");
|
||||
std::printf(" Emit .wspk Mage class spellbook layout — 4 tabs (General + Arcane/Fire/Frost) including Frostbolt rank 1 (spellId 116)\n");
|
||||
std::printf(" --gen-spk-rogue <wspk-base> [name]\n");
|
||||
std::printf(" Emit .wspk Rogue class spellbook layout — 4 tabs (General + Assassination/Combat/Subtlety) with poison + lethality picks\n");
|
||||
std::printf(" --info-wspk <wspk-base> [--json]\n");
|
||||
std::printf(" Print WSPK entries (packId / classId+name / tabIndex / iconIndex / spell count / tabName)\n");
|
||||
std::printf(" --validate-wspk <wspk-base> [--json]\n");
|
||||
std::printf(" Static checks: packId+tabName required, classId in 1..11, tabIndex in 0..3, no duplicate packIds, no duplicate (classId,tabIndex) pairs (spellbook UI dispatch tie), no zero spellIds, no duplicate spellIds within any tab; warns on classId 6/10 (vanilla DBC gap) and on empty tabs (player would see blank spellbook)\n");
|
||||
std::printf(" --catalog-pluck <wXXX-file> <id> [--json]\n");
|
||||
std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n");
|
||||
std::printf(" --catalog-find <directory> <id> [--magic <WXXX>] [--json]\n");
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ constexpr FormatRow kFormats[] = {
|
|||
{"WLAN", ".wlan", "i18n", "Locale_*.MPQ + DBC trailing strings","Localization catalog (per-language string overlay)"},
|
||||
{"WGCH", ".wgch", "chat", "ChatChannels.dbc + zone-default joins","Global chat channel catalog (access policy + zone auto-join)"},
|
||||
{"WMOD", ".wmod", "addons", "per-addon TOC text + load-order rules","Addon manifest catalog (deps + cycle detection)"},
|
||||
{"WSPK", ".wspk", "spells", "SkillLineAbility + per-spec tab order","Spell pack catalog (per-class spellbook tab layout)"},
|
||||
|
||||
// Additional pipeline catalogs without the alternating
|
||||
// gen/info/validate CLI surface (loaded by the engine
|
||||
|
|
|
|||
291
tools/editor/cli_spell_pack_catalog.cpp
Normal file
291
tools/editor/cli_spell_pack_catalog.cpp
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
#include "cli_spell_pack_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_spell_pack.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWspkExt(std::string base) {
|
||||
stripExt(base, ".wspk");
|
||||
return base;
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeSpellPack& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeSpellPackLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wspk\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeSpellPack& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wspk\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" packs : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenWarrior(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "WarriorSpellPack";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWspkExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellPackLoader::
|
||||
makeWarriorPack(name);
|
||||
if (!saveOrError(c, base, "gen-spk-warrior")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenMage(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "MageSpellPack";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWspkExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellPackLoader::
|
||||
makeMagePack(name);
|
||||
if (!saveOrError(c, base, "gen-spk-mage")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenRogue(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "RogueSpellPack";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWspkExt(base);
|
||||
auto c = wowee::pipeline::WoweeSpellPackLoader::
|
||||
makeRoguePack(name);
|
||||
if (!saveOrError(c, base, "gen-spk-rogue")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* classIdName(uint8_t c) {
|
||||
// Vanilla 1.12 PlayerClass DBC ids — used for the
|
||||
// info-table display only.
|
||||
switch (c) {
|
||||
case 1: return "Warrior";
|
||||
case 2: return "Paladin";
|
||||
case 3: return "Hunter";
|
||||
case 4: return "Rogue";
|
||||
case 5: return "Priest";
|
||||
case 7: return "Shaman";
|
||||
case 8: return "Mage";
|
||||
case 9: return "Warlock";
|
||||
case 11: return "Druid";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
int handleInfo(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWspkExt(base);
|
||||
if (!wowee::pipeline::WoweeSpellPackLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WSPK not found: %s.wspk\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeSpellPackLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wspk"] = base + ".wspk";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
arr.push_back({
|
||||
{"packId", e.packId},
|
||||
{"classId", e.classId},
|
||||
{"className", classIdName(e.classId)},
|
||||
{"tabIndex", e.tabIndex},
|
||||
{"iconIndex", e.iconIndex},
|
||||
{"tabName", e.tabName},
|
||||
{"spellIds", e.spellIds},
|
||||
});
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WSPK: %s.wspk\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" packs : %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
std::printf(" id class tab icon spells tabName\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" %4u %2u %-9s %3u %4u %5zu %s\n",
|
||||
e.packId, e.classId,
|
||||
classIdName(e.classId),
|
||||
e.tabIndex, e.iconIndex,
|
||||
e.spellIds.size(), e.tabName.c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWspkExt(base);
|
||||
if (!wowee::pipeline::WoweeSpellPackLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wspk: WSPK not found: %s.wspk\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeSpellPackLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::set<uint32_t> packIdsSeen;
|
||||
std::set<std::pair<uint8_t, uint8_t>> classTabPairs;
|
||||
for (size_t k = 0; k < c.entries.size(); ++k) {
|
||||
const auto& e = c.entries[k];
|
||||
std::string ctx = "entry " + std::to_string(k) +
|
||||
" (packId=" + std::to_string(e.packId);
|
||||
if (!e.tabName.empty()) ctx += " " + e.tabName;
|
||||
ctx += ")";
|
||||
if (e.packId == 0)
|
||||
errors.push_back(ctx + ": packId is 0");
|
||||
if (e.tabName.empty())
|
||||
errors.push_back(ctx + ": tabName is empty");
|
||||
// Vanilla classes: 1..11 with id 6 + 10 unused.
|
||||
if (e.classId == 0 || e.classId > 11) {
|
||||
errors.push_back(ctx + ": classId " +
|
||||
std::to_string(e.classId) +
|
||||
" out of vanilla range (1..11)");
|
||||
}
|
||||
if (e.classId == 6 || e.classId == 10) {
|
||||
warnings.push_back(ctx + ": classId " +
|
||||
std::to_string(e.classId) +
|
||||
" is unused in vanilla (gap in PlayerClass DBC)");
|
||||
}
|
||||
// Tab 0 = General; 1..3 = the three spec trees.
|
||||
if (e.tabIndex > 3) {
|
||||
errors.push_back(ctx + ": tabIndex " +
|
||||
std::to_string(e.tabIndex) +
|
||||
" out of range (0..3 — General + 3 specs)");
|
||||
}
|
||||
// (classId, tabIndex) MUST be unique — the
|
||||
// spellbook UI dispatches by this pair, two
|
||||
// entries with the same pair would tie.
|
||||
auto pair = std::make_pair(e.classId, e.tabIndex);
|
||||
if (!classTabPairs.insert(pair).second) {
|
||||
errors.push_back(ctx +
|
||||
": duplicate (classId=" +
|
||||
std::to_string(e.classId) +
|
||||
", tabIndex=" +
|
||||
std::to_string(e.tabIndex) +
|
||||
") — spellbook UI tab dispatch tie");
|
||||
}
|
||||
if (!packIdsSeen.insert(e.packId).second) {
|
||||
errors.push_back(ctx + ": duplicate packId");
|
||||
}
|
||||
// Per-tab spell uniqueness — the same spellId
|
||||
// appearing twice in one tab is a copy-paste bug
|
||||
// (the UI would render it twice).
|
||||
std::set<uint32_t> spellsInTab;
|
||||
for (uint32_t sid : e.spellIds) {
|
||||
if (sid == 0) {
|
||||
errors.push_back(ctx +
|
||||
": tab contains spellId 0 (placeholder "
|
||||
"or copy-paste error)");
|
||||
}
|
||||
if (sid != 0 && !spellsInTab.insert(sid).second) {
|
||||
errors.push_back(ctx +
|
||||
": duplicate spellId " +
|
||||
std::to_string(sid) +
|
||||
" within tab — would render twice in "
|
||||
"spellbook");
|
||||
}
|
||||
}
|
||||
// Empty tab: warn — General tab with zero spells
|
||||
// means the player starts with no abilities at
|
||||
// all on that tree.
|
||||
if (e.spellIds.empty()) {
|
||||
warnings.push_back(ctx +
|
||||
": tab has zero spells — player would see "
|
||||
"an empty spellbook tab");
|
||||
}
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wspk"] = base + ".wspk";
|
||||
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-wspk: %s.wspk\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu packs, all packIds unique, "
|
||||
"(classId,tabIndex) unique, classId in "
|
||||
"1..11, tabIndex in 0..3, no duplicate "
|
||||
"spellIds within any tab\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 handleSpellPackCatalog(int& i, int argc, char** argv,
|
||||
int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-spk-warrior") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenWarrior(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-spk-mage") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenMage(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-spk-rogue") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenRogue(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wspk") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wspk") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
12
tools/editor/cli_spell_pack_catalog.hpp
Normal file
12
tools/editor/cli_spell_pack_catalog.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleSpellPackCatalog(int& i, int argc, char** argv,
|
||||
int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue