From 4868f780ccda9967203b39f44fb4796a3346c90c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 15:37:59 -0700 Subject: [PATCH] feat(pipeline): add WFAC (Wowee Faction Catalog) format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for Blizzard's Faction.dbc + FactionTemplate.dbc + the AzerothCore-style reputation_reward / reputation_spillover SQL tables. The 17th open format added to the editor. Combines the "displayable Faction" (player-facing name + reputation thresholds for friendly/honored/revered/exalted) with the "FactionTemplate matrix" (which factions are hostile to which) into one entry. The runtime walks the catalog to answer two questions: • "Will faction A attack faction B on sight?" -> enemy list • "What rep tier is the player with X?" -> thresholds Cross-references with previously-added formats: WCRT.entry.factionId -> WFAC.entry.factionId WFAC.entry.parentFactionId -> WFAC.entry.factionId WFAC.entry.enemies[] -> WFAC.entry.factionId WFAC.entry.friends[] -> WFAC.entry.factionId The starter preset's factionId 35 (Friendly) and 14 (Hostile) deliberately match the WCRT preset defaults, so the demo content stack is consistent: WCRT.makeBandit's factionId=14 has a real entry in WFAC.makeStarter that declares it hostile to friendly NPCs (35) and players (1). Format: • magic "WFAC", version 1, little-endian • per faction: factionId / parentFactionId / name / description / reputationFlags / baseReputation / 7 ascending tier thresholds (hostile..exalted) / enemies[] / friends[] Enums: • ReputationFlags: VisibleOnTab / AtWarDefault / Hidden / NoReputation / IsHeader (group label) • Tier (canonical): Hated / Hostile / Unfriendly / Neutral / Friendly / Honored / Revered / Exalted API: WoweeFactionLoader::save / load / exists / findById + WoweeFaction::isHostile(a, b); presets makeStarter (3-faction demo matching WCRT defaults), makeAlliance (header + Stormwind / Darnassus / Ironforge with reciprocal friend lists + Defias enemy), makeWildlife (4 beast factions, each hostile to player but ignoring other beasts). CLI added (5 flags, 514 documented total now): --gen-factions / --gen-factions-alliance / --gen-factions-wildlife --info-wfac / --validate-wfac Validator catches: factionId=0 + duplicates, empty name, threshold ordering violations (hostile must be < unfriendly < neutral < ... < exalted), self-listed as enemy or friend, faction in both enemies and friends (incoherent). --- CMakeLists.txt | 3 + include/pipeline/wowee_factions.hpp | 130 ++++++++++++ src/pipeline/wowee_factions.cpp | 274 ++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_factions_catalog.cpp | 268 +++++++++++++++++++++++++ tools/editor/cli_factions_catalog.hpp | 11 ++ tools/editor/cli_help.cpp | 10 + 8 files changed, 700 insertions(+) create mode 100644 include/pipeline/wowee_factions.hpp create mode 100644 src/pipeline/wowee_factions.cpp create mode 100644 tools/editor/cli_factions_catalog.cpp create mode 100644 tools/editor/cli_factions_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 83150d1f..0aa969d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -604,6 +604,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_creatures.cpp src/pipeline/wowee_quests.cpp src/pipeline/wowee_objects.cpp + src/pipeline/wowee_factions.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1354,6 +1355,7 @@ add_executable(wowee_editor tools/editor/cli_creatures_catalog.cpp tools/editor/cli_quests_catalog.cpp tools/editor/cli_objects_catalog.cpp + tools/editor/cli_factions_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1436,6 +1438,7 @@ add_executable(wowee_editor src/pipeline/wowee_creatures.cpp src/pipeline/wowee_quests.cpp src/pipeline/wowee_objects.cpp + src/pipeline/wowee_factions.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_factions.hpp b/include/pipeline/wowee_factions.hpp new file mode 100644 index 00000000..013c005a --- /dev/null +++ b/include/pipeline/wowee_factions.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Faction Catalog (.wfac) — novel replacement for +// Blizzard's Faction.dbc + FactionTemplate.dbc + the +// AzerothCore-style reputation_reward / reputation_spillover +// SQL tables. The 17th open format added to the editor. +// +// Combines the "displayable Faction" (player-facing name, +// reputation thresholds for friendly/honored/revered/exalted) +// with the "FactionTemplate matrix" (which factions are +// hostile to which) into one entry. The runtime walks the +// catalog to answer two questions: +// • "Will faction A attack faction B on sight?" -> enemy list +// • "What reputation tier is this player with X?" -> thresholds +// +// Cross-references: +// WCRT.entry.factionId -> WFAC.entry.factionId +// WFAC.entry.parentFactionId -> WFAC.entry.factionId +// (intra-format hierarchy) +// WFAC.entry.enemies[] -> WFAC.entry.factionId +// WFAC.entry.friends[] -> WFAC.entry.factionId +// +// Binary layout (little-endian): +// magic[4] = "WFAC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// factionId (uint32) +// parentFactionId (uint32) +// nameLen + name +// descLen + description +// reputationFlags (uint32) +// baseReputation (int32) +// thresholdHostile (int32) +// thresholdUnfriendly (int32) +// thresholdNeutral (int32) +// thresholdFriendly (int32) +// thresholdHonored (int32) +// thresholdRevered (int32) +// thresholdExalted (int32) +// enemyCount (uint8) + pad[3] + enemies[] (factionId × N) +// friendCount (uint8) + pad[3] + friends[] (factionId × N) +struct WoweeFaction { + enum ReputationFlags : uint32_t { + VisibleOnTab = 0x01, // shows up in the player's reputation panel + AtWarDefault = 0x02, // new players are at-war on first contact + Hidden = 0x04, // never shown (internal reputation tracking) + NoReputation = 0x08, // gives no reputation gains/losses + IsHeader = 0x10, // grouping header (parent), not a faction itself + }; + + // Canonical reputation tiers — clients use the threshold + // values to decide which tier badge to display. + enum Tier : int32_t { + Hated = -42000, + Hostile = -6000, + Unfriendly = -3000, + Neutral = 0, + Friendly = 3000, + Honored = 9000, + Revered = 21000, + Exalted = 42000, + }; + + struct Entry { + uint32_t factionId = 0; + uint32_t parentFactionId = 0; + std::string name; + std::string description; + uint32_t reputationFlags = VisibleOnTab; + int32_t baseReputation = 0; + int32_t thresholdHostile = Hostile; + int32_t thresholdUnfriendly = Unfriendly; + int32_t thresholdNeutral = Neutral; + int32_t thresholdFriendly = Friendly; + int32_t thresholdHonored = Honored; + int32_t thresholdRevered = Revered; + int32_t thresholdExalted = Exalted; + std::vector enemies; + std::vector friends; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by factionId — nullptr if not present. + const Entry* findById(uint32_t factionId) const; + + // True if A's enemies list contains B (hostile-on-sight). + // Does NOT walk parent factions; use isAtWarTransitive + // for that. + bool isHostile(uint32_t aFactionId, uint32_t bFactionId) const; +}; + +class WoweeFactionLoader { +public: + static bool save(const WoweeFaction& cat, + const std::string& basePath); + static WoweeFaction load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-factions* variants. + // + // makeStarter — 3 factions: Friendly (35), Hostile (14), + // PlayerHorde (1). Friendly is enemies of + // Hostile, vice versa. Useful as a starter + // template. + // makeAlliance — Stormwind / Ironforge / Darnassus (with + // reciprocal friend lists) + the canonical + // Defias enemy. + // makeWildlife — neutral wildlife factions: wolves, bears, + // spiders, kobolds. Each hostile to players + // but not to each other. + static WoweeFaction makeStarter(const std::string& catalogName); + static WoweeFaction makeAlliance(const std::string& catalogName); + static WoweeFaction makeWildlife(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_factions.cpp b/src/pipeline/wowee_factions.cpp new file mode 100644 index 00000000..497735ab --- /dev/null +++ b/src/pipeline/wowee_factions.cpp @@ -0,0 +1,274 @@ +#include "pipeline/wowee_factions.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'F', 'A', 'C'}; +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) != ".wfac") { + base += ".wfac"; + } + return base; +} + +} // namespace + +const WoweeFaction::Entry* WoweeFaction::findById(uint32_t factionId) const { + for (const auto& e : entries) { + if (e.factionId == factionId) return &e; + } + return nullptr; +} + +bool WoweeFaction::isHostile(uint32_t aFactionId, uint32_t bFactionId) const { + const Entry* a = findById(aFactionId); + if (!a) return false; + for (uint32_t e : a->enemies) { + if (e == bFactionId) return true; + } + return false; +} + +bool WoweeFactionLoader::save(const WoweeFaction& 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.factionId); + writePOD(os, e.parentFactionId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.reputationFlags); + writePOD(os, e.baseReputation); + writePOD(os, e.thresholdHostile); + writePOD(os, e.thresholdUnfriendly); + writePOD(os, e.thresholdNeutral); + writePOD(os, e.thresholdFriendly); + writePOD(os, e.thresholdHonored); + writePOD(os, e.thresholdRevered); + writePOD(os, e.thresholdExalted); + uint8_t enCount = static_cast( + e.enemies.size() > 255 ? 255 : e.enemies.size()); + writePOD(os, enCount); + uint8_t pad[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad), 3); + for (uint8_t k = 0; k < enCount; ++k) writePOD(os, e.enemies[k]); + uint8_t frCount = static_cast( + e.friends.size() > 255 ? 255 : e.friends.size()); + writePOD(os, frCount); + os.write(reinterpret_cast(pad), 3); + for (uint8_t k = 0; k < frCount; ++k) writePOD(os, e.friends[k]); + } + return os.good(); +} + +WoweeFaction WoweeFactionLoader::load(const std::string& basePath) { + WoweeFaction 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.factionId) || + !readPOD(is, e.parentFactionId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.reputationFlags) || + !readPOD(is, e.baseReputation) || + !readPOD(is, e.thresholdHostile) || + !readPOD(is, e.thresholdUnfriendly) || + !readPOD(is, e.thresholdNeutral) || + !readPOD(is, e.thresholdFriendly) || + !readPOD(is, e.thresholdHonored) || + !readPOD(is, e.thresholdRevered) || + !readPOD(is, e.thresholdExalted)) { + out.entries.clear(); return out; + } + uint8_t enCount = 0; + if (!readPOD(is, enCount)) { out.entries.clear(); return out; } + uint8_t pad[3]; + is.read(reinterpret_cast(pad), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + e.enemies.resize(enCount); + for (uint8_t k = 0; k < enCount; ++k) { + if (!readPOD(is, e.enemies[k])) { + out.entries.clear(); return out; + } + } + uint8_t frCount = 0; + if (!readPOD(is, frCount)) { out.entries.clear(); return out; } + is.read(reinterpret_cast(pad), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + e.friends.resize(frCount); + for (uint8_t k = 0; k < frCount; ++k) { + if (!readPOD(is, e.friends[k])) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeFactionLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeFaction WoweeFactionLoader::makeStarter(const std::string& catalogName) { + WoweeFaction c; + c.name = catalogName; + { + // factionId 35 is canonical "friendly to all" used by + // WCRT.makeStarter and WCRT.makeMerchants. + WoweeFaction::Entry e; + e.factionId = 35; e.name = "Friendly"; + e.description = "Friendly to all players. Standard NPCs."; + e.enemies.push_back(14); + c.entries.push_back(e); + } + { + // factionId 14 is canonical "hostile to all" used by + // WCRT.makeBandit. + WoweeFaction::Entry e; + e.factionId = 14; e.name = "Hostile"; + e.description = "Hostile on sight. Bandits / monsters."; + e.reputationFlags = WoweeFaction::AtWarDefault; + e.enemies.push_back(35); + e.enemies.push_back(1); + c.entries.push_back(e); + } + { + WoweeFaction::Entry e; + e.factionId = 1; e.name = "Player Faction"; + e.description = "The player's own faction; never hostile to self."; + e.enemies.push_back(14); + c.entries.push_back(e); + } + return c; +} + +WoweeFaction WoweeFactionLoader::makeAlliance(const std::string& catalogName) { + WoweeFaction c; + c.name = catalogName; + auto add = [&](uint32_t id, uint32_t parent, const char* name, + const char* desc, std::vector enemies, + std::vector friends_) { + WoweeFaction::Entry e; + e.factionId = id; e.parentFactionId = parent; + e.name = name; e.description = desc; + e.reputationFlags = WoweeFaction::VisibleOnTab; + e.enemies = std::move(enemies); + e.friends = std::move(friends_); + c.entries.push_back(e); + }; + // Header (parent grouping in the player reputation panel). + { + WoweeFaction::Entry e; + e.factionId = 1000; e.name = "Alliance"; + e.description = "The Alliance, united."; + e.reputationFlags = WoweeFaction::IsHeader; + c.entries.push_back(e); + } + add(72, 1000, "Stormwind", + "City of Stormwind. Capital of the human kingdom.", + {349}, // enemy: Defias + {69, 54}); // friends: Darnassus, Ironforge + add(69, 1000, "Darnassus", + "Night elf city in the world tree.", + {349}, {72, 54}); + add(54, 1000, "Ironforge", + "Dwarven mountain city.", + {349}, {72, 69}); + add(349, 0, "Defias Brotherhood", + "Outlaw conspiracy in Westfall.", + {72, 69, 54}, {}); + return c; +} + +WoweeFaction WoweeFactionLoader::makeWildlife(const std::string& catalogName) { + WoweeFaction c; + c.name = catalogName; + auto addBeast = [&](uint32_t id, const char* name) { + WoweeFaction::Entry e; + e.factionId = id; e.name = name; + e.reputationFlags = WoweeFaction::Hidden; + // Beasts are hostile to player factions (1) but not + // to each other — the wildlife of a zone fights the + // player but won't pull adjacent packs. + e.enemies.push_back(1); + c.entries.push_back(e); + }; + addBeast(2001, "Wolves"); + addBeast(2002, "Bears"); + addBeast(2003, "Spiders"); + { + WoweeFaction::Entry e; + e.factionId = 2010; e.name = "Kobolds"; + e.description = "Cave-dwelling humanoids; mob in groups."; + e.reputationFlags = WoweeFaction::AtWarDefault; + e.enemies.push_back(1); + // Kobolds and wolves coexist (both feral hostile, but + // different ecology niches). + c.entries.push_back(e); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index a0c7f96a..ea604f07 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -46,6 +46,8 @@ const char* const kArgRequired[] = { "--export-wqt-json", "--import-wqt-json", "--gen-objects", "--gen-objects-dungeon", "--gen-objects-gather", "--info-wgot", "--validate-wgot", + "--gen-factions", "--gen-factions-alliance", "--gen-factions-wildlife", + "--info-wfac", "--validate-wfac", "--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 a0bb0d3e..23399526 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -44,6 +44,7 @@ #include "cli_creatures_catalog.hpp" #include "cli_quests_catalog.hpp" #include "cli_objects_catalog.hpp" +#include "cli_factions_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -129,6 +130,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCreaturesCatalog, handleQuestsCatalog, handleObjectsCatalog, + handleFactionsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_factions_catalog.cpp b/tools/editor/cli_factions_catalog.cpp new file mode 100644 index 00000000..b609ac40 --- /dev/null +++ b/tools/editor/cli_factions_catalog.cpp @@ -0,0 +1,268 @@ +#include "cli_factions_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_factions.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWfacExt(std::string base) { + stripExt(base, ".wfac"); + return base; +} + +void appendRepFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeFaction::VisibleOnTab) s += "visible "; + if (flags & wowee::pipeline::WoweeFaction::AtWarDefault) s += "at-war "; + if (flags & wowee::pipeline::WoweeFaction::Hidden) s += "hidden "; + if (flags & wowee::pipeline::WoweeFaction::NoReputation) s += "no-rep "; + if (flags & wowee::pipeline::WoweeFaction::IsHeader) s += "header "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +bool saveOrError(const wowee::pipeline::WoweeFaction& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeFactionLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wfac\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeFaction& c, + const std::string& base) { + std::printf("Wrote %s.wfac\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" factions : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterFactions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWfacExt(base); + auto c = wowee::pipeline::WoweeFactionLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-factions")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAlliance(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "AllianceFactions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWfacExt(base); + auto c = wowee::pipeline::WoweeFactionLoader::makeAlliance(name); + if (!saveOrError(c, base, "gen-factions-alliance")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWildlife(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WildlifeFactions"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWfacExt(base); + auto c = wowee::pipeline::WoweeFactionLoader::makeWildlife(name); + if (!saveOrError(c, base, "gen-factions-wildlife")) 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 = stripWfacExt(base); + if (!wowee::pipeline::WoweeFactionLoader::exists(base)) { + std::fprintf(stderr, "WFAC not found: %s.wfac\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeFactionLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wfac"] = base + ".wfac"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string fs; + appendRepFlagsStr(fs, e.reputationFlags); + nlohmann::json je; + je["factionId"] = e.factionId; + je["parentFactionId"] = e.parentFactionId; + je["name"] = e.name; + je["description"] = e.description; + je["reputationFlags"] = e.reputationFlags; + je["reputationFlagsStr"] = fs; + je["baseReputation"] = e.baseReputation; + je["thresholdHostile"] = e.thresholdHostile; + je["thresholdUnfriendly"] = e.thresholdUnfriendly; + je["thresholdNeutral"] = e.thresholdNeutral; + je["thresholdFriendly"] = e.thresholdFriendly; + je["thresholdHonored"] = e.thresholdHonored; + je["thresholdRevered"] = e.thresholdRevered; + je["thresholdExalted"] = e.thresholdExalted; + je["enemies"] = e.enemies; + je["friends"] = e.friends; + arr.push_back(je); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WFAC: %s.wfac\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" factions : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id parent flags enemies friends name\n"); + for (const auto& e : c.entries) { + std::string fs; + appendRepFlagsStr(fs, e.reputationFlags); + std::printf(" %4u %4u %-12s %2zu %2zu %s\n", + e.factionId, e.parentFactionId, fs.c_str(), + e.enemies.size(), e.friends.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 = stripWfacExt(base); + if (!wowee::pipeline::WoweeFactionLoader::exists(base)) { + std::fprintf(stderr, + "validate-wfac: WFAC not found: %s.wfac\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeFactionLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + idsSeen.reserve(c.entries.size()); + 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.factionId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.factionId == 0) { + errors.push_back(ctx + ": factionId is 0"); + } + if (e.name.empty()) { + errors.push_back(ctx + ": name is empty"); + } + // Threshold ordering: hostile < unfriendly < neutral < + // friendly < honored < revered < exalted. + if (e.thresholdHostile >= e.thresholdUnfriendly || + e.thresholdUnfriendly >= e.thresholdNeutral || + e.thresholdNeutral >= e.thresholdFriendly || + e.thresholdFriendly >= e.thresholdHonored || + e.thresholdHonored >= e.thresholdRevered || + e.thresholdRevered >= e.thresholdExalted) { + errors.push_back(ctx + + ": reputation thresholds not strictly ascending " + "(hostile [--json]\n"); std::printf(" Static checks: objectId>0+unique, size>0, time min<=max, gathering needs skill, chest warns on no loot\n"); + std::printf(" --gen-factions [name]\n"); + std::printf(" Emit .wfac starter: 3 factions (Friendly id=35 / Hostile id=14 / Player id=1) matching WCRT defaults\n"); + std::printf(" --gen-factions-alliance [name]\n"); + std::printf(" Emit .wfac Alliance set: header + Stormwind + Darnassus + Ironforge (reciprocal friends) + Defias enemy\n"); + std::printf(" --gen-factions-wildlife [name]\n"); + std::printf(" Emit .wfac wildlife: wolves + bears + spiders + kobolds (each hostile to player, ignores other beasts)\n"); + std::printf(" --info-wfac [--json]\n"); + std::printf(" Print WFAC entries (id / parent / flags / enemy + friend counts / name)\n"); + std::printf(" --validate-wfac [--json]\n"); + std::printf(" Static checks: factionId>0+unique, name not empty, threshold ordering, no self-enemy, no enemy/friend overlap\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");