feat(editor): add WIQR (Item Quality) open catalog format

Open replacement for the hardcoded item quality tiers in the
WoW client (Poor / Common / Uncommon / Rare / Epic / Legendary
/ Artifact / Heirloom). Defines each tier's tooltip text color,
inventory slot border color, vendor price multiplier, drop-level
gating, and disenchant eligibility.

The hardcoded client uses a fixed color table (gray/white/green/
blue/purple/orange/red/gold). This catalog lets server admins:
  - retune the colors (rename "Epic" to "Tier 1" with custom hex)
  - add server-custom tiers above Heirloom
  - change vendor markup per tier (legendary 50x base price)
  - gate quality drops by character level (Heirlooms unlock 80)

The standard preset reproduces the canonical 8-tier scale with
exact hex values from the live client (#9d9d9d through #00ccff)
and standard disenchant rules (Common+ disenchantable, Legendary
and Artifact aren't). The server-custom preset shows 4 tiers
above the standard range with non-standard pricing (Junk 0.1x,
QuestLocked 0.0x unsellable). The raid preset gates 4
progression tiers behind minLevelToDrop=60 with escalating
vendor multipliers up to 50x for Legendary.

Cross-references back to WIT — item entries reference qualityId
here for tooltip color and sort order. canDropAtLevel(id, lvl)
is the engine helper used by loot generation.

Validation enforces name presence, no duplicate ids,
vendorPriceMultiplier >= 0, minLevelToDrop <= maxLevelToDrop;
warns on:
  - minLevelToDrop > 80 (unreachable at WotLK cap)
  - vendorPriceMultiplier > 100x (sanity check the economy)
  - nameColorRGBA with alpha=0 (text would be invisible in
    tooltips — common bug when copy-pasting RGB hex without
    alpha byte)

Wired through the cross-format table; WIQR appears automatically
in all 15 cross-format utilities. Format count 83 -> 84; CLI
flag count 1003 -> 1008.
This commit is contained in:
Kelsi 2026-05-09 22:59:27 -07:00
parent 18f07f1b8a
commit efb88be366
10 changed files with 652 additions and 0 deletions

View file

@ -0,0 +1,263 @@
#include "pipeline/wowee_item_qualities.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'I', 'Q', '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) != ".wiqr") {
base += ".wiqr";
}
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 WoweeItemQuality::Entry*
WoweeItemQuality::findById(uint32_t qualityId) const {
for (const auto& e : entries)
if (e.qualityId == qualityId) return &e;
return nullptr;
}
bool WoweeItemQuality::canDropAtLevel(uint32_t qualityId,
uint8_t characterLevel) const {
const Entry* e = findById(qualityId);
if (!e) return false;
if (characterLevel < e->minLevelToDrop) return false;
if (e->maxLevelToDrop != 0 && characterLevel > e->maxLevelToDrop)
return false;
return true;
}
bool WoweeItemQualityLoader::save(const WoweeItemQuality& 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.qualityId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.nameColorRGBA);
writePOD(os, e.borderColorRGBA);
writePOD(os, e.vendorPriceMultiplier);
writePOD(os, e.minLevelToDrop);
writePOD(os, e.maxLevelToDrop);
writePOD(os, e.canBeDisenchanted);
writePOD(os, e.pad0);
writeStr(os, e.inventoryBorderTexture);
}
return os.good();
}
WoweeItemQuality WoweeItemQualityLoader::load(
const std::string& basePath) {
WoweeItemQuality 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.qualityId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.nameColorRGBA) ||
!readPOD(is, e.borderColorRGBA) ||
!readPOD(is, e.vendorPriceMultiplier) ||
!readPOD(is, e.minLevelToDrop) ||
!readPOD(is, e.maxLevelToDrop) ||
!readPOD(is, e.canBeDisenchanted) ||
!readPOD(is, e.pad0)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.inventoryBorderTexture)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeItemQualityLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeItemQuality WoweeItemQualityLoader::makeStandard(
const std::string& catalogName) {
using Q = WoweeItemQuality;
WoweeItemQuality c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint8_t r, uint8_t g, uint8_t b,
float vendorMul, uint8_t minLvl, uint8_t maxLvl,
uint8_t disenchant, const char* texture,
const char* desc) {
Q::Entry e;
e.qualityId = id; e.name = name; e.description = desc;
e.nameColorRGBA = packRgba(r, g, b);
e.borderColorRGBA = packRgba(r, g, b);
e.vendorPriceMultiplier = vendorMul;
e.minLevelToDrop = minLvl;
e.maxLevelToDrop = maxLvl;
e.canBeDisenchanted = disenchant;
e.inventoryBorderTexture = texture;
c.entries.push_back(e);
};
// The canonical WoW item quality scale, with hex colors
// matching the live client. Heirlooms (id 7) are gated
// to character level 80 in WotLK.
add(0, "Poor", 0x9d, 0x9d, 0x9d, 0.5f, 1, 0, 0,
"", "Poor (gray) — junk loot; vendor sells at half price.");
add(1, "Common", 0xff, 0xff, 0xff, 1.0f, 1, 0, 1,
"", "Common (white) — basic gear; standard vendor pricing.");
add(2, "Uncommon", 0x1e, 0xff, 0x00, 1.5f, 1, 0, 1,
"Border-Uncommon",
"Uncommon (green) — early-tier quest reward; 50% markup.");
add(3, "Rare", 0x00, 0x70, 0xdd, 2.0f, 1, 0, 1,
"Border-Rare",
"Rare (blue) — dungeon-tier; 2x markup, can be disenchanted.");
add(4, "Epic", 0xa3, 0x35, 0xee, 4.0f, 60, 0, 1,
"Border-Epic",
"Epic (purple) — raid-tier; 4x markup, disenchants to high-tier dust.");
add(5, "Legendary", 0xff, 0x80, 0x00, 8.0f, 60, 0, 0,
"Border-Legendary",
"Legendary (orange) — extremely rare; cannot be disenchanted.");
add(6, "Artifact", 0xe6, 0xcc, 0x80, 16.0f, 80, 0, 0,
"Border-Artifact",
"Artifact (red-gold) — unique, account-bound.");
add(7, "Heirloom", 0x00, 0xcc, 0xff, 1.0f, 80, 0, 0,
"Border-Heirloom",
"Heirloom (cyan) — scales to character level, lvl 80+ unlock.");
return c;
}
WoweeItemQuality WoweeItemQualityLoader::makeServerCustom(
const std::string& catalogName) {
using Q = WoweeItemQuality;
WoweeItemQuality c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint8_t r, uint8_t g, uint8_t b,
float vendorMul, uint8_t disenchant,
const char* desc) {
Q::Entry e;
e.qualityId = id; e.name = name; e.description = desc;
e.nameColorRGBA = packRgba(r, g, b);
e.borderColorRGBA = packRgba(r, g, b);
e.vendorPriceMultiplier = vendorMul;
e.canBeDisenchanted = disenchant;
c.entries.push_back(e);
};
// 4 server-custom tiers above the standard 0..7 range.
add(100, "Junk", 0x33, 0x33, 0x33, 0.1f, 0,
"Server-custom: cosmetic junk, near-zero vendor price.");
add(101, "Weekly", 0x80, 0xff, 0x80, 5.0f, 0,
"Server-custom: drops only from weekly raids, "
"premium pricing.");
add(102, "QuestLocked",0xff, 0xff, 0x40, 0.0f, 0,
"Server-custom: quest-bound, cannot be sold.");
add(103, "Donator", 0xff, 0x40, 0xff, 0.0f, 0,
"Server-custom: donor reward, soulbound, unsellable.");
return c;
}
WoweeItemQuality WoweeItemQualityLoader::makeRaidTiers(
const std::string& catalogName) {
using Q = WoweeItemQuality;
WoweeItemQuality c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name,
uint8_t r, uint8_t g, uint8_t b,
float vendorMul, uint8_t minLvl,
const char* desc) {
Q::Entry e;
e.qualityId = id; e.name = name; e.description = desc;
e.nameColorRGBA = packRgba(r, g, b);
e.borderColorRGBA = packRgba(r, g, b);
e.vendorPriceMultiplier = vendorMul;
e.minLevelToDrop = minLvl;
e.canBeDisenchanted = 1;
c.entries.push_back(e);
};
// Vanilla raid progression tiers as alternative quality
// markers — each tier gates at a higher minLevelToDrop
// and commands a higher vendor multiplier.
add(200, "Tier1Raid", 0xa3, 0x35, 0xee, 4.0f, 60,
"Tier 1 raid set (MC / Onyxia) — Epic-color, lvl 60.");
add(201, "Tier2Raid", 0xc8, 0x4c, 0xff, 6.0f, 60,
"Tier 2 raid set (BWL) — slightly brighter purple, "
"premium pricing.");
add(202, "Tier3Raid", 0xff, 0x80, 0xc8, 10.0f, 60,
"Tier 3 raid set (Naxx pre-WotLK) — pink-orange, "
"rarest pre-TBC tier.");
add(203, "Legendary", 0xff, 0x80, 0x00, 50.0f, 60,
"Tier-equivalent legendary (Thunderfury / Sulfuras / "
"Atiesh) — premium economy pricing.");
return c;
}
} // namespace pipeline
} // namespace wowee