feat(pipeline): add WPCN (Wowee Player Condition) catalog

49th open format — replaces PlayerCondition.dbc plus the
AzerothCore-style condition resolver. Defines reusable
boolean checks that other catalogs reference by conditionId
to gate gossip options, vendor items, quest availability,
achievement criteria, spell trainer offerings.

16 condition kinds (Always, Race, Class, Level, Zone, Map,
Reputation, AchievementWon, QuestComplete, QuestActive,
SpellKnown, ItemEquipped, Faction, InCombat, Mounted,
Resting), 8 comparison ops (==, !=, >, >=, <, <=, in-set,
not-in-set), and 4 chain ops (none, and, or, not) — chain
multiple conditions via chainNextId to express arbitrary
boolean trees.

Cross-references with prior formats — targetIdA is
polymorphic by conditionKind: resolves to WCHC raceId/classId,
WMS areaId/mapId, WFAC factionId, WACH achievementId, WQT
questId, WSPL spellId, or WIT itemId. chainNextId resolves
within the same WPCN catalog.

CLI: --gen-pcn (3 single-check starters), --gen-pcn-quest-gates
(4 cross-format quest gates with real WQT/WFAC/WACH/WMS IDs),
--gen-pcn-composite (3 leaves + 3 chained roots showing AND/
OR/NOT). Validator catches id=0/duplicates, kind/op out of
range, chain self-loop (infinite recursion), chainOp set
without chainNextId (dangling chain), chainNextId set without
chainOp (dead pointer warning), and unresolved chainNextId
references.
This commit is contained in:
Kelsi 2026-05-09 19:36:56 -07:00
parent 30de6f56cd
commit b983ef6d48
10 changed files with 762 additions and 0 deletions

View file

@ -0,0 +1,320 @@
#include "pipeline/wowee_player_conditions.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'P', 'C', 'N'};
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) != ".wpcn") {
base += ".wpcn";
}
return base;
}
} // namespace
const WoweePlayerCondition::Entry*
WoweePlayerCondition::findById(uint32_t conditionId) const {
for (const auto& e : entries)
if (e.conditionId == conditionId) return &e;
return nullptr;
}
const char* WoweePlayerCondition::conditionKindName(uint8_t k) {
switch (k) {
case Always: return "always";
case Race: return "race";
case Class: return "class";
case Level: return "level";
case Zone: return "zone";
case Map: return "map";
case Reputation: return "reputation";
case AchievementWon: return "achievement";
case QuestComplete: return "quest-complete";
case QuestActive: return "quest-active";
case SpellKnown: return "spell-known";
case ItemEquipped: return "item-equipped";
case Faction: return "faction";
case InCombat: return "in-combat";
case Mounted: return "mounted";
case Resting: return "resting";
default: return "unknown";
}
}
const char* WoweePlayerCondition::comparisonOpName(uint8_t o) {
switch (o) {
case Equal: return "==";
case NotEqual: return "!=";
case GreaterThan: return ">";
case GreaterOrEqual: return ">=";
case LessThan: return "<";
case LessOrEqual: return "<=";
case InSet: return "in-set";
case NotInSet: return "not-in-set";
default: return "?";
}
}
const char* WoweePlayerCondition::chainOpName(uint8_t c) {
switch (c) {
case ChainNone: return "none";
case ChainAnd: return "and";
case ChainOr: return "or";
case ChainNot: return "not";
default: return "unknown";
}
}
bool WoweePlayerConditionLoader::save(const WoweePlayerCondition& 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.conditionId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.conditionKind);
writePOD(os, e.comparisonOp);
writePOD(os, e.chainOp);
uint8_t pad = 0;
writePOD(os, pad);
writePOD(os, e.targetIdA);
writePOD(os, e.targetIdB);
writePOD(os, e.intValueA);
writePOD(os, e.intValueB);
writePOD(os, e.chainNextId);
writeStr(os, e.failMessage);
}
return os.good();
}
WoweePlayerCondition WoweePlayerConditionLoader::load(
const std::string& basePath) {
WoweePlayerCondition 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.conditionId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.conditionKind) ||
!readPOD(is, e.comparisonOp) ||
!readPOD(is, e.chainOp)) {
out.entries.clear(); return out;
}
uint8_t pad = 0;
if (!readPOD(is, pad)) { out.entries.clear(); return out; }
if (!readPOD(is, e.targetIdA) ||
!readPOD(is, e.targetIdB) ||
!readPOD(is, e.intValueA) ||
!readPOD(is, e.intValueB) ||
!readPOD(is, e.chainNextId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.failMessage)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweePlayerConditionLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweePlayerCondition WoweePlayerConditionLoader::makeStarter(
const std::string& catalogName) {
WoweePlayerCondition c;
c.name = catalogName;
{
WoweePlayerCondition::Entry e;
e.conditionId = 1; e.name = "Level60Plus";
e.description = "Player must be level 60 or higher.";
e.conditionKind = WoweePlayerCondition::Level;
e.comparisonOp = WoweePlayerCondition::GreaterOrEqual;
e.intValueA = 60;
e.failMessage = "You must be at least level 60.";
c.entries.push_back(e);
}
{
WoweePlayerCondition::Entry e;
e.conditionId = 2; e.name = "RaceHuman";
e.description = "Player must be Human race (raceId=1).";
e.conditionKind = WoweePlayerCondition::Race;
e.comparisonOp = WoweePlayerCondition::Equal;
e.targetIdA = 1; // WCHC raceId Human
e.failMessage = "Only Humans may take this option.";
c.entries.push_back(e);
}
{
WoweePlayerCondition::Entry e;
e.conditionId = 3; e.name = "ClassWarrior";
e.description = "Player must be Warrior class (classId=1).";
e.conditionKind = WoweePlayerCondition::Class;
e.comparisonOp = WoweePlayerCondition::Equal;
e.targetIdA = 1; // WCHC classId Warrior
e.failMessage = "Only Warriors may take this option.";
c.entries.push_back(e);
}
return c;
}
WoweePlayerCondition WoweePlayerConditionLoader::makeQuestGates(
const std::string& catalogName) {
WoweePlayerCondition c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint8_t kind,
uint8_t op, uint32_t targetA, int32_t intA,
const char* desc, const char* failMsg) {
WoweePlayerCondition::Entry e;
e.conditionId = id; e.name = name; e.description = desc;
e.conditionKind = kind; e.comparisonOp = op;
e.targetIdA = targetA; e.intValueA = intA;
e.failMessage = failMsg;
c.entries.push_back(e);
};
add(100, "Quest1Complete", WoweePlayerCondition::QuestComplete,
WoweePlayerCondition::Equal, 1, 0,
"Player must have completed the bandit-trouble intro quest "
"(WQT questId=1).",
"You must complete 'Bandit Trouble' first.");
add(101, "StormwindHonored", WoweePlayerCondition::Reputation,
WoweePlayerCondition::GreaterOrEqual, 72, 9000,
"Player must be at least Honored (9000) with Stormwind "
"(WFAC factionId=72).",
"You need at least Honored standing with Stormwind.");
add(102, "AchHelloAzeroth", WoweePlayerCondition::AchievementWon,
WoweePlayerCondition::Equal, 6, 0,
"Player must have earned the 'Hello, Azeroth!' achievement "
"(WACH achievementId=6).",
"You haven't earned the 'Hello, Azeroth!' achievement yet.");
add(103, "InElwynnForest", WoweePlayerCondition::Zone,
WoweePlayerCondition::Equal, 12, 0,
"Player must be in Elwynn Forest (WMS areaId=12).",
"You must be in Elwynn Forest to do this.");
return c;
}
WoweePlayerCondition WoweePlayerConditionLoader::makeComposite(
const std::string& catalogName) {
WoweePlayerCondition c;
c.name = catalogName;
// First the leaves, then the chains. Leaves get IDs
// 200-202; the chained roots get 300-302 and reference
// them via chainNextId + chainOp.
auto leaf = [&](uint32_t id, const char* name, uint8_t kind,
uint8_t op, uint32_t targetA, int32_t intA,
const char* desc) {
WoweePlayerCondition::Entry e;
e.conditionId = id; e.name = name; e.description = desc;
e.conditionKind = kind; e.comparisonOp = op;
e.targetIdA = targetA; e.intValueA = intA;
c.entries.push_back(e);
};
leaf(200, "Level80", WoweePlayerCondition::Level,
WoweePlayerCondition::GreaterOrEqual, 0, 80,
"Leaf — level 80 or higher.");
leaf(201, "ClassWarriorLeaf", WoweePlayerCondition::Class,
WoweePlayerCondition::Equal, 1, 0,
"Leaf — class is Warrior.");
leaf(202, "AllyMember", WoweePlayerCondition::Faction,
WoweePlayerCondition::Equal, 469, 0,
"Leaf — member of the Alliance "
"(WFAC factionId=469).");
auto chain = [&](uint32_t id, const char* name, uint8_t headKind,
uint8_t headOp, uint32_t headTarget,
int32_t headInt, uint8_t chainOp,
uint32_t chainNextId, const char* desc,
const char* failMsg) {
WoweePlayerCondition::Entry e;
e.conditionId = id; e.name = name; e.description = desc;
e.conditionKind = headKind; e.comparisonOp = headOp;
e.targetIdA = headTarget; e.intValueA = headInt;
e.chainOp = chainOp;
e.chainNextId = chainNextId;
e.failMessage = failMsg;
c.entries.push_back(e);
};
chain(300, "Level80AndWarrior",
WoweePlayerCondition::Level,
WoweePlayerCondition::GreaterOrEqual, 0, 80,
WoweePlayerCondition::ChainAnd, 201,
"Composite — head=Level>=80 AND tail=Warrior.",
"Requires Warrior, level 80 or higher.");
chain(301, "AllyOrHonored",
WoweePlayerCondition::Reputation,
WoweePlayerCondition::GreaterOrEqual, 72, 9000,
WoweePlayerCondition::ChainOr, 202,
"Composite — head=Honored Stormwind OR tail=Alliance member.",
"Requires Alliance membership or Honored Stormwind.");
chain(302, "NotInCombat",
WoweePlayerCondition::Always,
WoweePlayerCondition::Equal, 0, 0,
WoweePlayerCondition::ChainNot, 200,
"Composite — NOT (level 80 leaf) — sample inverted check.",
"Cannot be used at max level.");
return c;
}
} // namespace pipeline
} // namespace wowee