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

@ -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