mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
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:
parent
30de6f56cd
commit
b983ef6d48
10 changed files with 762 additions and 0 deletions
|
|
@ -636,6 +636,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_animations.cpp
|
||||
src/pipeline/wowee_spell_visuals.cpp
|
||||
src/pipeline/wowee_world_state_ui.cpp
|
||||
src/pipeline/wowee_player_conditions.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1423,6 +1424,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_summary_dir.cpp
|
||||
tools/editor/cli_rename_magic.cpp
|
||||
tools/editor/cli_world_state_ui_catalog.cpp
|
||||
tools/editor/cli_player_conditions_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1537,6 +1539,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_animations.cpp
|
||||
src/pipeline/wowee_spell_visuals.cpp
|
||||
src/pipeline/wowee_world_state_ui.cpp
|
||||
src/pipeline/wowee_player_conditions.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
146
include/pipeline/wowee_player_conditions.hpp
Normal file
146
include/pipeline/wowee_player_conditions.hpp
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Player Condition catalog (.wpcn) — novel
|
||||
// replacement for Blizzard's PlayerCondition.dbc plus the
|
||||
// AzerothCore-style condition resolver. The 49th open
|
||||
// format added to the editor.
|
||||
//
|
||||
// Defines reusable conditional checks that other catalogs
|
||||
// reference by conditionId: quest availability ("player
|
||||
// is level 60+ AND has reputation honored with Stormwind"),
|
||||
// gossip option visibility, vendor item gating, achievement
|
||||
// criteria, spell trainer offerings. Conditions can chain
|
||||
// via chainNextId + chainOp to express AND / OR / NOT
|
||||
// composites, so even the simplest scalar entries scale up
|
||||
// to arbitrary boolean trees.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WPCN.entry.targetIdA → polymorphic by conditionKind:
|
||||
// Race → WCHC.race.raceId
|
||||
// Class → WCHC.class.classId
|
||||
// Zone → WMS.area.areaId
|
||||
// Map → WMS.map.mapId
|
||||
// Reputation→ WFAC.factionId
|
||||
// Achievement→WACH.achievementId
|
||||
// Quest → WQT.questId
|
||||
// SpellKnown→ WSPL.spellId
|
||||
// ItemEquipped→WIT.itemId
|
||||
// Faction → WFAC.factionId
|
||||
// WPCN.entry.chainNextId → WPCN.entry.conditionId
|
||||
// (composite chain terminator)
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WPCN"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// conditionId (uint32)
|
||||
// nameLen + name
|
||||
// descLen + description
|
||||
// conditionKind (uint8) / comparisonOp (uint8) /
|
||||
// chainOp (uint8) / pad[1]
|
||||
// targetIdA (uint32)
|
||||
// targetIdB (uint32)
|
||||
// intValueA (int32)
|
||||
// intValueB (int32)
|
||||
// chainNextId (uint32)
|
||||
// failMsgLen + failMessage
|
||||
struct WoweePlayerCondition {
|
||||
enum ConditionKind : uint8_t {
|
||||
Always = 0, // unconditional pass (used as base)
|
||||
Race = 1, // targetIdA = raceId
|
||||
Class = 2, // targetIdA = classId
|
||||
Level = 3, // intValueA = level threshold
|
||||
Zone = 4, // targetIdA = areaId
|
||||
Map = 5, // targetIdA = mapId
|
||||
Reputation = 6, // targetIdA = factionId, intA = standing
|
||||
AchievementWon = 7, // targetIdA = achievementId
|
||||
QuestComplete = 8, // targetIdA = questId
|
||||
QuestActive = 9, // targetIdA = questId
|
||||
SpellKnown = 10, // targetIdA = spellId
|
||||
ItemEquipped = 11, // targetIdA = itemId
|
||||
Faction = 12, // targetIdA = factionId (membership)
|
||||
InCombat = 13, // boolean
|
||||
Mounted = 14, // boolean
|
||||
Resting = 15, // boolean
|
||||
};
|
||||
|
||||
enum ComparisonOp : uint8_t {
|
||||
Equal = 0,
|
||||
NotEqual = 1,
|
||||
GreaterThan = 2,
|
||||
GreaterOrEqual = 3,
|
||||
LessThan = 4,
|
||||
LessOrEqual = 5,
|
||||
InSet = 6, // targetIdA, targetIdB are 2 valid values
|
||||
NotInSet = 7,
|
||||
};
|
||||
|
||||
enum ChainOp : uint8_t {
|
||||
ChainNone = 0, // no further check — terminator
|
||||
ChainAnd = 1, // also requires chainNextId to pass
|
||||
ChainOr = 2, // either this or chainNextId passes
|
||||
ChainNot = 3, // negate the chainNextId result
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t conditionId = 0;
|
||||
std::string name;
|
||||
std::string description;
|
||||
uint8_t conditionKind = Always;
|
||||
uint8_t comparisonOp = Equal;
|
||||
uint8_t chainOp = ChainNone;
|
||||
uint32_t targetIdA = 0;
|
||||
uint32_t targetIdB = 0;
|
||||
int32_t intValueA = 0;
|
||||
int32_t intValueB = 0;
|
||||
uint32_t chainNextId = 0; // WPCN cross-ref
|
||||
std::string failMessage;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
const Entry* findById(uint32_t conditionId) const;
|
||||
|
||||
static const char* conditionKindName(uint8_t k);
|
||||
static const char* comparisonOpName(uint8_t o);
|
||||
static const char* chainOpName(uint8_t c);
|
||||
};
|
||||
|
||||
class WoweePlayerConditionLoader {
|
||||
public:
|
||||
static bool save(const WoweePlayerCondition& cat,
|
||||
const std::string& basePath);
|
||||
static WoweePlayerCondition load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-pcn* variants.
|
||||
//
|
||||
// makeStarter — 3 single-check conditions (level
|
||||
// 60+, race Human, class Warrior).
|
||||
// makeQuestGates — 4 quest-style gates (quest X
|
||||
// complete, reputation honored with
|
||||
// faction Y, achievement Z earned,
|
||||
// player in zone W).
|
||||
// makeComposite — 3 chained conditions exercising
|
||||
// AND / OR / NOT chainOps (level 80
|
||||
// AND warrior; ally rep OR horde rep;
|
||||
// NOT in-combat).
|
||||
static WoweePlayerCondition makeStarter(const std::string& catalogName);
|
||||
static WoweePlayerCondition makeQuestGates(const std::string& catalogName);
|
||||
static WoweePlayerCondition makeComposite(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
320
src/pipeline/wowee_player_conditions.cpp
Normal file
320
src/pipeline/wowee_player_conditions.cpp
Normal 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
|
||||
|
|
@ -144,6 +144,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-wsui", "--gen-wsui-wintergrasp", "--gen-wsui-dungeon",
|
||||
"--info-wwui", "--validate-wwui",
|
||||
"--export-wwui-json", "--import-wwui-json",
|
||||
"--gen-pcn", "--gen-pcn-quest-gates", "--gen-pcn-composite",
|
||||
"--info-wpcn", "--validate-wpcn",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
#include "cli_summary_dir.hpp"
|
||||
#include "cli_rename_magic.hpp"
|
||||
#include "cli_world_state_ui_catalog.hpp"
|
||||
#include "cli_player_conditions_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -201,6 +202,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleSummaryDir,
|
||||
handleRenameMagic,
|
||||
handleWorldStateUICatalog,
|
||||
handlePlayerConditionsCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ constexpr FormatMagicEntry kFormats[] = {
|
|||
{{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"},
|
||||
{{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"},
|
||||
{{'W','W','U','I'}, ".wwui", "ui", "--info-wwui", "World-state UI catalog"},
|
||||
{{'W','P','C','N'}, ".wpcn", "logic", "--info-wpcn", "Player condition 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"},
|
||||
|
|
|
|||
|
|
@ -1397,6 +1397,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wwui to a human-editable JSON sidecar (defaults to <base>.wwui.json)\n");
|
||||
std::printf(" --import-wwui-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wwui.json sidecar back into binary .wwui (accepts displayKind/panelPosition int OR name string)\n");
|
||||
std::printf(" --gen-pcn <wpcn-base> [name]\n");
|
||||
std::printf(" Emit .wpcn starter: 3 single-check conditions (level>=60 / race=Human / class=Warrior)\n");
|
||||
std::printf(" --gen-pcn-quest-gates <wpcn-base> [name]\n");
|
||||
std::printf(" Emit .wpcn 4 quest-style gates (quest complete, reputation, achievement, zone presence) with cross-refs\n");
|
||||
std::printf(" --gen-pcn-composite <wpcn-base> [name]\n");
|
||||
std::printf(" Emit .wpcn 6 entries (3 leaves + 3 chained roots) exercising AND/OR/NOT chainOps for boolean trees\n");
|
||||
std::printf(" --info-wpcn <wpcn-base> [--json]\n");
|
||||
std::printf(" Print WPCN entries (id / kind / op / target IDs / int values / chainOp / chainNextId / name)\n");
|
||||
std::printf(" --validate-wpcn <wpcn-base> [--json]\n");
|
||||
std::printf(" Static checks: id>0+unique, name not empty, kind 0..15, op 0..7, chainOp 0..3, chain self-loop, dangling chainNextId warning\n");
|
||||
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ constexpr FormatRow kFormats[] = {
|
|||
{"WANI", ".wani", "anim", "AnimationData.dbc", "Animation ID + fallback + weapon-flag catalog"},
|
||||
{"WSVK", ".wsvk", "spellfx", "SpellVisualKit.dbc + SpellVisFx", "Spell visual kit (cast/proj/impact effects)"},
|
||||
{"WWUI", ".wwui", "ui", "WorldStateUI.dbc + world_state", "World-state UI (BG scoreboards / siege counters)"},
|
||||
{"WPCN", ".wpcn", "logic", "PlayerCondition.dbc + conditions", "Player condition (gates, AND/OR/NOT chains)"},
|
||||
|
||||
// Additional pipeline catalogs without the alternating
|
||||
// gen/info/validate CLI surface (loaded by the engine
|
||||
|
|
|
|||
265
tools/editor/cli_player_conditions_catalog.cpp
Normal file
265
tools/editor/cli_player_conditions_catalog.cpp
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
#include "cli_player_conditions_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_player_conditions.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWpcnExt(std::string base) {
|
||||
stripExt(base, ".wpcn");
|
||||
return base;
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweePlayerCondition& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweePlayerConditionLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wpcn\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweePlayerCondition& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wpcn\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" conditions : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterConditions";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWpcnExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerConditionLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-pcn")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenQuestGates(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "QuestGateConditions";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWpcnExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerConditionLoader::makeQuestGates(name);
|
||||
if (!saveOrError(c, base, "gen-pcn-quest-gates")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenComposite(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "CompositeConditions";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWpcnExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerConditionLoader::makeComposite(name);
|
||||
if (!saveOrError(c, base, "gen-pcn-composite")) 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 = stripWpcnExt(base);
|
||||
if (!wowee::pipeline::WoweePlayerConditionLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WPCN not found: %s.wpcn\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweePlayerConditionLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wpcn"] = base + ".wpcn";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
arr.push_back({
|
||||
{"conditionId", e.conditionId},
|
||||
{"name", e.name},
|
||||
{"description", e.description},
|
||||
{"conditionKind", e.conditionKind},
|
||||
{"conditionKindName", wowee::pipeline::WoweePlayerCondition::conditionKindName(e.conditionKind)},
|
||||
{"comparisonOp", e.comparisonOp},
|
||||
{"comparisonOpName", wowee::pipeline::WoweePlayerCondition::comparisonOpName(e.comparisonOp)},
|
||||
{"chainOp", e.chainOp},
|
||||
{"chainOpName", wowee::pipeline::WoweePlayerCondition::chainOpName(e.chainOp)},
|
||||
{"targetIdA", e.targetIdA},
|
||||
{"targetIdB", e.targetIdB},
|
||||
{"intValueA", e.intValueA},
|
||||
{"intValueB", e.intValueB},
|
||||
{"chainNextId", e.chainNextId},
|
||||
{"failMessage", e.failMessage},
|
||||
});
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WPCN: %s.wpcn\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" conditions : %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
std::printf(" id kind op tgtA tgtB intA intB chain next name\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" %4u %-14s %-10s %4u %4u %5d %5d %-5s %4u %s\n",
|
||||
e.conditionId,
|
||||
wowee::pipeline::WoweePlayerCondition::conditionKindName(e.conditionKind),
|
||||
wowee::pipeline::WoweePlayerCondition::comparisonOpName(e.comparisonOp),
|
||||
e.targetIdA, e.targetIdB,
|
||||
e.intValueA, e.intValueB,
|
||||
wowee::pipeline::WoweePlayerCondition::chainOpName(e.chainOp),
|
||||
e.chainNextId, 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 = stripWpcnExt(base);
|
||||
if (!wowee::pipeline::WoweePlayerConditionLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wpcn: WPCN not found: %s.wpcn\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweePlayerConditionLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> idsSeen;
|
||||
for (const auto& e : c.entries) idsSeen.push_back(e.conditionId);
|
||||
auto idExists = [&](uint32_t id) {
|
||||
for (uint32_t a : idsSeen) if (a == id) return true;
|
||||
return false;
|
||||
};
|
||||
std::vector<uint32_t> dupCheck;
|
||||
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.conditionId);
|
||||
if (!e.name.empty()) ctx += " " + e.name;
|
||||
ctx += ")";
|
||||
if (e.conditionId == 0)
|
||||
errors.push_back(ctx + ": conditionId is 0");
|
||||
if (e.name.empty())
|
||||
errors.push_back(ctx + ": name is empty");
|
||||
if (e.conditionKind > wowee::pipeline::WoweePlayerCondition::Resting) {
|
||||
errors.push_back(ctx + ": conditionKind " +
|
||||
std::to_string(e.conditionKind) + " not in 0..15");
|
||||
}
|
||||
if (e.comparisonOp > wowee::pipeline::WoweePlayerCondition::NotInSet) {
|
||||
errors.push_back(ctx + ": comparisonOp " +
|
||||
std::to_string(e.comparisonOp) + " not in 0..7");
|
||||
}
|
||||
if (e.chainOp > wowee::pipeline::WoweePlayerCondition::ChainNot) {
|
||||
errors.push_back(ctx + ": chainOp " +
|
||||
std::to_string(e.chainOp) + " not in 0..3");
|
||||
}
|
||||
// chainOp != ChainNone requires a non-zero chainNextId
|
||||
// — and that ID must point at another condition in
|
||||
// this catalog.
|
||||
if (e.chainOp != wowee::pipeline::WoweePlayerCondition::ChainNone) {
|
||||
if (e.chainNextId == 0) {
|
||||
errors.push_back(ctx + ": chainOp '" +
|
||||
wowee::pipeline::WoweePlayerCondition::chainOpName(e.chainOp) +
|
||||
"' set but chainNextId=0 (chain has no tail)");
|
||||
} else if (e.chainNextId == e.conditionId) {
|
||||
errors.push_back(ctx +
|
||||
": chainNextId equals conditionId "
|
||||
"(infinite loop)");
|
||||
} else if (!idExists(e.chainNextId)) {
|
||||
warnings.push_back(ctx + ": chainNextId=" +
|
||||
std::to_string(e.chainNextId) +
|
||||
" not found in this catalog (resolved at runtime)");
|
||||
}
|
||||
}
|
||||
// chainOp == ChainNone and chainNextId != 0 is dead
|
||||
// pointer — chainNextId is silently unused.
|
||||
if (e.chainOp == wowee::pipeline::WoweePlayerCondition::ChainNone &&
|
||||
e.chainNextId != 0) {
|
||||
warnings.push_back(ctx +
|
||||
": chainNextId set but chainOp=none "
|
||||
"(silently ignored at runtime)");
|
||||
}
|
||||
// duplicates
|
||||
for (size_t m = 0; m < k; ++m) {
|
||||
if (c.entries[m].conditionId == e.conditionId) {
|
||||
errors.push_back(ctx + ": duplicate conditionId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wpcn"] = base + ".wpcn";
|
||||
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-wpcn: %s.wpcn\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu conditions, all conditionIds unique, all chains resolved\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 handlePlayerConditionsCatalog(int& i, int argc, char** argv,
|
||||
int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-pcn") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-pcn-quest-gates") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenQuestGates(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-pcn-composite") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenComposite(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wpcn") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wpcn") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
12
tools/editor/cli_player_conditions_catalog.hpp
Normal file
12
tools/editor/cli_player_conditions_catalog.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handlePlayerConditionsCatalog(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