feat(pipeline): add WTSK (Wowee Trade Skill / Recipe) catalog

New open format — replaces SkillLineAbility.dbc plus the
recipe portions of SkillLine.dbc plus the AzerothCore
trade_skill SQL tables. Closes the crafting gap left by WSKL
(which carries skill lines but not the recipes that bind to
them).

14 professions (Blacksmithing, Tailoring, Engineering,
Alchemy, Enchanting, Leatherworking, Jewelcrafting,
Inscription, Mining, Skinning, Herbalism, Cooking, FirstAid,
Fishing). Each recipe has 4 skill-up bracket thresholds
(orange / yellow / green / gray) for skill-up probability,
a craft spell cross-ref (WSPL), produced item cross-ref
(WIT) with min/max quantity range, an optional tool item,
and up to 4 reagent slots (itemId + count).

Cross-references with prior formats — craftSpellId points at
WSPL.spellId, producedItemId / toolItemId / reagent[].itemId
all point at WIT.itemId, and skillId points at WSKL.skillId.

CLI: --gen-tsk (3-recipe entry-tier starter), --gen-tsk-
blacksmithing (5-recipe progression rough sharpening through
truesilver champion), --gen-tsk-alchemy (5-recipe progression
minor healing through flask of titans), --info-wtsk,
--validate-wtsk with --json variants. Validator catches
id=0/duplicates, profession out of range, missing craft spell
or produced item, monotonic-bracket check (must be orange <=
yellow <= green <= gray), reagent itemId-without-count
mismatch, and free-recipe warning (no reagents and no tool).

Format graph now exposes 49 distinct binary formats. CLI
flag count: 747 → 752.
This commit is contained in:
Kelsi 2026-05-09 19:41:49 -07:00
parent 6b05136ef1
commit 33a7b4b3cf
10 changed files with 786 additions and 0 deletions

View file

@ -637,6 +637,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_spell_visuals.cpp
src/pipeline/wowee_world_state_ui.cpp
src/pipeline/wowee_player_conditions.cpp
src/pipeline/wowee_trade_skills.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1425,6 +1426,7 @@ add_executable(wowee_editor
tools/editor/cli_rename_magic.cpp
tools/editor/cli_world_state_ui_catalog.cpp
tools/editor/cli_player_conditions_catalog.cpp
tools/editor/cli_trade_skills_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1540,6 +1542,7 @@ add_executable(wowee_editor
src/pipeline/wowee_spell_visuals.cpp
src/pipeline/wowee_world_state_ui.cpp
src/pipeline/wowee_player_conditions.cpp
src/pipeline/wowee_trade_skills.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,132 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Trade Skill / Recipe catalog (.wtsk) — novel
// replacement for Blizzard's SkillLineAbility.dbc plus the
// recipe portions of SkillLine.dbc plus the AzerothCore
// trade_skill SQL tables. The 50th open format added to
// the editor — a milestone format that closes the crafting
// gap left by WSKL (which only carries the skill lines
// themselves, not the recipes that bind to them).
//
// Defines per-profession recipes: Blacksmithing, Tailoring,
// Engineering, Alchemy, Enchanting, Leatherworking, Mining,
// Skinning, Herbalism, Cooking, First Aid, Fishing. Each
// recipe binds a craft spell (WSPL) to a produced item
// (WIT) and up to 4 reagent slots, gated by a skill rank
// threshold and bracket-coloured (orange / yellow / green
// / gray) for skill-up probability.
//
// Cross-references with previously-added formats:
// WTSK.entry.craftSpellId → WSPL.spellId
// WTSK.entry.producedItemId → WIT.itemId
// WTSK.entry.toolItemId → WIT.itemId (anvil/loom/...)
// WTSK.entry.reagent[0..3] → WIT.itemId
// WTSK.entry.skillId → WSKL.skillId
//
// Binary layout (little-endian):
// magic[4] = "WTSK"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// recipeId (uint32)
// nameLen + name
// descLen + description
// iconLen + iconPath
// profession (uint8) / pad[3]
// skillId (uint32)
// orangeRank (uint16) / yellowRank (uint16) /
// greenRank (uint16) / grayRank (uint16)
// craftSpellId (uint32)
// producedItemId (uint32)
// producedMinCount (uint8) / producedMaxCount (uint8) / pad[2]
// toolItemId (uint32)
// reagentItemId[4] (uint32) / reagentCount[4] (uint8) / pad[4]
struct WoweeTradeSkill {
enum Profession : uint8_t {
Blacksmithing = 0,
Tailoring = 1,
Engineering = 2,
Alchemy = 3,
Enchanting = 4,
Leatherworking = 5,
Jewelcrafting = 6,
Inscription = 7,
Mining = 8,
Skinning = 9,
Herbalism = 10,
Cooking = 11,
FirstAid = 12,
Fishing = 13,
};
static constexpr size_t kMaxReagents = 4;
struct Entry {
uint32_t recipeId = 0;
std::string name;
std::string description;
std::string iconPath;
uint8_t profession = Blacksmithing;
uint32_t skillId = 0; // WSKL cross-ref
uint16_t orangeRank = 1; // 100% skill-up chance
uint16_t yellowRank = 25; // ~75% skill-up
uint16_t greenRank = 50; // ~25% skill-up
uint16_t grayRank = 75; // 0% skill-up
uint32_t craftSpellId = 0; // WSPL cross-ref
uint32_t producedItemId = 0; // WIT cross-ref
uint8_t producedMinCount = 1;
uint8_t producedMaxCount = 1;
uint32_t toolItemId = 0; // WIT cross-ref (anvil/loom)
uint32_t reagentItemId[kMaxReagents] = {0, 0, 0, 0};
uint8_t reagentCount[kMaxReagents] = {0, 0, 0, 0};
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t recipeId) const;
static const char* professionName(uint8_t p);
};
class WoweeTradeSkillLoader {
public:
static bool save(const WoweeTradeSkill& cat,
const std::string& basePath);
static WoweeTradeSkill load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-tsk* variants.
//
// makeStarter — 3 recipes covering the entry-tier
// spread (Coarse Sharpening Stone,
// Linen Cloth Bandage, Minor
// Healing Potion) — one each for
// Blacksmithing / First Aid /
// Alchemy.
// makeBlacksmithing — 5 progression recipes
// (sharpening stone, copper chain
// belt, runed copper bracers,
// ironforge breastplate,
// truesilver champion).
// makeAlchemy — 5 progression recipes (minor
// healing, swiftness, lesser
// mana, greater healing,
// flask of titans).
static WoweeTradeSkill makeStarter(const std::string& catalogName);
static WoweeTradeSkill makeBlacksmithing(const std::string& catalogName);
static WoweeTradeSkill makeAlchemy(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,331 @@
#include "pipeline/wowee_trade_skills.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'T', 'S', '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;
}
std::string normalizePath(std::string base) {
if (base.size() < 5 || base.substr(base.size() - 5) != ".wtsk") {
base += ".wtsk";
}
return base;
}
} // namespace
const WoweeTradeSkill::Entry*
WoweeTradeSkill::findById(uint32_t recipeId) const {
for (const auto& e : entries)
if (e.recipeId == recipeId) return &e;
return nullptr;
}
const char* WoweeTradeSkill::professionName(uint8_t p) {
switch (p) {
case Blacksmithing: return "blacksmithing";
case Tailoring: return "tailoring";
case Engineering: return "engineering";
case Alchemy: return "alchemy";
case Enchanting: return "enchanting";
case Leatherworking: return "leatherworking";
case Jewelcrafting: return "jewelcrafting";
case Inscription: return "inscription";
case Mining: return "mining";
case Skinning: return "skinning";
case Herbalism: return "herbalism";
case Cooking: return "cooking";
case FirstAid: return "first-aid";
case Fishing: return "fishing";
default: return "unknown";
}
}
bool WoweeTradeSkillLoader::save(const WoweeTradeSkill& 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.recipeId);
writeStr(os, e.name);
writeStr(os, e.description);
writeStr(os, e.iconPath);
writePOD(os, e.profession);
uint8_t pad3[3] = {0, 0, 0};
os.write(reinterpret_cast<const char*>(pad3), 3);
writePOD(os, e.skillId);
writePOD(os, e.orangeRank);
writePOD(os, e.yellowRank);
writePOD(os, e.greenRank);
writePOD(os, e.grayRank);
writePOD(os, e.craftSpellId);
writePOD(os, e.producedItemId);
writePOD(os, e.producedMinCount);
writePOD(os, e.producedMaxCount);
uint8_t pad2[2] = {0, 0};
os.write(reinterpret_cast<const char*>(pad2), 2);
writePOD(os, e.toolItemId);
for (size_t k = 0; k < WoweeTradeSkill::kMaxReagents; ++k) {
writePOD(os, e.reagentItemId[k]);
}
for (size_t k = 0; k < WoweeTradeSkill::kMaxReagents; ++k) {
writePOD(os, e.reagentCount[k]);
}
}
return os.good();
}
WoweeTradeSkill WoweeTradeSkillLoader::load(const std::string& basePath) {
WoweeTradeSkill 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.recipeId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description) ||
!readStr(is, e.iconPath)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.profession)) {
out.entries.clear(); return out;
}
uint8_t pad3[3];
is.read(reinterpret_cast<char*>(pad3), 3);
if (is.gcount() != 3) { out.entries.clear(); return out; }
if (!readPOD(is, e.skillId) ||
!readPOD(is, e.orangeRank) ||
!readPOD(is, e.yellowRank) ||
!readPOD(is, e.greenRank) ||
!readPOD(is, e.grayRank) ||
!readPOD(is, e.craftSpellId) ||
!readPOD(is, e.producedItemId) ||
!readPOD(is, e.producedMinCount) ||
!readPOD(is, e.producedMaxCount)) {
out.entries.clear(); return out;
}
uint8_t pad2[2];
is.read(reinterpret_cast<char*>(pad2), 2);
if (is.gcount() != 2) { out.entries.clear(); return out; }
if (!readPOD(is, e.toolItemId)) {
out.entries.clear(); return out;
}
for (size_t k = 0; k < WoweeTradeSkill::kMaxReagents; ++k) {
if (!readPOD(is, e.reagentItemId[k])) {
out.entries.clear(); return out;
}
}
for (size_t k = 0; k < WoweeTradeSkill::kMaxReagents; ++k) {
if (!readPOD(is, e.reagentCount[k])) {
out.entries.clear(); return out;
}
}
}
return out;
}
bool WoweeTradeSkillLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeTradeSkill WoweeTradeSkillLoader::makeStarter(
const std::string& catalogName) {
WoweeTradeSkill c;
c.name = catalogName;
{
// Coarse Sharpening Stone — Blacksmithing 75.
WoweeTradeSkill::Entry e;
e.recipeId = 1; e.name = "Coarse Sharpening Stone";
e.description = "Use stone on a weapon to apply +2 damage "
"for 30 minutes.";
e.iconPath = "Interface/Icons/Inv_Stone_Sharpening_03.blp";
e.profession = WoweeTradeSkill::Blacksmithing;
e.skillId = 164; // WSKL Blacksmithing skillId
e.orangeRank = 75; e.yellowRank = 95;
e.greenRank = 115; e.grayRank = 135;
e.craftSpellId = 3326; // canonical craft spellId
e.producedItemId = 2862; // canonical item
e.producedMinCount = 1; e.producedMaxCount = 1;
e.toolItemId = 5956; // Blacksmith Hammer
e.reagentItemId[0] = 2836; // Coarse Stone
e.reagentCount[0] = 1;
c.entries.push_back(e);
}
{
// Linen Bandage — First Aid 1.
WoweeTradeSkill::Entry e;
e.recipeId = 2; e.name = "Linen Bandage";
e.description = "Heal target for 66 health over 8 seconds.";
e.iconPath = "Interface/Icons/Inv_Misc_Bandage_15.blp";
e.profession = WoweeTradeSkill::FirstAid;
e.skillId = 129; // WSKL First Aid skillId
e.orangeRank = 1; e.yellowRank = 30;
e.greenRank = 60; e.grayRank = 90;
e.craftSpellId = 3275;
e.producedItemId = 1251;
e.producedMinCount = 1; e.producedMaxCount = 1;
e.reagentItemId[0] = 2589; // Linen Cloth
e.reagentCount[0] = 1;
c.entries.push_back(e);
}
{
// Minor Healing Potion — Alchemy 1.
WoweeTradeSkill::Entry e;
e.recipeId = 3; e.name = "Minor Healing Potion";
e.description = "Restores 70 to 90 health.";
e.iconPath = "Interface/Icons/Inv_Potion_50.blp";
e.profession = WoweeTradeSkill::Alchemy;
e.skillId = 171; // WSKL Alchemy skillId
e.orangeRank = 1; e.yellowRank = 55;
e.greenRank = 85; e.grayRank = 115;
e.craftSpellId = 2330;
e.producedItemId = 118;
e.producedMinCount = 1; e.producedMaxCount = 2;
e.toolItemId = 4470; // Empty Vial / Alchemist's Lab
e.reagentItemId[0] = 765; // Silverleaf
e.reagentCount[0] = 1;
e.reagentItemId[1] = 2453; // Briarthorn
e.reagentCount[1] = 1;
c.entries.push_back(e);
}
return c;
}
WoweeTradeSkill WoweeTradeSkillLoader::makeBlacksmithing(
const std::string& catalogName) {
WoweeTradeSkill c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint16_t orange,
uint16_t yellow, uint16_t green, uint16_t gray,
uint32_t spellId, uint32_t itemId, uint32_t tool,
uint32_t r1, uint8_t r1c,
uint32_t r2, uint8_t r2c,
uint32_t r3, uint8_t r3c, const char* desc) {
WoweeTradeSkill::Entry e;
e.recipeId = id; e.name = name; e.description = desc;
e.iconPath = std::string("Interface/Icons/Inv_") + name + ".blp";
e.profession = WoweeTradeSkill::Blacksmithing;
e.skillId = 164;
e.orangeRank = orange; e.yellowRank = yellow;
e.greenRank = green; e.grayRank = gray;
e.craftSpellId = spellId; e.producedItemId = itemId;
e.toolItemId = tool;
e.reagentItemId[0] = r1; e.reagentCount[0] = r1c;
e.reagentItemId[1] = r2; e.reagentCount[1] = r2c;
e.reagentItemId[2] = r3; e.reagentCount[2] = r3c;
c.entries.push_back(e);
};
add(100, "RoughSharpeningStone", 1, 25, 50, 75, 2660, 2862, 5956,
2835, 1, 0, 0, 0, 0,
"Apply to weapon — minor temp damage buff.");
add(101, "CopperChainBelt", 50, 70, 90, 110, 2664, 2386, 5956,
2840, 4, 0, 0, 0, 0,
"Light chain belt for early-level warriors.");
add(102, "RunedCopperBracers", 100, 120, 140, 160, 2667, 2406, 5956,
2840, 6, 818, 1, 0, 0,
"Bracers with a minor magic enhancement.");
add(103, "IronforgeBreastplate", 195, 215, 235, 255, 9959, 7915, 5956,
2842, 8, 3858, 4, 0, 0,
"Heavy iron breastplate — Ironforge guard standard issue.");
add(104, "TruesilverChampion", 265, 285, 305, 325, 16728, 12793, 5956,
7910, 10, 7910, 5, 12808, 1,
"Pinnacle 60-era plate — requires arcanite reagents.");
return c;
}
WoweeTradeSkill WoweeTradeSkillLoader::makeAlchemy(
const std::string& catalogName) {
WoweeTradeSkill c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint16_t orange,
uint16_t yellow, uint16_t green, uint16_t gray,
uint32_t spellId, uint32_t itemId,
uint8_t produceMin, uint8_t produceMax,
uint32_t r1, uint8_t r1c,
uint32_t r2, uint8_t r2c, const char* desc) {
WoweeTradeSkill::Entry e;
e.recipeId = id; e.name = name; e.description = desc;
e.iconPath = std::string("Interface/Icons/Inv_Potion_") +
name + ".blp";
e.profession = WoweeTradeSkill::Alchemy;
e.skillId = 171;
e.orangeRank = orange; e.yellowRank = yellow;
e.greenRank = green; e.grayRank = gray;
e.craftSpellId = spellId; e.producedItemId = itemId;
e.producedMinCount = produceMin; e.producedMaxCount = produceMax;
e.toolItemId = 4470; // Empty Vial / Alchemist Lab
e.reagentItemId[0] = r1; e.reagentCount[0] = r1c;
e.reagentItemId[1] = r2; e.reagentCount[1] = r2c;
c.entries.push_back(e);
};
add(200, "MinorHealing", 1, 55, 85, 115, 2330, 118, 1, 2,
765, 1, 2453, 1, "Restores 70 to 90 health.");
add(201, "Swiftness", 60, 85, 115, 145, 2336, 858, 1, 1,
2447, 1, 2452, 1, "Free-action / move speed buff.");
add(202, "LesserMana", 90, 115, 140, 165, 2331, 3385, 1, 2,
785, 1, 2453, 1, "Restores 140 to 180 mana.");
add(203, "GreaterHealing", 155, 175, 200, 225, 3171, 1710, 1, 2,
3819, 1, 3820, 1, "Restores 455 to 585 health.");
add(204, "FlaskOfTheTitans", 300, 320, 340, 360, 17636, 13510, 1, 1,
13463, 30, 13468, 10,
"2-hour flask — +400 max health, persists through death.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -147,6 +147,8 @@ const char* const kArgRequired[] = {
"--gen-pcn", "--gen-pcn-quest-gates", "--gen-pcn-composite",
"--info-wpcn", "--validate-wpcn",
"--export-wpcn-json", "--import-wpcn-json",
"--gen-tsk", "--gen-tsk-blacksmithing", "--gen-tsk-alchemy",
"--info-wtsk", "--validate-wtsk",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -81,6 +81,7 @@
#include "cli_rename_magic.hpp"
#include "cli_world_state_ui_catalog.hpp"
#include "cli_player_conditions_catalog.hpp"
#include "cli_trade_skills_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -203,6 +204,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleRenameMagic,
handleWorldStateUICatalog,
handlePlayerConditionsCatalog,
handleTradeSkillsCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -51,6 +51,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"},
{{'W','W','U','I'}, ".wwui", "ui", "--info-wwui", "World-state UI catalog"},
{{'W','P','C','N'}, ".wpcn", "logic", "--info-wpcn", "Player condition catalog"},
{{'W','T','S','K'}, ".wtsk", "crafting", "--info-wtsk", "Trade skill recipe 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"},

View file

@ -1411,6 +1411,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wpcn to a human-editable JSON sidecar (defaults to <base>.wpcn.json)\n");
std::printf(" --import-wpcn-json <json-path> [out-base]\n");
std::printf(" Import a .wpcn.json sidecar back into binary .wpcn (accepts conditionKind/comparisonOp/chainOp int OR name string)\n");
std::printf(" --gen-tsk <wtsk-base> [name]\n");
std::printf(" Emit .wtsk starter: 3 entry-tier recipes (Coarse Sharpening Stone / Linen Bandage / Minor Healing Potion)\n");
std::printf(" --gen-tsk-blacksmithing <wtsk-base> [name]\n");
std::printf(" Emit .wtsk 5-recipe Blacksmithing progression (rough sharpening → truesilver champion plate)\n");
std::printf(" --gen-tsk-alchemy <wtsk-base> [name]\n");
std::printf(" Emit .wtsk 5-recipe Alchemy progression (minor healing → flask of titans) with reagent slots\n");
std::printf(" --info-wtsk <wtsk-base> [--json]\n");
std::printf(" Print WTSK entries (id / profession / 4 skill brackets / craft spell / produced item / qty / tool / reagent count / name)\n");
std::printf(" --validate-wtsk <wtsk-base> [--json]\n");
std::printf(" Static checks: id>0+unique, name not empty, profession 0..13, craft spell + produced item required, monotonic skill brackets\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");

View file

@ -73,6 +73,7 @@ constexpr FormatRow kFormats[] = {
{"WSVK", ".wsvk", "spellfx", "SpellVisualKit.dbc + SpellVisFx", "Spell visual kit (cast/proj/impact effects)"},
{"WWUI", ".wwui", "ui", "WorldStateUI.dbc + world_state", "World-state UI (BG scoreboards / siege counters)"},
{"WPCN", ".wpcn", "logic", "PlayerCondition.dbc + conditions", "Player condition (gates, AND/OR/NOT chains)"},
{"WTSK", ".wtsk", "crafting", "SkillLineAbility.dbc + recipes", "Trade skill recipes (per-profession crafts)"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,293 @@
#include "cli_trade_skills_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_trade_skills.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 stripWtskExt(std::string base) {
stripExt(base, ".wtsk");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeTradeSkill& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeTradeSkillLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wtsk\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeTradeSkill& c,
const std::string& base) {
std::printf("Wrote %s.wtsk\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" recipes : %zu\n", c.entries.size());
}
int handleGenStarter(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "StarterRecipes";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtskExt(base);
auto c = wowee::pipeline::WoweeTradeSkillLoader::makeStarter(name);
if (!saveOrError(c, base, "gen-tsk")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenBlacksmithing(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "BlacksmithingRecipes";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtskExt(base);
auto c = wowee::pipeline::WoweeTradeSkillLoader::makeBlacksmithing(name);
if (!saveOrError(c, base, "gen-tsk-blacksmithing")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenAlchemy(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "AlchemyRecipes";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWtskExt(base);
auto c = wowee::pipeline::WoweeTradeSkillLoader::makeAlchemy(name);
if (!saveOrError(c, base, "gen-tsk-alchemy")) return 1;
printGenSummary(c, base);
return 0;
}
void appendEntryJson(nlohmann::json& arr,
const wowee::pipeline::WoweeTradeSkill::Entry& e) {
nlohmann::json reagents = nlohmann::json::array();
for (size_t k = 0;
k < wowee::pipeline::WoweeTradeSkill::kMaxReagents; ++k) {
if (e.reagentItemId[k] == 0 && e.reagentCount[k] == 0) continue;
reagents.push_back({
{"itemId", e.reagentItemId[k]},
{"count", e.reagentCount[k]},
});
}
arr.push_back({
{"recipeId", e.recipeId},
{"name", e.name},
{"description", e.description},
{"iconPath", e.iconPath},
{"profession", e.profession},
{"professionName", wowee::pipeline::WoweeTradeSkill::professionName(e.profession)},
{"skillId", e.skillId},
{"orangeRank", e.orangeRank},
{"yellowRank", e.yellowRank},
{"greenRank", e.greenRank},
{"grayRank", e.grayRank},
{"craftSpellId", e.craftSpellId},
{"producedItemId", e.producedItemId},
{"producedMinCount", e.producedMinCount},
{"producedMaxCount", e.producedMaxCount},
{"toolItemId", e.toolItemId},
{"reagents", reagents},
});
}
int handleInfo(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = consumeJsonFlag(i, argc, argv);
base = stripWtskExt(base);
if (!wowee::pipeline::WoweeTradeSkillLoader::exists(base)) {
std::fprintf(stderr, "WTSK not found: %s.wtsk\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTradeSkillLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wtsk"] = base + ".wtsk";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) appendEntryJson(arr, e);
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WTSK: %s.wtsk\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" recipes : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id profession ranks(O/Y/G/Gr) spell item qty tool rgts name\n");
for (const auto& e : c.entries) {
size_t reagentCount = 0;
for (size_t k = 0;
k < wowee::pipeline::WoweeTradeSkill::kMaxReagents; ++k) {
if (e.reagentItemId[k] != 0 || e.reagentCount[k] != 0)
++reagentCount;
}
std::printf(" %4u %-13s %3u/%3u/%3u/%3u %5u %5u %u-%u %5u %4zu %s\n",
e.recipeId,
wowee::pipeline::WoweeTradeSkill::professionName(e.profession),
e.orangeRank, e.yellowRank, e.greenRank, e.grayRank,
e.craftSpellId, e.producedItemId,
e.producedMinCount, e.producedMaxCount,
e.toolItemId, reagentCount, 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 = stripWtskExt(base);
if (!wowee::pipeline::WoweeTradeSkillLoader::exists(base)) {
std::fprintf(stderr,
"validate-wtsk: WTSK not found: %s.wtsk\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeTradeSkillLoader::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.recipeId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.recipeId == 0)
errors.push_back(ctx + ": recipeId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.profession > wowee::pipeline::WoweeTradeSkill::Fishing) {
errors.push_back(ctx + ": profession " +
std::to_string(e.profession) + " not in 0..13");
}
if (e.craftSpellId == 0)
errors.push_back(ctx +
": craftSpellId is 0 (recipe has no craft action)");
if (e.producedItemId == 0)
errors.push_back(ctx +
": producedItemId is 0 (recipe produces nothing)");
if (e.producedMinCount == 0 || e.producedMaxCount == 0) {
errors.push_back(ctx +
": producedMin/MaxCount must be >= 1");
}
if (e.producedMinCount > e.producedMaxCount) {
errors.push_back(ctx + ": producedMinCount " +
std::to_string(e.producedMinCount) +
" > producedMaxCount " +
std::to_string(e.producedMaxCount));
}
// Skill-up bracket thresholds must be monotonic:
// orange < yellow < green < gray.
if (!(e.orangeRank <= e.yellowRank &&
e.yellowRank <= e.greenRank &&
e.greenRank <= e.grayRank)) {
errors.push_back(ctx +
": skill brackets non-monotonic (require "
"orange <= yellow <= green <= gray, got " +
std::to_string(e.orangeRank) + "/" +
std::to_string(e.yellowRank) + "/" +
std::to_string(e.greenRank) + "/" +
std::to_string(e.grayRank) + ")");
}
if (e.skillId == 0)
warnings.push_back(ctx +
": skillId=0 (recipe not bound to a WSKL skill line)");
// A recipe with zero reagents and no tool is suspicious
// — most crafts need at least one of the two.
bool anyReagent = false;
for (size_t r = 0;
r < wowee::pipeline::WoweeTradeSkill::kMaxReagents; ++r) {
if (e.reagentItemId[r] != 0 && e.reagentCount[r] > 0) {
anyReagent = true; break;
}
if (e.reagentItemId[r] != 0 && e.reagentCount[r] == 0) {
errors.push_back(ctx + ": reagent slot " +
std::to_string(r) + " has itemId=" +
std::to_string(e.reagentItemId[r]) +
" but count=0 (set count or clear itemId)");
}
}
if (!anyReagent && e.toolItemId == 0) {
warnings.push_back(ctx +
": no reagents and no tool — recipe is free");
}
for (uint32_t prev : idsSeen) {
if (prev == e.recipeId) {
errors.push_back(ctx + ": duplicate recipeId");
break;
}
}
idsSeen.push_back(e.recipeId);
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wtsk"] = base + ".wtsk";
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-wtsk: %s.wtsk\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu recipes, all recipeIds unique, all skill brackets monotonic\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 handleTradeSkillsCatalog(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-tsk") == 0 && i + 1 < argc) {
outRc = handleGenStarter(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-tsk-blacksmithing") == 0 &&
i + 1 < argc) {
outRc = handleGenBlacksmithing(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-tsk-alchemy") == 0 && i + 1 < argc) {
outRc = handleGenAlchemy(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wtsk") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wtsk") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,11 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleTradeSkillsCatalog(int& i, int argc, char** argv, int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee