From b10bc2be5dba614ff83b18cbce8e83aba3c53e81 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 23:04:02 -0700 Subject: [PATCH] feat(editor): add WSCS (Skill Cost) open catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for Blizzard's SkillCostsData.dbc plus the per-rank training cost tables. Defines the tiered progression of trainable skills: each rank unlocks a skill range, requires a minimum character level, and costs a fixed amount of gold to learn. The canonical 6-tier profession progression captured by the default preset: Apprentice skill 0-75 lvl 5 1s Journeyman skill 50-150 lvl 10 5s Expert skill 125-225 lvl 20 1g Artisan skill 200-300 lvl 35 5g Master skill 275-375 lvl 50 10g Grand Master skill 350-450 lvl 65 25g Same shape applies to weapon skills (free, level-gated, capped at 5x char level) and riding skills (canonical Vanilla / TBC / WotLK gold costs from 90g Apprentice through 5000g Artisan flying down to 1000g Cold Weather Flying). Five costKind values cover the full training-skill space (Profession / WeaponSkill / RidingSkill / ClassSkill / Misc). Each entry's copperCost stores the cost in copper (1g = 10000c) which the info renderer pretty-prints as "25g 0s 0c". Cross-references back to WSKL — skill entries reference costId here for the tiered training schedule. nextTrainable(currentSkill, characterLevel) is the engine helper that returns the lowest-rank tier a character qualifies for and hasn't capped yet — used by trainer NPCs to populate their offered-skill list. Three preset emitters: --gen-scs (6 profession tiers), --gen-scs- weapon (5 weapon skill tiers), --gen-scs-riding (5 riding tiers with canonical gold costs). Validation enforces id+name presence, costKind 0..4, no duplicate ids, min 80 (unreachable at WotLK cap) - RidingSkill with requiredLevel < 20 (Apprentice canonically unlocks at 20) - Profession kind with copperCost=0 (every standard tier costs at least a copper — usually a config bug) Wired through the cross-format table; WSCS appears automatically in all 15 cross-format utilities. Format count 84 -> 85; CLI flag count 1010 -> 1015. --- CMakeLists.txt | 3 + include/pipeline/wowee_skill_costs.hpp | 120 ++++++++++ src/pipeline/wowee_skill_costs.cpp | 273 +++++++++++++++++++++++ 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_skill_costs_catalog.cpp | 265 ++++++++++++++++++++++ tools/editor/cli_skill_costs_catalog.hpp | 12 + 10 files changed, 689 insertions(+) create mode 100644 include/pipeline/wowee_skill_costs.hpp create mode 100644 src/pipeline/wowee_skill_costs.cpp create mode 100644 tools/editor/cli_skill_costs_catalog.cpp create mode 100644 tools/editor/cli_skill_costs_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 17c22a0d..f074e6ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -673,6 +673,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_spell_effect_types.cpp src/pipeline/wowee_spell_aura_types.cpp src/pipeline/wowee_item_qualities.cpp + src/pipeline/wowee_skill_costs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1506,6 +1507,7 @@ add_executable(wowee_editor tools/editor/cli_spell_effect_types_catalog.cpp tools/editor/cli_spell_aura_types_catalog.cpp tools/editor/cli_item_qualities_catalog.cpp + tools/editor/cli_skill_costs_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1657,6 +1659,7 @@ add_executable(wowee_editor src/pipeline/wowee_spell_effect_types.cpp src/pipeline/wowee_spell_aura_types.cpp src/pipeline/wowee_item_qualities.cpp + src/pipeline/wowee_skill_costs.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_skill_costs.hpp b/include/pipeline/wowee_skill_costs.hpp new file mode 100644 index 00000000..82f1ed31 --- /dev/null +++ b/include/pipeline/wowee_skill_costs.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Skill Cost catalog (.wscs) — novel +// replacement for Blizzard's SkillCostsData.dbc plus the +// per-rank training cost tables. Defines the tiered +// progression of trainable skills: each rank unlocks a +// skill range, requires a minimum character level, and +// costs a fixed amount of gold to learn. +// +// The canonical 6-tier profession progression: +// Apprentice skill 0-75 lvl 5 1s +// Journeyman skill 50-150 lvl 10 5s +// Expert skill 125-225 lvl 20 1g +// Artisan skill 200-300 lvl 35 5g +// Master skill 275-375 lvl 50 10g +// Grand Master skill 350-450 lvl 65 25g +// +// Same shape applies to weapon skills (with different +// caps), riding skills (with level gates per mount tier), +// and class secondary skills (Lockpicking for Rogues, +// First Aid for everyone). +// +// Cross-references with previously-added formats: +// WSKL: skill entries reference costId here for the +// tiered training schedule. +// +// Binary layout (little-endian): +// magic[4] = "WSCS" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// costId (uint32) +// nameLen + name +// descLen + description +// skillRankIndex (uint32) +// minSkillToLearn (uint16) / maxSkillUnlocked (uint16) +// requiredLevel (uint8) / costKind (uint8) / pad[2] +// copperCost (uint32) +// iconColorRGBA (uint32) +struct WoweeSkillCost { + enum CostKind : uint8_t { + Profession = 0, // primary/secondary profession rank + WeaponSkill = 1, // weapon skill cap (Sword / Mace / Axe / etc) + RidingSkill = 2, // mount riding skill (60% / 100% / 150% / 280% / Cold) + ClassSkill = 3, // class-specific (Lockpicking / Poisons) + Misc = 4, // catch-all + }; + + struct Entry { + uint32_t costId = 0; + std::string name; + std::string description; + uint32_t skillRankIndex = 0; + uint16_t minSkillToLearn = 0; + uint16_t maxSkillUnlocked = 75; + uint8_t requiredLevel = 1; + uint8_t costKind = Profession; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t copperCost = 0; // 1g = 10000c + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t costId) const; + + // Returns the entry that would be next-trainable for a + // character with the given current skill points and + // character level — i.e. the lowest-rank entry the + // character qualifies for and hasn't already maxed out. + // Returns nullptr if every entry is either capped or + // gated by level. + const Entry* nextTrainable(uint16_t currentSkill, + uint8_t characterLevel) const; + + static const char* costKindName(uint8_t k); +}; + +class WoweeSkillCostLoader { +public: + static bool save(const WoweeSkillCost& cat, + const std::string& basePath); + static WoweeSkillCost load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-scs* variants. + // + // makeProfession — 6 canonical profession tiers + // (Apprentice through Grand Master) + // with the standard skill ranges and + // gold costs from a Vanilla / TBC / + // WotLK-era server. + // makeWeapon — 5 weapon skill tiers (Beginner / + // Trained / Skilled / Expert / + // Master) for free-to-train weapon + // skills capped at 5x character lvl. + // makeRiding — 5 riding skill tiers (Apprentice + // 60% / Journeyman 100% / Expert + // 150% / Artisan 280% / Cold Weather + // Flying) with the canonical Vanilla + // /TBC / WotLK gold costs. + static WoweeSkillCost makeProfession(const std::string& catalogName); + static WoweeSkillCost makeWeapon(const std::string& catalogName); + static WoweeSkillCost makeRiding(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_skill_costs.cpp b/src/pipeline/wowee_skill_costs.cpp new file mode 100644 index 00000000..a3e2a22a --- /dev/null +++ b/src/pipeline/wowee_skill_costs.cpp @@ -0,0 +1,273 @@ +#include "pipeline/wowee_skill_costs.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'C', 'S'}; +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) != ".wscs") { + base += ".wscs"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeSkillCost::Entry* +WoweeSkillCost::findById(uint32_t costId) const { + for (const auto& e : entries) + if (e.costId == costId) return &e; + return nullptr; +} + +const WoweeSkillCost::Entry* +WoweeSkillCost::nextTrainable(uint16_t currentSkill, + uint8_t characterLevel) const { + const Entry* best = nullptr; + for (const auto& e : entries) { + if (characterLevel < e.requiredLevel) continue; + // Already maxed this tier; skip. + if (currentSkill >= e.maxSkillUnlocked) continue; + // Choose the lowest-rank tier the character is + // qualified for — typically the one whose + // minSkillToLearn matches their current skill. + if (best == nullptr || + e.skillRankIndex < best->skillRankIndex) { + best = &e; + } + } + return best; +} + +const char* WoweeSkillCost::costKindName(uint8_t k) { + switch (k) { + case Profession: return "profession"; + case WeaponSkill: return "weapon"; + case RidingSkill: return "riding"; + case ClassSkill: return "class-skill"; + case Misc: return "misc"; + default: return "unknown"; + } +} + +bool WoweeSkillCostLoader::save(const WoweeSkillCost& 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.costId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.skillRankIndex); + writePOD(os, e.minSkillToLearn); + writePOD(os, e.maxSkillUnlocked); + writePOD(os, e.requiredLevel); + writePOD(os, e.costKind); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.copperCost); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSkillCost WoweeSkillCostLoader::load( + const std::string& basePath) { + WoweeSkillCost 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.costId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.skillRankIndex) || + !readPOD(is, e.minSkillToLearn) || + !readPOD(is, e.maxSkillUnlocked) || + !readPOD(is, e.requiredLevel) || + !readPOD(is, e.costKind) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.copperCost) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSkillCostLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSkillCost WoweeSkillCostLoader::makeProfession( + const std::string& catalogName) { + using S = WoweeSkillCost; + WoweeSkillCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t rank, + uint16_t minSkill, uint16_t maxSkill, + uint8_t lvl, uint32_t cop, const char* desc) { + S::Entry e; + e.costId = id; e.name = name; e.description = desc; + e.skillRankIndex = rank; + e.minSkillToLearn = minSkill; + e.maxSkillUnlocked = maxSkill; + e.requiredLevel = lvl; + e.costKind = S::Profession; + e.copperCost = cop; + e.iconColorRGBA = packRgba(180, 140, 80); // crafting brown + c.entries.push_back(e); + }; + // The canonical 6-tier profession progression with + // standard gold costs (1g = 10000c). + add(1, "Apprentice", 0, 0, 75, 5, 100, + "Apprentice — entry tier; 1 silver, lvl 5+."); + add(2, "Journeyman", 1, 50, 150, 10, 500, + "Journeyman — basic tier; 5 silver, lvl 10+."); + add(3, "Expert", 2, 125, 225, 20, 10000, + "Expert — pre-Outland tier; 1 gold, lvl 20+."); + add(4, "Artisan", 3, 200, 300, 35, 50000, + "Artisan — Vanilla cap tier; 5 gold, lvl 35+."); + add(5, "Master", 4, 275, 375, 50, 100000, + "Master — TBC cap tier; 10 gold, lvl 50+."); + add(6, "GrandMaster", 5, 350, 450, 65, 250000, + "Grand Master — WotLK cap tier; 25 gold, lvl 65+."); + return c; +} + +WoweeSkillCost WoweeSkillCostLoader::makeWeapon( + const std::string& catalogName) { + using S = WoweeSkillCost; + WoweeSkillCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t rank, + uint16_t minSkill, uint16_t maxSkill, + uint8_t lvl, const char* desc) { + S::Entry e; + e.costId = id; e.name = name; e.description = desc; + e.skillRankIndex = rank; + e.minSkillToLearn = minSkill; + e.maxSkillUnlocked = maxSkill; + e.requiredLevel = lvl; + e.costKind = S::WeaponSkill; + e.copperCost = 0; // weapon skills are free + e.iconColorRGBA = packRgba(220, 180, 100); // weapon yellow + c.entries.push_back(e); + }; + // Weapon skills cap at 5x char level. Free to train + // but level-gated. + add(100, "WeaponBeginner", 0, 0, 100, 5, + "Beginner weapon skill — caps at 100 (lvl 5+)."); + add(101, "WeaponTrained", 1, 100, 200, 20, + "Trained weapon skill — caps at 200 (lvl 20+)."); + add(102, "WeaponSkilled", 2, 200, 300, 40, + "Skilled weapon skill — caps at 300 (lvl 40+)."); + add(103, "WeaponExpert", 3, 300, 400, 60, + "Expert weapon skill — caps at 400 (lvl 60+)."); + add(104, "WeaponMaster", 4, 400, 500, 80, + "Master weapon skill — caps at 500 (lvl 80+)."); + return c; +} + +WoweeSkillCost WoweeSkillCostLoader::makeRiding( + const std::string& catalogName) { + using S = WoweeSkillCost; + WoweeSkillCost c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t rank, + uint16_t minSkill, uint16_t maxSkill, + uint8_t lvl, uint32_t cop, const char* desc) { + S::Entry e; + e.costId = id; e.name = name; e.description = desc; + e.skillRankIndex = rank; + e.minSkillToLearn = minSkill; + e.maxSkillUnlocked = maxSkill; + e.requiredLevel = lvl; + e.costKind = S::RidingSkill; + e.copperCost = cop; + e.iconColorRGBA = packRgba(180, 100, 220); // riding purple + c.entries.push_back(e); + }; + // Canonical Vanilla / TBC / WotLK riding gold costs. + add(200, "Apprentice60", 0, 0, 75, 20, 900000, + "Apprentice Riding — 60% land mount; 90g, lvl 20+."); + add(201, "Journeyman100", 1, 75, 150, 40, 5000000, + "Journeyman Riding — 100% land mount; 500g, lvl 40+."); + add(202, "Expert150", 2, 150, 225, 60, 8000000, + "Expert Riding — 150% flying; 800g, lvl 60+ (TBC)."); + add(203, "Artisan280", 3, 225, 300, 70, 50000000, + "Artisan Riding — 280% flying; 5000g, lvl 70+ (TBC epic)."); + add(204, "ColdWeather", 4, 300, 375, 77, 1000000, + "Cold Weather Flying — required for Northrend; " + "1000g, lvl 77+ (WotLK)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index c1f8edea..18c8b126 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -260,6 +260,8 @@ const char* const kArgRequired[] = { "--gen-iqr", "--gen-iqr-server", "--gen-iqr-raid", "--info-wiqr", "--validate-wiqr", "--export-wiqr-json", "--import-wiqr-json", + "--gen-scs", "--gen-scs-weapon", "--gen-scs-riding", + "--info-wscs", "--validate-wscs", "--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 fa0a2b17..3796985e 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -126,6 +126,7 @@ #include "cli_spell_effect_types_catalog.hpp" #include "cli_spell_aura_types_catalog.hpp" #include "cli_item_qualities_catalog.hpp" +#include "cli_skill_costs_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -293,6 +294,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellEffectTypesCatalog, handleSpellAuraTypesCatalog, handleItemQualitiesCatalog, + handleSkillCostsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 06b6c3a3..0144d413 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -87,6 +87,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','E','F'}, ".wsef", "spells", "--info-wsef", "Spell effect type catalog"}, {{'W','A','U','R'}, ".waur", "spells", "--info-waur", "Spell aura type catalog"}, {{'W','I','Q','R'}, ".wiqr", "items", "--info-wiqr", "Item quality tier catalog"}, + {{'W','S','C','S'}, ".wscs", "skills", "--info-wscs", "Skill cost / training tier 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 8dea4b41..f30b8495 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1937,6 +1937,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wiqr to a human-editable JSON sidecar (defaults to .wiqr.json). Colors as RGBA uint32, easy to paste hex values\n"); std::printf(" --import-wiqr-json [out-base]\n"); std::printf(" Import a .wiqr.json sidecar back into binary .wiqr (canBeDisenchanted accepts bool OR int)\n"); + std::printf(" --gen-scs [name]\n"); + std::printf(" Emit .wscs 6 canonical profession tiers (Apprentice/Journeyman/Expert/Artisan/Master/GrandMaster) with standard skill ranges and gold costs\n"); + std::printf(" --gen-scs-weapon [name]\n"); + std::printf(" Emit .wscs 5 weapon skill tiers (Beginner/Trained/Skilled/Expert/Master) — free to train, level-gated, capped at 5x char level\n"); + std::printf(" --gen-scs-riding [name]\n"); + std::printf(" Emit .wscs 5 riding skill tiers (Apprentice 60%% / Journeyman 100%% / Expert 150%% / Artisan 280%% / Cold Weather Flying) with canonical Vanilla/TBC/WotLK gold costs\n"); + std::printf(" --info-wscs [--json]\n"); + std::printf(" Print WSCS entries (id / rank / kind / minSkill / maxSkill / required level / gold cost / name)\n"); + std::printf(" --validate-wscs [--json]\n"); + std::printf(" Static checks: id+name required, costKind 0..4, no duplicate ids, min80, RidingSkill below lvl 20, Profession with cost=0\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 5e7fd931..7fde6e77 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -109,6 +109,7 @@ constexpr FormatRow kFormats[] = { {"WSEF", ".wsef", "spells", "SpellEffect.Effect dispatch", "Spell effect type catalog"}, {"WAUR", ".waur", "spells", "SpellEffect.EffectAuraType", "Spell aura type catalog"}, {"WIQR", ".wiqr", "items", "Item quality tier colors+rules", "Item quality tier catalog"}, + {"WSCS", ".wscs", "skills", "SkillCostsData.dbc + train tiers", "Skill cost / training tier catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_skill_costs_catalog.cpp b/tools/editor/cli_skill_costs_catalog.cpp new file mode 100644 index 00000000..4f633c2d --- /dev/null +++ b/tools/editor/cli_skill_costs_catalog.cpp @@ -0,0 +1,265 @@ +#include "cli_skill_costs_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_skill_costs.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWscsExt(std::string base) { + stripExt(base, ".wscs"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSkillCost& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSkillCostLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wscs\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSkillCost& c, + const std::string& base) { + std::printf("Wrote %s.wscs\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tiers : %zu\n", c.entries.size()); +} + +int handleGenProfession(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ProfessionSkillCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWscsExt(base); + auto c = wowee::pipeline::WoweeSkillCostLoader::makeProfession(name); + if (!saveOrError(c, base, "gen-scs")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWeapon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeaponSkillCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWscsExt(base); + auto c = wowee::pipeline::WoweeSkillCostLoader::makeWeapon(name); + if (!saveOrError(c, base, "gen-scs-weapon")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRiding(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RidingSkillCosts"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWscsExt(base); + auto c = wowee::pipeline::WoweeSkillCostLoader::makeRiding(name); + if (!saveOrError(c, base, "gen-scs-riding")) return 1; + printGenSummary(c, base); + return 0; +} + +void formatGold(uint32_t copper, char* buf, size_t bufSize) { + uint32_t g = copper / 10000; + uint32_t s = (copper % 10000) / 100; + uint32_t cop = copper % 100; + if (g > 0) { + std::snprintf(buf, bufSize, "%ug %us %uc", g, s, cop); + } else if (s > 0) { + std::snprintf(buf, bufSize, "%us %uc", s, cop); + } else { + std::snprintf(buf, bufSize, "%uc", cop); + } +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWscsExt(base); + if (!wowee::pipeline::WoweeSkillCostLoader::exists(base)) { + std::fprintf(stderr, "WSCS not found: %s.wscs\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSkillCostLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wscs"] = base + ".wscs"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"costId", e.costId}, + {"name", e.name}, + {"description", e.description}, + {"skillRankIndex", e.skillRankIndex}, + {"minSkillToLearn", e.minSkillToLearn}, + {"maxSkillUnlocked", e.maxSkillUnlocked}, + {"requiredLevel", e.requiredLevel}, + {"costKind", e.costKind}, + {"costKindName", wowee::pipeline::WoweeSkillCost::costKindName(e.costKind)}, + {"copperCost", e.copperCost}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSCS: %s.wscs\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" tiers : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id rank kind minSkill maxSkill lvl cost name\n"); + for (const auto& e : c.entries) { + char goldBuf[32]; + formatGold(e.copperCost, goldBuf, sizeof(goldBuf)); + std::printf(" %4u %2u %-11s %5u %5u %3u %-13s %s\n", + e.costId, e.skillRankIndex, + wowee::pipeline::WoweeSkillCost::costKindName(e.costKind), + e.minSkillToLearn, e.maxSkillUnlocked, + e.requiredLevel, goldBuf, + 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 = stripWscsExt(base); + if (!wowee::pipeline::WoweeSkillCostLoader::exists(base)) { + std::fprintf(stderr, + "validate-wscs: WSCS not found: %s.wscs\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSkillCostLoader::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.costId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.costId == 0) + errors.push_back(ctx + ": costId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.costKind > wowee::pipeline::WoweeSkillCost::Misc) { + errors.push_back(ctx + ": costKind " + + std::to_string(e.costKind) + " not in 0..4"); + } + if (e.minSkillToLearn >= e.maxSkillUnlocked) { + errors.push_back(ctx + + ": minSkillToLearn " + + std::to_string(e.minSkillToLearn) + + " >= maxSkillUnlocked " + + std::to_string(e.maxSkillUnlocked) + + " — tier provides no skill range"); + } + if (e.requiredLevel > 80) { + warnings.push_back(ctx + + ": requiredLevel " + + std::to_string(e.requiredLevel) + + " > 80 — tier unreachable at WotLK cap"); + } + // Riding skill at lvl < 20 is unusual (Apprentice + // requires lvl 20). + if (e.costKind == wowee::pipeline::WoweeSkillCost::RidingSkill && + e.requiredLevel < 20) { + warnings.push_back(ctx + + ": Riding skill with requiredLevel=" + + std::to_string(e.requiredLevel) + + " < 20 — canonical Apprentice Riding unlocks " + "at level 20"); + } + // Profession with cost=0 is unusual — every standard + // profession tier costs at least a copper. + if (e.costKind == wowee::pipeline::WoweeSkillCost::Profession && + e.copperCost == 0) { + warnings.push_back(ctx + + ": Profession kind with copperCost=0 — " + "unusual, profession tiers normally cost " + "at least a copper"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.costId) { + errors.push_back(ctx + ": duplicate costId"); + break; + } + } + idsSeen.push_back(e.costId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wscs"] = base + ".wscs"; + 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-wscs: %s.wscs\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu tiers, all costIds unique\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 handleSkillCostsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-scs") == 0 && i + 1 < argc) { + outRc = handleGenProfession(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-scs-weapon") == 0 && i + 1 < argc) { + outRc = handleGenWeapon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-scs-riding") == 0 && i + 1 < argc) { + outRc = handleGenRiding(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wscs") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wscs") == 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_skill_costs_catalog.hpp b/tools/editor/cli_skill_costs_catalog.hpp new file mode 100644 index 00000000..3c29491b --- /dev/null +++ b/tools/editor/cli_skill_costs_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSkillCostsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee