mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
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:
parent
6b05136ef1
commit
33a7b4b3cf
10 changed files with 786 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
132
include/pipeline/wowee_trade_skills.hpp
Normal file
132
include/pipeline/wowee_trade_skills.hpp
Normal 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
|
||||
331
src/pipeline/wowee_trade_skills.cpp
Normal file
331
src/pipeline/wowee_trade_skills.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
293
tools/editor/cli_trade_skills_catalog.cpp
Normal file
293
tools/editor/cli_trade_skills_catalog.cpp
Normal 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
|
||||
11
tools/editor/cli_trade_skills_catalog.hpp
Normal file
11
tools/editor/cli_trade_skills_catalog.hpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue