mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +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
|
|
@ -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