diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e790ef1..e6f94d77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/pipeline/wowee_player_conditions.hpp b/include/pipeline/wowee_player_conditions.hpp new file mode 100644 index 00000000..936b559c --- /dev/null +++ b/include/pipeline/wowee_player_conditions.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include + +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 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 diff --git a/src/pipeline/wowee_player_conditions.cpp b/src/pipeline/wowee_player_conditions.cpp new file mode 100644 index 00000000..22976774 --- /dev/null +++ b/src/pipeline/wowee_player_conditions.cpp @@ -0,0 +1,320 @@ +#include "pipeline/wowee_player_conditions.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'P', 'C', 'N'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(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(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(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 diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8476c25c..a0fac6b3 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -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", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index e5323f04..2299d225 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -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, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 66f303fe..e2e03772 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -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"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index b68c60f8..82ee1e3a 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1397,6 +1397,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wwui to a human-editable JSON sidecar (defaults to .wwui.json)\n"); std::printf(" --import-wwui-json [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 [name]\n"); + std::printf(" Emit .wpcn starter: 3 single-check conditions (level>=60 / race=Human / class=Warrior)\n"); + std::printf(" --gen-pcn-quest-gates [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 [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 [--json]\n"); + std::printf(" Print WPCN entries (id / kind / op / target IDs / int values / chainOp / chainNextId / name)\n"); + std::printf(" --validate-wpcn [--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 [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 10dc91b0..e35f08b2 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -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 diff --git a/tools/editor/cli_player_conditions_catalog.cpp b/tools/editor/cli_player_conditions_catalog.cpp new file mode 100644 index 00000000..cdfa683f --- /dev/null +++ b/tools/editor/cli_player_conditions_catalog.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +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 errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector 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 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 diff --git a/tools/editor/cli_player_conditions_catalog.hpp b/tools/editor/cli_player_conditions_catalog.hpp new file mode 100644 index 00000000..8ed15261 --- /dev/null +++ b/tools/editor/cli_player_conditions_catalog.hpp @@ -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