From c1c4b8fa12019262e58b7d32d1b32f881e3528ce Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 23:26:13 -0700 Subject: [PATCH] feat(editor): add WTBR (Token Reward) open catalog format Open replacement for AzerothCore's currency_token_reward SQL table plus the per-vendor token redemption rows in npc_vendor. Each entry says "spend N copies of token X to receive reward Y", with reward type polymorphism: Y can be an item, a spell (taught to the character), a title, a mount, a companion pet, a currency conversion, an heirloom unlock, or a cosmetic (tabard / pennant / fluff). The rewardId field's interpretation depends on the rewardKind enum. Distinct from WTKN (Token catalog) which defines the token currency items themselves. WTKN says "the Champion's Seal exists as item 44990"; WTBR says "spend 25 Champion's Seals at Argent Tournament for the Squire's Belt (item 45517)". Eight rewardKind values cover the full reward space (Item / Spell / Title / Mount / Pet / Currency / Heirloom / Cosmetic), and an 8-tier requiredFactionStanding gates by reputation (Hated / Hostile / Unfriendly / Neutral / Friendly / Honored / Revered / Exalted) when paired with a non-zero requiredFactionId. Cross-references back to WTKN (spentTokenItemId), WIT (Item rewards), WSPL (Spell rewards), WTIT (Title rewards), WMOU (Mount rewards), WCMP (Pet rewards), WCTR (Currency conversion rewards), and WFAC (faction-rep gating). findByToken(itemId) is the engine helper used by vendor frames to populate the "what can I buy with these?" list. Three preset emitters: --gen-tbr (5 raid tier-token redemptions consuming Trophy of the Crusade and Emblem of Frost), --gen-tbr-pvp (5 PvP rewards spanning honor / arena / conquest plus title and tabard kinds), --gen-tbr-faction (5 faction- gated rewards demonstrating each standing tier from Honored through Exalted). Validation enforces id+name+spentTokenItemId+spentTokenCount presence, rewardKind 0..7, requiredFactionStanding 0..7, no duplicate ids; warns on: - rewardId=0 (no actual reward, vendor offers entry but grants nothing) - requiredFactionStanding > Neutral with requiredFactionId=0 (rep gate has no faction to check) - Currency conversion item -> itself (typo / config bug) Wired through the cross-format table; WTBR appears automatically in all 18 cross-format utilities. Format count 87 -> 88; CLI flag count 1034 -> 1039. --- CMakeLists.txt | 3 + include/pipeline/wowee_token_rewards.hpp | 135 ++++++++++ src/pipeline/wowee_token_rewards.cpp | 285 +++++++++++++++++++++ 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_token_rewards_catalog.cpp | 260 +++++++++++++++++++ tools/editor/cli_token_rewards_catalog.hpp | 12 + 10 files changed, 711 insertions(+) create mode 100644 include/pipeline/wowee_token_rewards.hpp create mode 100644 src/pipeline/wowee_token_rewards.cpp create mode 100644 tools/editor/cli_token_rewards_catalog.cpp create mode 100644 tools/editor/cli_token_rewards_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1530fc34..2bbd76af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -676,6 +676,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_skill_costs.cpp src/pipeline/wowee_item_flags.cpp src/pipeline/wowee_npc_services.cpp + src/pipeline/wowee_token_rewards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1515,6 +1516,7 @@ add_executable(wowee_editor tools/editor/cli_skill_costs_catalog.cpp tools/editor/cli_item_flags_catalog.cpp tools/editor/cli_npc_services_catalog.cpp + tools/editor/cli_token_rewards_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1669,6 +1671,7 @@ add_executable(wowee_editor src/pipeline/wowee_skill_costs.cpp src/pipeline/wowee_item_flags.cpp src/pipeline/wowee_npc_services.cpp + src/pipeline/wowee_token_rewards.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_token_rewards.hpp b/include/pipeline/wowee_token_rewards.hpp new file mode 100644 index 00000000..f6d90489 --- /dev/null +++ b/include/pipeline/wowee_token_rewards.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Token Reward catalog (.wtbr) — novel +// replacement for the per-vendor token redemption tables +// (currency_token_reward / npc_vendor token rows). Each +// entry says "spend N copies of token X to receive +// reward Y", with reward type polymorphism: Y can be an +// item, a spell (taught to the character), a title, a +// mount, a companion pet, a currency conversion, or an +// heirloom unlock. The rewardId field's interpretation +// depends on rewardKind. +// +// Distinct from WTKN (Token catalog) which defines the +// token currency items themselves. WTKN says "the +// Champion's Seal exists as item 44990"; WTBR says +// "spend 25 Champion's Seals at Argent Tournament for +// the Squire's Belt (item 45517)". +// +// Cross-references with previously-added formats: +// WTKN: spentTokenItemId references the token currency. +// WIT: rewardId references WIT.itemId for Item kind. +// WSPL: rewardId references WSPL.spellId for Spell kind. +// WTIT: rewardId references WTIT.titleId for Title kind. +// WMOU: rewardId references WMOU.mountId for Mount kind. +// WCMP: rewardId references WCMP.companionId for Pet kind. +// WCTR: rewardId references WCTR.currencyId for Currency +// conversion kind (e.g. 1 Trophy -> 50 Justice). +// WFAC: requiredFactionId references WFAC.factionId for +// reputation-gated rewards. +// +// Binary layout (little-endian): +// magic[4] = "WTBR" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// tokenRewardId (uint32) +// nameLen + name +// descLen + description +// spentTokenItemId (uint32) +// spentTokenCount (uint32) +// rewardKind (uint8) / requiredFactionStanding (uint8) / pad[2] +// rewardId (uint32) +// rewardCount (uint32) +// requiredFactionId (uint32) +// iconColorRGBA (uint32) +struct WoweeTokenReward { + enum RewardKind : uint8_t { + Item = 0, // grants WIT item + Spell = 1, // teaches WSPL spell to character + Title = 2, // grants WTIT title + Mount = 3, // unlocks WMOU mount + Pet = 4, // grants WCMP companion pet + Currency = 5, // converts to WCTR currency + Heirloom = 6, // unlocks heirloom from WIT + Cosmetic = 7, // tabard / pennant / fluff + }; + + enum FactionStanding : uint8_t { + Hated = 0, + Hostile = 1, + Unfriendly = 2, + Neutral = 3, + Friendly = 4, + Honored = 5, + Revered = 6, + Exalted = 7, + }; + + struct Entry { + uint32_t tokenRewardId = 0; + std::string name; + std::string description; + uint32_t spentTokenItemId = 0; + uint32_t spentTokenCount = 1; + uint8_t rewardKind = Item; + uint8_t requiredFactionStanding = Neutral; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t rewardId = 0; + uint32_t rewardCount = 1; + uint32_t requiredFactionId = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t tokenRewardId) const; + + // Returns all rewards offered for spending the given + // token item id. Used by vendor frames to populate + // the "what can I buy with these?" list. + std::vector findByToken(uint32_t tokenItemId) const; + + static const char* rewardKindName(uint8_t k); + static const char* factionStandingName(uint8_t s); +}; + +class WoweeTokenRewardLoader { +public: + static bool save(const WoweeTokenReward& cat, + const std::string& basePath); + static WoweeTokenReward load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-tbr* variants. + // + // makeRaidTokens — 5 raid tier-token redemptions + // (T9 Conqueror's helm, Vanquisher's + // chest, etc) consuming Trophy of + // the Crusade. + // makePvP — 5 PvP redemptions (Honor → BG + // mount, Arena → arena weapon, + // Conquest → PvP tier helm). + // makeFaction — 5 faction-gated rewards (Argent + // Tournament tabard at Honored, + // Hodir mammoth mount at Exalted, + // Cenarion ring at Revered, etc.) + static WoweeTokenReward makeRaidTokens(const std::string& catalogName); + static WoweeTokenReward makePvP(const std::string& catalogName); + static WoweeTokenReward makeFaction(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_token_rewards.cpp b/src/pipeline/wowee_token_rewards.cpp new file mode 100644 index 00000000..6830f3b0 --- /dev/null +++ b/src/pipeline/wowee_token_rewards.cpp @@ -0,0 +1,285 @@ +#include "pipeline/wowee_token_rewards.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'T', 'B', 'R'}; +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) != ".wtbr") { + base += ".wtbr"; + } + 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 WoweeTokenReward::Entry* +WoweeTokenReward::findById(uint32_t tokenRewardId) const { + for (const auto& e : entries) + if (e.tokenRewardId == tokenRewardId) return &e; + return nullptr; +} + +std::vector +WoweeTokenReward::findByToken(uint32_t tokenItemId) const { + std::vector out; + for (const auto& e : entries) { + if (e.spentTokenItemId == tokenItemId) out.push_back(&e); + } + return out; +} + +const char* WoweeTokenReward::rewardKindName(uint8_t k) { + switch (k) { + case Item: return "item"; + case Spell: return "spell"; + case Title: return "title"; + case Mount: return "mount"; + case Pet: return "pet"; + case Currency: return "currency"; + case Heirloom: return "heirloom"; + case Cosmetic: return "cosmetic"; + default: return "unknown"; + } +} + +const char* WoweeTokenReward::factionStandingName(uint8_t s) { + switch (s) { + case Hated: return "hated"; + case Hostile: return "hostile"; + case Unfriendly: return "unfriendly"; + case Neutral: return "neutral"; + case Friendly: return "friendly"; + case Honored: return "honored"; + case Revered: return "revered"; + case Exalted: return "exalted"; + default: return "unknown"; + } +} + +bool WoweeTokenRewardLoader::save(const WoweeTokenReward& 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.tokenRewardId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.spentTokenItemId); + writePOD(os, e.spentTokenCount); + writePOD(os, e.rewardKind); + writePOD(os, e.requiredFactionStanding); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.rewardId); + writePOD(os, e.rewardCount); + writePOD(os, e.requiredFactionId); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeTokenReward WoweeTokenRewardLoader::load( + const std::string& basePath) { + WoweeTokenReward 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.tokenRewardId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.spentTokenItemId) || + !readPOD(is, e.spentTokenCount) || + !readPOD(is, e.rewardKind) || + !readPOD(is, e.requiredFactionStanding) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.rewardId) || + !readPOD(is, e.rewardCount) || + !readPOD(is, e.requiredFactionId) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeTokenRewardLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeTokenReward WoweeTokenRewardLoader::makeRaidTokens( + const std::string& catalogName) { + using T = WoweeTokenReward; + WoweeTokenReward c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t tokenItem, + uint32_t tokenCount, uint32_t rewardItem, + const char* desc) { + T::Entry e; + e.tokenRewardId = id; e.name = name; e.description = desc; + e.spentTokenItemId = tokenItem; + e.spentTokenCount = tokenCount; + e.rewardKind = T::Item; + e.rewardId = rewardItem; + e.rewardCount = 1; + e.iconColorRGBA = packRgba(180, 100, 240); // raid epic purple + c.entries.push_back(e); + }; + // Item 47241 = Trophy of the Crusade (T9 redeem token). + // Item 49426 = Emblem of Frost (T10 redeem token). + // Item ids on the right are illustrative tier-set helms. + add(1, "T9_Conqueror_Helm", 47241, 1, 47242, + "T9 Conqueror's Helm — 1 Trophy of the Crusade."); + add(2, "T9_Vanquisher_Chest",47241, 1, 47243, + "T9 Vanquisher's Chest — 1 Trophy of the Crusade."); + add(3, "T10_Protector_Legs", 49426, 95, 50688, + "T10 Protector's Greaves — 95 Emblem of Frost."); + add(4, "T10_Trophy_Gloves", 49426, 60, 50689, + "T10 Trophy Gauntlets — 60 Emblem of Frost."); + add(5, "T10_Helm_Crusader", 49426, 95, 50690, + "T10 Crusader's Helm — 95 Emblem of Frost."); + return c; +} + +WoweeTokenReward WoweeTokenRewardLoader::makePvP( + const std::string& catalogName) { + using T = WoweeTokenReward; + WoweeTokenReward c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t tokenItem, + uint32_t tokenCount, uint8_t rewardKind, + uint32_t rewardId, const char* desc) { + T::Entry e; + e.tokenRewardId = id; e.name = name; e.description = desc; + e.spentTokenItemId = tokenItem; + e.spentTokenCount = tokenCount; + e.rewardKind = rewardKind; + e.rewardId = rewardId; + e.rewardCount = 1; + e.iconColorRGBA = packRgba(220, 80, 100); // pvp red + c.entries.push_back(e); + }; + // Honor Points = 43308; Arena Points = 29024 (item form). + add(100, "PvPMount_Stallion", 43308, 50000, T::Mount, + 4806, "PvP Stallion — 50000 honor points."); + add(101, "ArenaWeapon_Sword", 29024, 3650, T::Item, + 51811, "Arena Season 8 Sword — 3650 Arena Points."); + add(102, "PvPHelm_Wrathful", 29024, 1650, T::Item, + 51812, "Wrathful Gladiator's Helm — 1650 Arena Points."); + add(103, "ConquestTitle_Combatant",29024, 50, T::Title, + 38, "Combatant title — 50 Arena Points."); + add(104, "PvPTabard", 43308, 3000, T::Cosmetic, + 45984, "PvP Tabard — 3000 honor points."); + return c; +} + +WoweeTokenReward WoweeTokenRewardLoader::makeFaction( + const std::string& catalogName) { + using T = WoweeTokenReward; + WoweeTokenReward c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t tokenItem, + uint32_t tokenCount, uint8_t rewardKind, + uint32_t rewardId, uint32_t factionId, + uint8_t standing, const char* desc) { + T::Entry e; + e.tokenRewardId = id; e.name = name; e.description = desc; + e.spentTokenItemId = tokenItem; + e.spentTokenCount = tokenCount; + e.rewardKind = rewardKind; + e.rewardId = rewardId; + e.rewardCount = 1; + e.requiredFactionId = factionId; + e.requiredFactionStanding = standing; + e.iconColorRGBA = packRgba(100, 200, 100); // faction green + c.entries.push_back(e); + }; + // Token items: Champion's Seal (44990, Argent Tournament), + // Spear-fragment of Hodir (41511, Sons of Hodir). + add(200, "ArgentTabard", 44990, 25, T::Cosmetic, 45984, 1106, T::Honored, + "Argent Tournament tabard — 25 Champion's Seals " + "@ Honored with Argent Crusade."); + add(201, "HodirMammoth", 41511,200, T::Mount, 44171, 1119, T::Exalted, + "Sons of Hodir Mammoth — 200 Spear-fragments " + "@ Exalted with Sons of Hodir."); + add(202, "CenarionRing", 20809,150, T::Item, 19438, 609, T::Revered, + "Cenarion Ring of Casting — 150 Marks of Cenarion " + "@ Revered with Cenarion Circle."); + add(203, "ArgentTitle", 44990,250, T::Title, 149, 1106, T::Exalted, + "Crusader title — 250 Champion's Seals @ Exalted " + "with Argent Crusade."); + add(204, "WintergraspPet",43589, 30, T::Pet, 45890, 1156, T::Honored, + "Wintergrasp commemorative pet — 30 Wintergrasp " + "Marks @ Honored with The Wintergrasp Defenders."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index b7242ebc..87339b9e 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -270,6 +270,8 @@ const char* const kArgRequired[] = { "--gen-bkd", "--gen-bkd-battle", "--gen-bkd-profession", "--info-wbkd", "--validate-wbkd", "--export-wbkd-json", "--import-wbkd-json", + "--gen-tbr", "--gen-tbr-pvp", "--gen-tbr-faction", + "--info-wtbr", "--validate-wtbr", "--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 eb562e34..816c4bfb 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -132,6 +132,7 @@ #include "cli_skill_costs_catalog.hpp" #include "cli_item_flags_catalog.hpp" #include "cli_npc_services_catalog.hpp" +#include "cli_token_rewards_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -305,6 +306,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSkillCostsCatalog, handleItemFlagsCatalog, handleNPCServicesCatalog, + handleTokenRewardsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 9d060ba9..41714389 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -90,6 +90,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','C','S'}, ".wscs", "skills", "--info-wscs", "Skill cost / training tier catalog"}, {{'W','I','F','S'}, ".wifs", "items", "--info-wifs", "Item flag bit catalog"}, {{'W','B','K','D'}, ".wbkd", "npcs", "--info-wbkd", "NPC service definition catalog"}, + {{'W','T','B','R'}, ".wtbr", "tokens", "--info-wtbr", "Token reward redemption 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 5ae819b2..ac9fce45 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1985,6 +1985,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wbkd to a human-editable JSON sidecar (defaults to .wbkd.json)\n"); std::printf(" --import-wbkd-json [out-base]\n"); std::printf(" Import a .wbkd.json sidecar back into binary .wbkd (accepts serviceKind int OR serviceKindName string)\n"); + std::printf(" --gen-tbr [name]\n"); + std::printf(" Emit .wtbr 5 raid tier-token redemptions (T9 Conqueror's helm / Vanquisher's chest / T10 Protector's legs / Trophy Gloves) consuming Trophy of the Crusade and Emblem of Frost\n"); + std::printf(" --gen-tbr-pvp [name]\n"); + std::printf(" Emit .wtbr 5 PvP token redemptions (BG mount / Arena weapon / Wrathful PvP helm / Combatant title / PvP tabard)\n"); + std::printf(" --gen-tbr-faction [name]\n"); + std::printf(" Emit .wtbr 5 faction-rep-gated rewards (Argent tabard @ Honored / Hodir mammoth @ Exalted / Cenarion ring @ Revered / Argent title @ Exalted / Wintergrasp pet @ Honored)\n"); + std::printf(" --info-wtbr [--json]\n"); + std::printf(" Print WTBR entries (id / spent token x count / reward kind / rewardId / faction@standing gate / name)\n"); + std::printf(" --validate-wtbr [--json]\n"); + std::printf(" Static checks: id+name+spentTokenItemId+count required, rewardKind 0..7, requiredFactionStanding 0..7, no duplicate ids; warns on rewardId=0 (no actual reward), standing>Neutral with factionId=0, Currency conversion item->itself\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 59411b55..65042cf4 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -112,6 +112,7 @@ constexpr FormatRow kFormats[] = { {"WSCS", ".wscs", "skills", "SkillCostsData.dbc + train tiers", "Skill cost / training tier catalog"}, {"WIFS", ".wifs", "items", "Item.dbc Flags bit meanings", "Item flag bit catalog"}, {"WBKD", ".wbkd", "npcs", "npc_vendor + npc_trainer SQL", "NPC service definition catalog"}, + {"WTBR", ".wtbr", "tokens", "currency_token_reward SQL", "Token reward redemption catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_token_rewards_catalog.cpp b/tools/editor/cli_token_rewards_catalog.cpp new file mode 100644 index 00000000..f696399f --- /dev/null +++ b/tools/editor/cli_token_rewards_catalog.cpp @@ -0,0 +1,260 @@ +#include "cli_token_rewards_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_token_rewards.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWtbrExt(std::string base) { + stripExt(base, ".wtbr"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeTokenReward& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeTokenRewardLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wtbr\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeTokenReward& c, + const std::string& base) { + std::printf("Wrote %s.wtbr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" rewards : %zu\n", c.entries.size()); +} + +int handleGenRaid(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RaidTokenRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbrExt(base); + auto c = wowee::pipeline::WoweeTokenRewardLoader::makeRaidTokens(name); + if (!saveOrError(c, base, "gen-tbr")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenPvP(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "PvPTokenRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbrExt(base); + auto c = wowee::pipeline::WoweeTokenRewardLoader::makePvP(name); + if (!saveOrError(c, base, "gen-tbr-pvp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenFaction(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FactionTokenRewards"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWtbrExt(base); + auto c = wowee::pipeline::WoweeTokenRewardLoader::makeFaction(name); + if (!saveOrError(c, base, "gen-tbr-faction")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWtbrExt(base); + if (!wowee::pipeline::WoweeTokenRewardLoader::exists(base)) { + std::fprintf(stderr, "WTBR not found: %s.wtbr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTokenRewardLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wtbr"] = base + ".wtbr"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"tokenRewardId", e.tokenRewardId}, + {"name", e.name}, + {"description", e.description}, + {"spentTokenItemId", e.spentTokenItemId}, + {"spentTokenCount", e.spentTokenCount}, + {"rewardKind", e.rewardKind}, + {"rewardKindName", wowee::pipeline::WoweeTokenReward::rewardKindName(e.rewardKind)}, + {"rewardId", e.rewardId}, + {"rewardCount", e.rewardCount}, + {"requiredFactionId", e.requiredFactionId}, + {"requiredFactionStanding", e.requiredFactionStanding}, + {"requiredFactionStandingName", wowee::pipeline::WoweeTokenReward::factionStandingName(e.requiredFactionStanding)}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WTBR: %s.wtbr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" rewards : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id spent(item x count) kind rewardId faction@standing name\n"); + for (const auto& e : c.entries) { + char gateBuf[64] = "-"; + if (e.requiredFactionId != 0) { + std::snprintf(gateBuf, sizeof(gateBuf), "%u@%s", + e.requiredFactionId, + wowee::pipeline::WoweeTokenReward::factionStandingName(e.requiredFactionStanding)); + } + std::printf(" %4u %5u x %5u %-9s %5u %-20s %s\n", + e.tokenRewardId, + e.spentTokenItemId, e.spentTokenCount, + wowee::pipeline::WoweeTokenReward::rewardKindName(e.rewardKind), + e.rewardId, gateBuf, + 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 = stripWtbrExt(base); + if (!wowee::pipeline::WoweeTokenRewardLoader::exists(base)) { + std::fprintf(stderr, + "validate-wtbr: WTBR not found: %s.wtbr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeTokenRewardLoader::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.tokenRewardId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.tokenRewardId == 0) + errors.push_back(ctx + ": tokenRewardId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.spentTokenItemId == 0) + errors.push_back(ctx + + ": spentTokenItemId is 0 — missing token currency"); + if (e.spentTokenCount == 0) + errors.push_back(ctx + + ": spentTokenCount is 0 — would grant reward for free"); + if (e.rewardKind > wowee::pipeline::WoweeTokenReward::Cosmetic) { + errors.push_back(ctx + ": rewardKind " + + std::to_string(e.rewardKind) + " not in 0..7"); + } + if (e.requiredFactionStanding > wowee::pipeline::WoweeTokenReward::Exalted) { + errors.push_back(ctx + ": requiredFactionStanding " + + std::to_string(e.requiredFactionStanding) + + " not in 0..7"); + } + if (e.rewardId == 0) + warnings.push_back(ctx + + ": rewardId is 0 — no actual reward target, " + "vendor will offer the entry but grant nothing"); + // requiredFactionStanding > Neutral with no + // requiredFactionId is contradictory — the gate + // can't apply. + if (e.requiredFactionStanding > wowee::pipeline::WoweeTokenReward::Neutral && + e.requiredFactionId == 0) { + warnings.push_back(ctx + + ": requiredFactionStanding=" + + wowee::pipeline::WoweeTokenReward::factionStandingName(e.requiredFactionStanding) + + " set but requiredFactionId=0 — rep gate " + "has no faction to check, gate will be ignored"); + } + // Currency conversion to same item is suspicious + // (1 X -> N X is usually a config bug). + if (e.rewardKind == wowee::pipeline::WoweeTokenReward::Currency && + e.rewardId == e.spentTokenItemId) { + warnings.push_back(ctx + + ": Currency conversion from item " + + std::to_string(e.spentTokenItemId) + + " to itself — usually a typo"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.tokenRewardId) { + errors.push_back(ctx + ": duplicate tokenRewardId"); + break; + } + } + idsSeen.push_back(e.tokenRewardId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wtbr"] = base + ".wtbr"; + 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-wtbr: %s.wtbr\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu rewards, all tokenRewardIds 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 handleTokenRewardsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-tbr") == 0 && i + 1 < argc) { + outRc = handleGenRaid(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tbr-pvp") == 0 && i + 1 < argc) { + outRc = handleGenPvP(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-tbr-faction") == 0 && i + 1 < argc) { + outRc = handleGenFaction(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wtbr") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wtbr") == 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_token_rewards_catalog.hpp b/tools/editor/cli_token_rewards_catalog.hpp new file mode 100644 index 00000000..05151254 --- /dev/null +++ b/tools/editor/cli_token_rewards_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTokenRewardsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee