mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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:
parent
f7ea99948a
commit
8fee281899
10 changed files with 826 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
126
include/pipeline/wowee_reputation_rewards.hpp
Normal file
126
include/pipeline/wowee_reputation_rewards.hpp
Normal 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
|
||||
334
src/pipeline/wowee_reputation_rewards.cpp
Normal file
334
src/pipeline/wowee_reputation_rewards.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
335
tools/editor/cli_reputation_rewards_catalog.cpp
Normal file
335
tools/editor/cli_reputation_rewards_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_reputation_rewards_catalog.hpp
Normal file
12
tools/editor/cli_reputation_rewards_catalog.hpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue