feat(editor): add WRPR (Reputation Reward tier) — 109th open format

Novel replacement for the implicit reputation-tier rules
vanilla WoW encoded across multiple SQL tables
(npc_vendor with reqstanding columns, item_template
faction gates, quest_template ReqMinRepFaction). Each
WRPR entry binds one (factionId, minStanding) tier to
its rewards: a vendor discount percentage, two variable-
length arrays of unlocked content (item IDs + recipe
IDs), and tabard + mount unlock boolean flags.

First catalog with TWO variable-length payload arrays
per entry (unlockedItemIds + unlockedRecipeIds) —
previous variable-length formats used a single array
(WCMR waypoints, WCMG members, WPTT spellIdsByRank,
WBAB rank-chain pointers). The two-array shape is
serialized as count1 + ids1[] + count2 + ids2[] for
easy reader-side validation.

Three preset emitters: makeArgentCrusade (4 tiers
Friendly/Honored/Revered/Exalted with progressive items
+ recipes plus Argent Charger mount at Exalted),
makeKaluak (4 fishing-themed tiers with cooking recipe
unlocks plus Pygmy Suit cosmetic at Exalted),
makeAccordTabard (3 tiers showcasing both grantsTabard
and grantsMount flags via Wyrmrest Accord's iconic
Reins of the Red Drake mount).

Validator's most novel checks combine relational and
domain logic: (factionId, minStanding) tuple uniqueness
prevents ambiguous active-tier lookup, AND per-faction
monotonic discount progression — sorts each faction's
tiers by standing and verifies discountPct is non-
decreasing. A higher reputation tier giving a worse
vendor discount would be a content authoring bug.

findActiveTierFor() helper picks the highest-standing
tier the player meets — used by the vendor UI to
compute the active discount without scanning the
catalog.

Format count 108 -> 109. CLI flag count 1184 -> 1189.
This commit is contained in:
Kelsi 2026-05-10 01:59:03 -07:00
parent f7ea99948a
commit 8fee281899
10 changed files with 826 additions and 0 deletions

View file

@ -697,6 +697,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_creature_resists.cpp
src/pipeline/wowee_pet_talents.cpp
src/pipeline/wowee_heroic_scaling.cpp
src/pipeline/wowee_reputation_rewards.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1557,6 +1558,7 @@ add_executable(wowee_editor
tools/editor/cli_creature_resists_catalog.cpp
tools/editor/cli_pet_talents_catalog.cpp
tools/editor/cli_heroic_scaling_catalog.cpp
tools/editor/cli_reputation_rewards_catalog.cpp
tools/editor/cli_catalog_pluck.cpp
tools/editor/cli_catalog_find.cpp
tools/editor/cli_catalog_by_name.cpp
@ -1735,6 +1737,7 @@ add_executable(wowee_editor
src/pipeline/wowee_creature_resists.cpp
src/pipeline/wowee_pet_talents.cpp
src/pipeline/wowee_heroic_scaling.cpp
src/pipeline/wowee_reputation_rewards.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,126 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Reputation Reward Tier catalog (.wrpr) —
// novel replacement for the implicit reputation-tier
// rules vanilla WoW encoded across multiple SQL tables
// (npc_vendor with reqstanding columns, item_template
// AllowableRace/Class plus PaperDoll faction gates,
// quest_template ReqMinRepFaction). Each entry binds
// one (factionId, minStanding) tier to its rewards: a
// vendor discount percentage, a list of unlocked item
// IDs, a list of unlocked recipe IDs, and tabard +
// mount unlock flags.
//
// First catalog with TWO variable-length payload arrays
// per entry (unlockedItemIds + unlockedRecipeIds) —
// previous variable-length formats used a single array
// (WCMR waypoints, WCMG members, WPTT spellIdsByRank).
//
// Cross-references with previously-added formats:
// WFAC: factionId references the WFAC faction catalog.
// WIT: unlockedItemIds entries reference the WIT
// item catalog (gear, consumables, mounts).
// WTSK: unlockedRecipeIds entries reference the WTSK
// trade-skill recipe catalog.
// WTBD: when grantsTabard=1, the faction tabard
// becomes purchasable (per-faction tabardId
// lookup deferred to the WTBD catalog at runtime).
//
// Binary layout (little-endian):
// magic[4] = "WRPR"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// tierId (uint32)
// nameLen + name
// descLen + description
// factionId (uint32)
// minStanding (int32) — Hated -42000 to
// Exalted +42000
// discountPct (uint8) — 0..20 vendor
// discount tier
// grantsTabard (uint8) — 0/1 bool
// grantsMount (uint8) — 0/1 bool
// pad0 (uint8)
// iconColorRGBA (uint32)
// unlockedItemCount (uint32)
// unlockedItemIds (count × uint32)
// unlockedRecipeCount (uint32)
// unlockedRecipeIds (count × uint32)
struct WoweeReputationRewards {
struct Entry {
uint32_t tierId = 0;
std::string name;
std::string description;
uint32_t factionId = 0;
int32_t minStanding = 0;
uint8_t discountPct = 0;
uint8_t grantsTabard = 0;
uint8_t grantsMount = 0;
uint8_t pad0 = 0;
uint32_t iconColorRGBA = 0xFFFFFFFFu;
std::vector<uint32_t> unlockedItemIds;
std::vector<uint32_t> unlockedRecipeIds;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t tierId) const;
// Returns the tier for a given (faction, current
// standing). Picks the highest tier whose
// minStanding the player meets. Used by the vendor
// UI to compute "at what discount can I buy from
// this NPC?" without scanning the catalog.
const Entry* findActiveTierFor(uint32_t factionId,
int32_t currentStanding) const;
// Returns all tiers for a faction in ascending
// standing order. Used by the achievement / unlock
// preview UI ("what do I get at Revered?").
std::vector<const Entry*> findByFaction(uint32_t factionId) const;
};
class WoweeReputationRewardsLoader {
public:
static bool save(const WoweeReputationRewards& cat,
const std::string& basePath);
static WoweeReputationRewards load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-rpr* variants.
//
// makeArgentCrusade — 4 tiers (Friendly 3000 /
// Honored 9000 / Revered 21000
// / Exalted 42000) with
// progressive item + recipe
// unlocks plus tabard at
// Friendly and mount at
// Exalted.
// makeKaluak — 4 fishing-themed tiers for
// Kalu'ak faction, progressive
// cooking recipe unlocks.
// makeAccordTabard — 3 tiers showcasing the
// grantsTabard + grantsMount
// flags (Wyrmrest Accord
// Honored item / Revered
// tabard / Exalted Red Drake
// mount).
static WoweeReputationRewards makeArgentCrusade(const std::string& catalogName);
static WoweeReputationRewards makeKaluak(const std::string& catalogName);
static WoweeReputationRewards makeAccordTabard(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,334 @@
#include "pipeline/wowee_reputation_rewards.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'R', 'P', 'R'};
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) != ".wrpr") {
base += ".wrpr";
}
return base;
}
uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) {
return (static_cast<uint32_t>(a) << 24) |
(static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(g) << 8) |
static_cast<uint32_t>(r);
}
} // namespace
const WoweeReputationRewards::Entry*
WoweeReputationRewards::findById(uint32_t tierId) const {
for (const auto& e : entries)
if (e.tierId == tierId) return &e;
return nullptr;
}
const WoweeReputationRewards::Entry*
WoweeReputationRewards::findActiveTierFor(uint32_t factionId,
int32_t currentStanding) const {
const Entry* best = nullptr;
for (const auto& e : entries) {
if (e.factionId != factionId) continue;
if (currentStanding < e.minStanding) continue;
// Highest minStanding wins.
if (best == nullptr ||
e.minStanding > best->minStanding) {
best = &e;
}
}
return best;
}
std::vector<const WoweeReputationRewards::Entry*>
WoweeReputationRewards::findByFaction(uint32_t factionId) const {
std::vector<const Entry*> out;
for (const auto& e : entries) {
if (e.factionId == factionId) out.push_back(&e);
}
std::sort(out.begin(), out.end(),
[](const Entry* a, const Entry* b) {
return a->minStanding < b->minStanding;
});
return out;
}
bool WoweeReputationRewardsLoader::save(
const WoweeReputationRewards& 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.tierId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.factionId);
writePOD(os, e.minStanding);
writePOD(os, e.discountPct);
writePOD(os, e.grantsTabard);
writePOD(os, e.grantsMount);
writePOD(os, e.pad0);
writePOD(os, e.iconColorRGBA);
uint32_t itemCount = static_cast<uint32_t>(
e.unlockedItemIds.size());
writePOD(os, itemCount);
for (uint32_t id : e.unlockedItemIds) writePOD(os, id);
uint32_t recipeCount = static_cast<uint32_t>(
e.unlockedRecipeIds.size());
writePOD(os, recipeCount);
for (uint32_t id : e.unlockedRecipeIds) writePOD(os, id);
}
return os.good();
}
WoweeReputationRewards WoweeReputationRewardsLoader::load(
const std::string& basePath) {
WoweeReputationRewards 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.tierId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.factionId) ||
!readPOD(is, e.minStanding) ||
!readPOD(is, e.discountPct) ||
!readPOD(is, e.grantsTabard) ||
!readPOD(is, e.grantsMount) ||
!readPOD(is, e.pad0) ||
!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
uint32_t itemCount = 0;
if (!readPOD(is, itemCount)) {
out.entries.clear(); return out;
}
if (itemCount > (1u << 16)) {
out.entries.clear(); return out;
}
e.unlockedItemIds.resize(itemCount);
for (uint32_t k = 0; k < itemCount; ++k) {
if (!readPOD(is, e.unlockedItemIds[k])) {
out.entries.clear(); return out;
}
}
uint32_t recipeCount = 0;
if (!readPOD(is, recipeCount)) {
out.entries.clear(); return out;
}
if (recipeCount > (1u << 16)) {
out.entries.clear(); return out;
}
e.unlockedRecipeIds.resize(recipeCount);
for (uint32_t k = 0; k < recipeCount; ++k) {
if (!readPOD(is, e.unlockedRecipeIds[k])) {
out.entries.clear(); return out;
}
}
}
return out;
}
bool WoweeReputationRewardsLoader::exists(
const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeReputationRewards
WoweeReputationRewardsLoader::makeArgentCrusade(
const std::string& catalogName) {
using R = WoweeReputationRewards;
WoweeReputationRewards c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
int32_t standing, uint8_t discount,
uint8_t tabard, uint8_t mount,
std::vector<uint32_t> items,
std::vector<uint32_t> recipes,
const char* desc) {
R::Entry e;
e.tierId = id; e.name = name; e.description = desc;
e.factionId = 1106; // Argent Crusade
e.minStanding = standing;
e.discountPct = discount;
e.grantsTabard = tabard;
e.grantsMount = mount;
e.unlockedItemIds = std::move(items);
e.unlockedRecipeIds = std::move(recipes);
e.iconColorRGBA = packRgba(220, 220, 220); // silver
c.entries.push_back(e);
};
// standing thresholds: Friendly=3000, Honored=9000,
// Revered=21000, Exalted=42000.
add(1, "ArgentCrusade_Friendly", 3000, 0, 0, 0,
{ 44128 }, {},
"Friendly tier — basic faction recognition. "
"Quartermaster opens. No discount yet.");
add(2, "ArgentCrusade_Honored", 9000, 5, 1, 0,
{ 44128, 44131, 44137 }, { 49736 },
"Honored tier — 5%% vendor discount, tabard "
"becomes purchasable, first crafting recipe "
"(Argent Sword pattern) unlocks.");
add(3, "ArgentCrusade_Revered", 21000, 10, 1, 0,
{ 44128, 44131, 44137, 44141, 44144 },
{ 49736, 49737 },
"Revered tier — 10%% vendor discount. Two "
"additional rare items + second recipe (Argent "
"Plate Gauntlets) unlock.");
add(4, "ArgentCrusade_Exalted", 42000, 15, 1, 1,
{ 44128, 44131, 44137, 44141, 44144, 44171, 44174 },
{ 49736, 49737, 49738 },
"Exalted tier — 15%% vendor discount, the Argent "
"Charger mount unlocks (3500g, paladin-only "
"originally), full set of rare items, all recipes.");
return c;
}
WoweeReputationRewards WoweeReputationRewardsLoader::makeKaluak(
const std::string& catalogName) {
using R = WoweeReputationRewards;
WoweeReputationRewards c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
int32_t standing, uint8_t discount,
uint8_t tabard,
std::vector<uint32_t> items,
std::vector<uint32_t> recipes,
const char* desc) {
R::Entry e;
e.tierId = id; e.name = name; e.description = desc;
e.factionId = 1073; // The Kalu'ak
e.minStanding = standing;
e.discountPct = discount;
e.grantsTabard = tabard;
e.grantsMount = 0;
e.unlockedItemIds = std::move(items);
e.unlockedRecipeIds = std::move(recipes);
e.iconColorRGBA = packRgba(140, 200, 220); // sea blue
c.entries.push_back(e);
};
add(100, "Kaluak_Friendly", 3000, 0, 0,
{ 44707 }, {},
"Friendly — basic Kalu'ak fishing pole "
"purchasable.");
add(101, "Kaluak_Honored", 9000, 5, 0,
{ 44707, 44710 }, { 45550 },
"Honored — Kalu'ak Cured Sweet Potato cooking "
"recipe unlocks.");
add(102, "Kaluak_Revered", 21000, 10, 1,
{ 44707, 44710, 44715 }, { 45550, 45551 },
"Revered — Kalu'ak Tabard purchasable, second "
"cooking recipe unlocks.");
add(103, "Kaluak_Exalted", 42000, 15, 1,
{ 44707, 44710, 44715, 44722 },
{ 45550, 45551, 45552 },
"Exalted — Pygmy Suit cosmetic + 3rd cooking "
"recipe (Imperial Manta Steak) unlock. No "
"mount reward for Kalu'ak.");
return c;
}
WoweeReputationRewards
WoweeReputationRewardsLoader::makeAccordTabard(
const std::string& catalogName) {
using R = WoweeReputationRewards;
WoweeReputationRewards c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
int32_t standing, uint8_t discount,
uint8_t tabard, uint8_t mount,
std::vector<uint32_t> items,
const char* desc) {
R::Entry e;
e.tierId = id; e.name = name; e.description = desc;
e.factionId = 1091; // Wyrmrest Accord
e.minStanding = standing;
e.discountPct = discount;
e.grantsTabard = tabard;
e.grantsMount = mount;
e.unlockedItemIds = std::move(items);
e.iconColorRGBA = packRgba(180, 60, 60); // dragon red
c.entries.push_back(e);
};
add(200, "WyrmrestAccord_Honored", 9000, 5, 0, 0,
{ 44156, 44158 },
"Honored — first ring + cloak unlock. No tabard "
"yet (Accord makes you wait until Revered).");
add(201, "WyrmrestAccord_Revered", 21000, 10, 1, 0,
{ 44156, 44158, 44160 },
"Revered — Accord Tabard purchasable + medallion. "
"Equipping the tabard counts Wyrmrest rep on "
"ALL Northrend Heroic kills.");
add(202, "WyrmrestAccord_Exalted", 42000, 15, 1, 1,
{ 44156, 44158, 44160, 44178 },
"Exalted — the Reins of the Red Drake mount "
"unlocks (3000g). One of the iconic Wrath rep "
"rewards.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -334,6 +334,8 @@ const char* const kArgRequired[] = {
"--gen-hrd", "--gen-hrd-raid25", "--gen-hrd-cm",
"--info-whrd", "--validate-whrd",
"--export-whrd-json", "--import-whrd-json",
"--gen-rpr", "--gen-rpr-kaluak", "--gen-rpr-accord",
"--info-wrpr", "--validate-wrpr",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -153,6 +153,7 @@
#include "cli_creature_resists_catalog.hpp"
#include "cli_pet_talents_catalog.hpp"
#include "cli_heroic_scaling_catalog.hpp"
#include "cli_reputation_rewards_catalog.hpp"
#include "cli_catalog_pluck.hpp"
#include "cli_catalog_find.hpp"
#include "cli_catalog_by_name.hpp"
@ -350,6 +351,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleCreatureResistsCatalog,
handlePetTalentsCatalog,
handleHeroicScalingCatalog,
handleReputationRewardsCatalog,
handleCatalogPluck,
handleCatalogFind,
handleCatalogByName,

View file

@ -111,6 +111,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','C','R','E'}, ".wcre", "creatures", "--info-wcre", "Creature resist + immunity catalog"},
{{'W','P','T','T'}, ".wptt", "pets", "--info-wptt", "Hunter pet talent tree catalog"},
{{'W','H','R','D'}, ".whrd", "raid", "--info-whrd", "Heroic loot scaling catalog"},
{{'W','R','P','R'}, ".wrpr", "factions", "--info-wrpr", "Reputation reward 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"},

View file

@ -2279,6 +2279,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .whrd to a human-editable JSON sidecar (defaults to <base>.whrd.json; emits bonusQualityChance as both raw basis points AND derived bonusQualityPct float convenience field)\n");
std::printf(" --import-whrd-json <json-path> [out-base]\n");
std::printf(" Import a .whrd.json sidecar back into binary .whrd (bonusQualityChance accepts raw basis points int OR bonusQualityPct float — converts pct *100 -> basis points; itemLevelDelta is signed int16; dropChanceMultiplier is float)\n");
std::printf(" --gen-rpr <wrpr-base> [name]\n");
std::printf(" Emit .wrpr 4 Argent Crusade reputation tiers (Friendly 3000 / Honored 9000 / Revered 21000 / Exalted 42000) with progressive item + recipe unlocks plus tabard at Honored and Argent Charger mount at Exalted\n");
std::printf(" --gen-rpr-kaluak <wrpr-base> [name]\n");
std::printf(" Emit .wrpr 4 Kalu'ak fishing-themed reputation tiers with cooking recipe unlocks (Sweet Potato / Imperial Manta Steak / Pygmy Suit cosmetic at Exalted)\n");
std::printf(" --gen-rpr-accord <wrpr-base> [name]\n");
std::printf(" Emit .wrpr 3 Wyrmrest Accord tiers showcasing tabard + mount unlock flags (Honored items / Revered tabard / Exalted Reins of the Red Drake)\n");
std::printf(" --info-wrpr <wrpr-base> [--json]\n");
std::printf(" Print WRPR entries (id / faction / standing / standing tier / discount %% / tabard flag / mount flag / item count / recipe count / name)\n");
std::printf(" --validate-wrpr <wrpr-base> [--json]\n");
std::printf(" Static checks: id+name+factionId required, minStanding [-42000, 42000], no zero item/recipe IDs, no duplicate tierIds, no two tiers binding same (factionId,minStanding) tuple; warns on discountPct>20%% (exceeds Exalted cap), and PER-FACTION non-monotonic discount progression (higher standing should never give worse discount)\n");
std::printf(" --catalog-pluck <wXXX-file> <id> [--json]\n");
std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n");
std::printf(" --catalog-find <directory> <id> [--magic <WXXX>] [--json]\n");

View file

@ -133,6 +133,7 @@ constexpr FormatRow kFormats[] = {
{"WCRE", ".wcre", "creatures", "creature_template resist + immunity","Creature resist + CC-immunity profile catalog"},
{"WPTT", ".wptt", "pets", "PetTalent.dbc + PetTalentTab.dbc", "Hunter pet talent tree catalog (3 trees, grid+graph)"},
{"WHRD", ".whrd", "raid", "implicit Heroic-mode loot scaling", "Heroic loot scaling catalog (per instance+difficulty)"},
{"WRPR", ".wrpr", "factions", "npc_vendor reqstanding + rep gates", "Reputation reward tier catalog (per faction)"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine

View file

@ -0,0 +1,335 @@
#include "cli_reputation_rewards_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_reputation_rewards.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWrprExt(std::string base) {
stripExt(base, ".wrpr");
return base;
}
const char* standingTierName(int32_t standing) {
if (standing >= 42000) return "Exalted";
if (standing >= 21000) return "Revered";
if (standing >= 9000) return "Honored";
if (standing >= 3000) return "Friendly";
if (standing >= 0) return "Neutral";
if (standing >= -3000) return "Unfriendly";
if (standing >= -6000) return "Hostile";
return "Hated";
}
bool saveOrError(const wowee::pipeline::WoweeReputationRewards& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeReputationRewardsLoader::save(
c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wrpr\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeReputationRewards& c,
const std::string& base) {
size_t totalItems = 0;
size_t totalRecipes = 0;
for (const auto& e : c.entries) {
totalItems += e.unlockedItemIds.size();
totalRecipes += e.unlockedRecipeIds.size();
}
std::printf("Wrote %s.wrpr\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" tiers : %zu (%zu items, %zu recipes total)\n",
c.entries.size(), totalItems, totalRecipes);
}
int handleGenArgent(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "ArgentCrusadeRewards";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWrprExt(base);
auto c = wowee::pipeline::WoweeReputationRewardsLoader::
makeArgentCrusade(name);
if (!saveOrError(c, base, "gen-rpr")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenKaluak(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "KaluakRewards";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWrprExt(base);
auto c = wowee::pipeline::WoweeReputationRewardsLoader::
makeKaluak(name);
if (!saveOrError(c, base, "gen-rpr-kaluak")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenAccord(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "WyrmrestAccordRewards";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWrprExt(base);
auto c = wowee::pipeline::WoweeReputationRewardsLoader::
makeAccordTabard(name);
if (!saveOrError(c, base, "gen-rpr-accord")) 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 = stripWrprExt(base);
if (!wowee::pipeline::WoweeReputationRewardsLoader::exists(
base)) {
std::fprintf(stderr, "WRPR not found: %s.wrpr\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeReputationRewardsLoader::load(
base);
if (jsonOut) {
nlohmann::json j;
j["wrpr"] = base + ".wrpr";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"tierId", e.tierId},
{"name", e.name},
{"description", e.description},
{"factionId", e.factionId},
{"minStanding", e.minStanding},
{"standingTier", standingTierName(e.minStanding)},
{"discountPct", e.discountPct},
{"grantsTabard", e.grantsTabard != 0},
{"grantsMount", e.grantsMount != 0},
{"iconColorRGBA", e.iconColorRGBA},
{"unlockedItemIds", e.unlockedItemIds},
{"unlockedRecipeIds", e.unlockedRecipeIds},
{"unlockedItemCount", e.unlockedItemIds.size()},
{"unlockedRecipeCount",
e.unlockedRecipeIds.size()},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WRPR: %s.wrpr\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 faction standing tier discount tab mnt items recipes name\n");
for (const auto& e : c.entries) {
std::printf(" %4u %5u %+6d %-9s %3u%% %s %s %4zu %4zu %s\n",
e.tierId, e.factionId,
e.minStanding,
standingTierName(e.minStanding),
e.discountPct,
e.grantsTabard ? "yes" : "no ",
e.grantsMount ? "yes" : "no ",
e.unlockedItemIds.size(),
e.unlockedRecipeIds.size(),
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 = stripWrprExt(base);
if (!wowee::pipeline::WoweeReputationRewardsLoader::exists(
base)) {
std::fprintf(stderr,
"validate-wrpr: WRPR not found: %s.wrpr\n",
base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeReputationRewardsLoader::load(
base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (c.entries.empty()) {
warnings.push_back("catalog has zero entries");
}
std::set<uint32_t> idsSeen;
// (factionId, minStanding) tuple uniqueness — two
// tiers binding the same (faction, standing) would
// make the active-tier lookup ambiguous.
std::set<uint64_t> tierTupleSeen;
// Per-faction tier-monotonicity check: discountPct
// should be non-decreasing as standing increases.
std::map<uint32_t, std::vector<
const wowee::pipeline::WoweeReputationRewards::Entry*>>
byFaction;
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.tierId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.tierId == 0)
errors.push_back(ctx + ": tierId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.factionId == 0) {
errors.push_back(ctx +
": factionId is 0 — tier is not bound "
"to any WFAC faction");
}
if (e.minStanding < -42000 || e.minStanding > 42000) {
errors.push_back(ctx + ": minStanding " +
std::to_string(e.minStanding) +
" outside [-42000, 42000] (Hated to "
"Exalted) valid range");
}
if (e.discountPct > 20) {
warnings.push_back(ctx + ": discountPct " +
std::to_string(e.discountPct) +
" > 20%% — exceeds typical max vendor "
"discount (Exalted is canonically 20%%)");
}
// No item/recipe IDs may be 0.
for (size_t s = 0; s < e.unlockedItemIds.size(); ++s) {
if (e.unlockedItemIds[s] == 0) {
errors.push_back(ctx +
": unlockedItemIds[" + std::to_string(s) +
"] = 0");
}
}
for (size_t s = 0; s < e.unlockedRecipeIds.size(); ++s) {
if (e.unlockedRecipeIds[s] == 0) {
errors.push_back(ctx +
": unlockedRecipeIds[" + std::to_string(s) +
"] = 0");
}
}
if (e.factionId != 0) {
uint64_t key = (static_cast<uint64_t>(e.factionId)
<< 32) |
static_cast<uint32_t>(e.minStanding);
if (!tierTupleSeen.insert(key).second) {
errors.push_back(ctx +
": (factionId=" +
std::to_string(e.factionId) +
", minStanding=" +
std::to_string(e.minStanding) +
") combo already bound by another "
"tier — active-tier lookup would be "
"ambiguous");
}
}
if (!idsSeen.insert(e.tierId).second) {
errors.push_back(ctx + ": duplicate tierId");
}
byFaction[e.factionId].push_back(&e);
}
// Per-faction monotonicity: discountPct should be
// non-decreasing as standing increases. Higher
// standing should never give a worse discount.
for (auto& [factionId, tiers] : byFaction) {
if (tiers.size() < 2) continue;
std::sort(tiers.begin(), tiers.end(),
[](auto* a, auto* b) {
return a->minStanding < b->minStanding;
});
for (size_t k = 1; k < tiers.size(); ++k) {
if (tiers[k]->discountPct < tiers[k-1]->discountPct) {
warnings.push_back("faction " +
std::to_string(factionId) +
" has decreasing discount: tier '" +
tiers[k-1]->name + "' (standing " +
std::to_string(tiers[k-1]->minStanding) +
", discount " +
std::to_string(tiers[k-1]->discountPct) +
"%) > tier '" + tiers[k]->name +
"' (standing " +
std::to_string(tiers[k]->minStanding) +
", discount " +
std::to_string(tiers[k]->discountPct) +
"%) — higher standing should not "
"have worse discount");
}
}
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wrpr"] = base + ".wrpr";
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-wrpr: %s.wrpr\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu tiers, all tierIds + "
"(faction,standing) tuples unique, "
"discounts monotonic per faction\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 handleReputationRewardsCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-rpr") == 0 && i + 1 < argc) {
outRc = handleGenArgent(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-rpr-kaluak") == 0 &&
i + 1 < argc) {
outRc = handleGenKaluak(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-rpr-accord") == 0 &&
i + 1 < argc) {
outRc = handleGenAccord(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wrpr") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wrpr") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,12 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleReputationRewardsCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee