From 33a7b4b3cfc1f203716e4a471d602e4152c63295 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:41:49 -0700 Subject: [PATCH] feat(pipeline): add WTSK (Wowee Trade Skill / Recipe) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CMakeLists.txt | 3 + include/pipeline/wowee_trade_skills.hpp | 132 +++++++++ src/pipeline/wowee_trade_skills.cpp | 331 ++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_trade_skills_catalog.cpp | 293 +++++++++++++++++++ tools/editor/cli_trade_skills_catalog.hpp | 11 + 10 files changed, 786 insertions(+) create mode 100644 include/pipeline/wowee_trade_skills.hpp create mode 100644 src/pipeline/wowee_trade_skills.cpp create mode 100644 tools/editor/cli_trade_skills_catalog.cpp create mode 100644 tools/editor/cli_trade_skills_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e6f94d77..bc2afb05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/pipeline/wowee_trade_skills.hpp b/include/pipeline/wowee_trade_skills.hpp new file mode 100644 index 00000000..c468b2a1 --- /dev/null +++ b/include/pipeline/wowee_trade_skills.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include + +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 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 diff --git a/src/pipeline/wowee_trade_skills.cpp b/src/pipeline/wowee_trade_skills.cpp new file mode 100644 index 00000000..b7c4c492 --- /dev/null +++ b/src/pipeline/wowee_trade_skills.cpp @@ -0,0 +1,331 @@ +#include "pipeline/wowee_trade_skills.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'S', 'K'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".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(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(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(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(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(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 diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8f0eb2d0..088895c0 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -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", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 2299d225..29c8e0c8 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -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, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index e2e03772..93aa1a91 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -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"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 6d2842d7..566201c0 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1411,6 +1411,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wpcn to a human-editable JSON sidecar (defaults to .wpcn.json)\n"); std::printf(" --import-wpcn-json [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 [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 [name]\n"); + std::printf(" Emit .wtsk 5-recipe Blacksmithing progression (rough sharpening → truesilver champion plate)\n"); + std::printf(" --gen-tsk-alchemy [name]\n"); + std::printf(" Emit .wtsk 5-recipe Alchemy progression (minor healing → flask of titans) with reagent slots\n"); + std::printf(" --info-wtsk [--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 [--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 [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index e35f08b2..29139ad9 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -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 diff --git a/tools/editor/cli_trade_skills_catalog.cpp b/tools/editor/cli_trade_skills_catalog.cpp new file mode 100644 index 00000000..daee1f7a --- /dev/null +++ b/tools/editor/cli_trade_skills_catalog.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +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 errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.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 diff --git a/tools/editor/cli_trade_skills_catalog.hpp b/tools/editor/cli_trade_skills_catalog.hpp new file mode 100644 index 00000000..98541635 --- /dev/null +++ b/tools/editor/cli_trade_skills_catalog.hpp @@ -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