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