From 94e145541aea395c9b7a7d09611d2ad43edeef80 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 22:43:44 -0700 Subject: [PATCH] feat(editor): add WACR (Achievement Criteria) open catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open replacement for Blizzard's Achievement_Criteria.dbc. Defines the individual progression criteria that a character must complete to earn an achievement. Each WACH achievement has a tree of WACR criteria — "Kill 100 boars" is one criteria entry with criteriaType=KillCreature, targetId=boarCreatureId, requiredCount=100. Multi-criteria achievements (e.g. "Visit all 3 capital cities") have one entry per sub-objective, all referencing the same achievementId, with progressOrder determining their display sequence in the achievement UI. Thirteen criteriaType values cover the full progression variety: KillCreature / ReachLevel / CompleteQuest / EarnGold / GainHonor / EarnReputation / ExploreZone / LootItem / UseItem / CastSpell / PvPKill / DungeonRun / Misc The targetId field is type-polymorphic — for KillCreature it references WCRT.creatureId, for CompleteQuest it references WQT.questId, for ExploreZone it's a WMS.zoneId, etc. The engine interprets it based on criteriaType. Cross-references back to WACH (achievementId), WCRT (KillCreature.targetId), WQT (CompleteQuest.targetId), WIT (LootItem/UseItem.targetId), WMS (ExploreZone.targetId), WSPL (CastSpell.targetId). findByAchievement(achId) returns all criteria for an achievement sorted by progressOrder — used directly by the achievement UI to render the progress checklist. Three preset emitters: --gen-acr (5 kill criteria under one composite achievement showing different creature targets), --gen-acr-quest (4-step quest progression), --gen-acr-mixed (5 cross-type criteria demonstrating the full CriteriaType variety). Validation enforces id+name+achievementId presence, criteriaType 0..12, no duplicate ids; warns on: - missing targetId for type-specific kinds (KillCreature, CompleteQuest, etc. — engine cannot track without it) - ReachLevel with requiredCount > 80 (above WotLK cap) - timeLimitMs set on non-time-sensitive types (engine ignores it for ReachLevel / EarnGold) - requiredCount=0 (criteria completes instantly on first progress event — usually a misconfig) Wired through the cross-format table; WACR appears automatically in all 14 cross-format utilities. Format count 80 -> 81; CLI flag count 981 -> 986. --- CMakeLists.txt | 3 + .../pipeline/wowee_achievement_criteria.hpp | 124 ++++++++ src/pipeline/wowee_achievement_criteria.cpp | 274 ++++++++++++++++++ .../cli_achievement_criteria_catalog.cpp | 266 +++++++++++++++++ .../cli_achievement_criteria_catalog.hpp | 12 + tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + 10 files changed, 695 insertions(+) create mode 100644 include/pipeline/wowee_achievement_criteria.hpp create mode 100644 src/pipeline/wowee_achievement_criteria.cpp create mode 100644 tools/editor/cli_achievement_criteria_catalog.cpp create mode 100644 tools/editor/cli_achievement_criteria_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d80d01e1..b39995fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -669,6 +669,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_talent_tabs.cpp src/pipeline/wowee_currency_types.cpp src/pipeline/wowee_spell_reagents.cpp + src/pipeline/wowee_achievement_criteria.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1497,6 +1498,7 @@ add_executable(wowee_editor tools/editor/cli_talent_tabs_catalog.cpp tools/editor/cli_currency_types_catalog.cpp tools/editor/cli_spell_reagents_catalog.cpp + tools/editor/cli_achievement_criteria_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1644,6 +1646,7 @@ add_executable(wowee_editor src/pipeline/wowee_talent_tabs.cpp src/pipeline/wowee_currency_types.cpp src/pipeline/wowee_spell_reagents.cpp + src/pipeline/wowee_achievement_criteria.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_achievement_criteria.hpp b/include/pipeline/wowee_achievement_criteria.hpp new file mode 100644 index 00000000..7fe286ae --- /dev/null +++ b/include/pipeline/wowee_achievement_criteria.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Achievement Criteria catalog (.wacr) — +// novel replacement for Blizzard's Achievement_Criteria.dbc. +// Defines the individual progression criteria that a +// character must complete to earn an achievement. +// +// Each WACH achievement has a tree of WACR criteria — +// "Kill 100 boars" is one criteria entry with +// criteriaType=KillCreature, targetId=boarCreatureId, +// requiredCount=100. Multi-criteria achievements +// (e.g. "Visit all 3 capital cities") have one entry +// per sub-objective, all referencing the same +// achievementId, with progressOrder determining their +// display sequence in the achievement UI. +// +// Cross-references with previously-added formats: +// WACH: achievementId references the WACH parent. +// WCRT: targetId references WCRT.creatureId for +// KillCreature criteria. +// WQT: targetId references WQT.questId for +// CompleteQuest criteria. +// WIT: targetId references WIT.itemId for LootItem / +// UseItem criteria. +// WMS: targetId references WMS.zoneId for ExploreZone +// criteria. +// +// Binary layout (little-endian): +// magic[4] = "WACR" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// criteriaId (uint32) +// nameLen + name +// descLen + description +// achievementId (uint32) +// targetId (uint32) +// requiredCount (uint32) +// timeLimitMs (uint32) +// criteriaType (uint8) / progressOrder (uint8) / pad[2] +// iconColorRGBA (uint32) +struct WoweeAchievementCriteria { + enum CriteriaType : uint8_t { + KillCreature = 0, // count creature kills (targetId=creature) + ReachLevel = 1, // hit a character level (requiredCount=lvl) + CompleteQuest = 2, // finish a specific quest (targetId=quest) + EarnGold = 3, // accumulate copper (requiredCount=copper) + GainHonor = 4, // earn honor (requiredCount=honor) + EarnReputation = 5, // hit rep level with faction (targetId=faction) + ExploreZone = 6, // discover all subzones (targetId=zone) + LootItem = 7, // loot a specific item (targetId=item) + UseItem = 8, // use a specific item (targetId=item) + CastSpell = 9, // cast a specific spell (targetId=spell) + PvPKill = 10, // kill players (requiredCount=count) + DungeonRun = 11, // complete a dungeon (targetId=instance) + Misc = 12, // engine-defined custom progression + }; + + struct Entry { + uint32_t criteriaId = 0; + std::string name; + std::string description; + uint32_t achievementId = 0; + uint32_t targetId = 0; + uint32_t requiredCount = 1; + uint32_t timeLimitMs = 0; + uint8_t criteriaType = KillCreature; + uint8_t progressOrder = 0; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t criteriaId) const; + + // Return all criteria for the given achievement, sorted + // by progressOrder. The achievement UI uses this to + // show the progress checklist. + std::vector findByAchievement( + uint32_t achievementId) const; + + static const char* criteriaTypeName(uint8_t k); +}; + +class WoweeAchievementCriteriaLoader { +public: + static bool save(const WoweeAchievementCriteria& cat, + const std::string& basePath); + static WoweeAchievementCriteria load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-acr* variants. + // + // makeKill — 5 kill-counting criteria (Defias 50, + // Murloc 25, Naga 100, Dragon 1, + // RareElite 1) for a "Kill 'Em All" + // style achievement. + // makeQuest — 4 quest-completion criteria covering + // tutorial/zone/daily/escort progression + // in one composite achievement. + // makeMixed — 5 cross-type criteria (ReachLevel 80, + // EarnGold 10000, GainHonor 5000, PvPKill + // 100, ExploreZone Stormwind) showing + // the variety of CriteriaType values. + static WoweeAchievementCriteria makeKill(const std::string& catalogName); + static WoweeAchievementCriteria makeQuest(const std::string& catalogName); + static WoweeAchievementCriteria makeMixed(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_achievement_criteria.cpp b/src/pipeline/wowee_achievement_criteria.cpp new file mode 100644 index 00000000..a3286613 --- /dev/null +++ b/src/pipeline/wowee_achievement_criteria.cpp @@ -0,0 +1,274 @@ +#include "pipeline/wowee_achievement_criteria.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'A', 'C', 'R'}; +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) != ".wacr") { + base += ".wacr"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeAchievementCriteria::Entry* +WoweeAchievementCriteria::findById(uint32_t criteriaId) const { + for (const auto& e : entries) + if (e.criteriaId == criteriaId) return &e; + return nullptr; +} + +std::vector +WoweeAchievementCriteria::findByAchievement( + uint32_t achievementId) const { + std::vector out; + for (const auto& e : entries) { + if (e.achievementId == achievementId) out.push_back(&e); + } + std::sort(out.begin(), out.end(), + [](const Entry* a, const Entry* b) { + return a->progressOrder < b->progressOrder; + }); + return out; +} + +const char* WoweeAchievementCriteria::criteriaTypeName(uint8_t k) { + switch (k) { + case KillCreature: return "kill-creature"; + case ReachLevel: return "reach-level"; + case CompleteQuest: return "complete-quest"; + case EarnGold: return "earn-gold"; + case GainHonor: return "gain-honor"; + case EarnReputation: return "earn-reputation"; + case ExploreZone: return "explore-zone"; + case LootItem: return "loot-item"; + case UseItem: return "use-item"; + case CastSpell: return "cast-spell"; + case PvPKill: return "pvp-kill"; + case DungeonRun: return "dungeon-run"; + case Misc: return "misc"; + default: return "unknown"; + } +} + +bool WoweeAchievementCriteriaLoader::save( + const WoweeAchievementCriteria& 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.criteriaId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.achievementId); + writePOD(os, e.targetId); + writePOD(os, e.requiredCount); + writePOD(os, e.timeLimitMs); + writePOD(os, e.criteriaType); + writePOD(os, e.progressOrder); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeAchievementCriteria WoweeAchievementCriteriaLoader::load( + const std::string& basePath) { + WoweeAchievementCriteria 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.criteriaId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.achievementId) || + !readPOD(is, e.targetId) || + !readPOD(is, e.requiredCount) || + !readPOD(is, e.timeLimitMs) || + !readPOD(is, e.criteriaType) || + !readPOD(is, e.progressOrder) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeAchievementCriteriaLoader::exists( + const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeAchievementCriteria WoweeAchievementCriteriaLoader::makeKill( + const std::string& catalogName) { + using A = WoweeAchievementCriteria; + WoweeAchievementCriteria c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t ach, + uint32_t creatureId, uint32_t count, + uint8_t order, const char* desc) { + A::Entry e; + e.criteriaId = id; e.name = name; e.description = desc; + e.achievementId = ach; + e.targetId = creatureId; + e.requiredCount = count; + e.criteriaType = A::KillCreature; + e.progressOrder = order; + e.iconColorRGBA = packRgba(220, 80, 80); // kill red + c.entries.push_back(e); + }; + // Five kill criteria all under one composite + // achievement (achievementId 5000) — slay diverse + // enemies for "Kill 'Em All". + add(1, "DefiasKills", 5000, 448, 50, 0, + "Slay 50 Defias bandits in Westfall."); + add(2, "MurlocKills", 5000, 346, 25, 1, + "Slay 25 murlocs anywhere."); + add(3, "NagaKills", 5000, 4356, 100, 2, + "Slay 100 naga in Azshara or Maraudon."); + add(4, "DragonKills", 5000, 6109, 1, 3, + "Slay 1 dragon (Onyxia / Nefarian / Ysondre / etc)."); + add(5, "RareEliteKills", 5000, 7846, 1, 4, + "Slay 1 rare elite mob (silver dragon nameplate)."); + return c; +} + +WoweeAchievementCriteria WoweeAchievementCriteriaLoader::makeQuest( + const std::string& catalogName) { + using A = WoweeAchievementCriteria; + WoweeAchievementCriteria c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t ach, + uint32_t questId, uint8_t order, + const char* desc) { + A::Entry e; + e.criteriaId = id; e.name = name; e.description = desc; + e.achievementId = ach; + e.targetId = questId; + e.requiredCount = 1; + e.criteriaType = A::CompleteQuest; + e.progressOrder = order; + e.iconColorRGBA = packRgba(220, 200, 100); // quest gold + c.entries.push_back(e); + }; + // 4-step quest progression under achievement 5100. + add(100, "FinishTutorial", 5100, 1, 0, + "Complete the starting-area tutorial chain."); + add(101, "FinishStartingZone", 5100, 24, 1, + "Complete every quest in the level-1 starting zone."); + add(102, "FinishDaily", 5100, 12013, 2, + "Complete a daily quest."); + add(103, "FinishEscort", 5100, 123, 3, + "Complete an escort quest of any kind."); + return c; +} + +WoweeAchievementCriteria WoweeAchievementCriteriaLoader::makeMixed( + const std::string& catalogName) { + using A = WoweeAchievementCriteria; + WoweeAchievementCriteria c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t ach, + uint8_t kind, uint32_t target, uint32_t count, + uint8_t order, + uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + A::Entry e; + e.criteriaId = id; e.name = name; e.description = desc; + e.achievementId = ach; + e.targetId = target; + e.requiredCount = count; + e.criteriaType = kind; + e.progressOrder = order; + e.iconColorRGBA = packRgba(r, g, b); + c.entries.push_back(e); + }; + // Five different criteria types under achievement 5200 + // — demonstrate the full CriteriaType variety. + add(200, "ReachLevel80", 5200, A::ReachLevel, 0, 80, 0, + 100, 240, 100, "Reach level 80."); + add(201, "EarnGold10k", 5200, A::EarnGold, 0, 100000000, 1, + 220, 200, 100, "Accumulate 10000 gold (100M copper)."); + add(202, "GainHonor5k", 5200, A::GainHonor, 0, 5000, 2, + 220, 80, 80, "Earn 5000 honor points."); + add(203, "PvPKill100", 5200, A::PvPKill, 0, 100, 3, + 180, 100, 240, "Kill 100 enemy players in PvP."); + add(204, "ExploreStormwind",5200,A::ExploreZone, 1519, 1, 4, + 100, 140, 240, "Discover every subzone in Stormwind."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_achievement_criteria_catalog.cpp b/tools/editor/cli_achievement_criteria_catalog.cpp new file mode 100644 index 00000000..b2e52354 --- /dev/null +++ b/tools/editor/cli_achievement_criteria_catalog.cpp @@ -0,0 +1,266 @@ +#include "cli_achievement_criteria_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_achievement_criteria.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWacrExt(std::string base) { + stripExt(base, ".wacr"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeAchievementCriteria& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeAchievementCriteriaLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wacr\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeAchievementCriteria& c, + const std::string& base) { + std::printf("Wrote %s.wacr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" criteria : %zu\n", c.entries.size()); +} + +int handleGenKill(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "KillCriteria"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWacrExt(base); + auto c = wowee::pipeline::WoweeAchievementCriteriaLoader::makeKill(name); + if (!saveOrError(c, base, "gen-acr")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenQuest(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "QuestCriteria"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWacrExt(base); + auto c = wowee::pipeline::WoweeAchievementCriteriaLoader::makeQuest(name); + if (!saveOrError(c, base, "gen-acr-quest")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMixed(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MixedCriteria"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWacrExt(base); + auto c = wowee::pipeline::WoweeAchievementCriteriaLoader::makeMixed(name); + if (!saveOrError(c, base, "gen-acr-mixed")) 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 = stripWacrExt(base); + if (!wowee::pipeline::WoweeAchievementCriteriaLoader::exists(base)) { + std::fprintf(stderr, "WACR not found: %s.wacr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAchievementCriteriaLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wacr"] = base + ".wacr"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"criteriaId", e.criteriaId}, + {"name", e.name}, + {"description", e.description}, + {"achievementId", e.achievementId}, + {"targetId", e.targetId}, + {"requiredCount", e.requiredCount}, + {"timeLimitMs", e.timeLimitMs}, + {"criteriaType", e.criteriaType}, + {"criteriaTypeName", wowee::pipeline::WoweeAchievementCriteria::criteriaTypeName(e.criteriaType)}, + {"progressOrder", e.progressOrder}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WACR: %s.wacr\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" criteria : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id ach type targetId count timeMs ord name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %-15s %8u %5u %5u %u %s\n", + e.criteriaId, e.achievementId, + wowee::pipeline::WoweeAchievementCriteria::criteriaTypeName(e.criteriaType), + e.targetId, e.requiredCount, e.timeLimitMs, + e.progressOrder, 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 = stripWacrExt(base); + if (!wowee::pipeline::WoweeAchievementCriteriaLoader::exists(base)) { + std::fprintf(stderr, + "validate-wacr: WACR not found: %s.wacr\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAchievementCriteriaLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + 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.criteriaId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.criteriaId == 0) + errors.push_back(ctx + ": criteriaId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.achievementId == 0) + errors.push_back(ctx + + ": achievementId is 0 — missing WACH cross-ref"); + if (e.criteriaType > wowee::pipeline::WoweeAchievementCriteria::Misc) { + errors.push_back(ctx + ": criteriaType " + + std::to_string(e.criteriaType) + " not in 0..12"); + } + if (e.requiredCount == 0) + warnings.push_back(ctx + + ": requiredCount is 0 — criteria completes " + "instantly on first progress event"); + // Type-specific cross-ref checks. + switch (e.criteriaType) { + case wowee::pipeline::WoweeAchievementCriteria::KillCreature: + case wowee::pipeline::WoweeAchievementCriteria::CompleteQuest: + case wowee::pipeline::WoweeAchievementCriteria::EarnReputation: + case wowee::pipeline::WoweeAchievementCriteria::ExploreZone: + case wowee::pipeline::WoweeAchievementCriteria::LootItem: + case wowee::pipeline::WoweeAchievementCriteria::UseItem: + case wowee::pipeline::WoweeAchievementCriteria::CastSpell: + case wowee::pipeline::WoweeAchievementCriteria::DungeonRun: + if (e.targetId == 0) { + warnings.push_back(ctx + + ": " + + wowee::pipeline::WoweeAchievementCriteria::criteriaTypeName(e.criteriaType) + + " kind requires targetId — engine cannot " + "track progression without it"); + } + break; + case wowee::pipeline::WoweeAchievementCriteria::ReachLevel: + if (e.requiredCount > 80) { + warnings.push_back(ctx + + ": ReachLevel with requiredCount=" + + std::to_string(e.requiredCount) + + " > 80 — character cap is 80 in WotLK"); + } + break; + case wowee::pipeline::WoweeAchievementCriteria::EarnGold: + case wowee::pipeline::WoweeAchievementCriteria::GainHonor: + case wowee::pipeline::WoweeAchievementCriteria::PvPKill: + case wowee::pipeline::WoweeAchievementCriteria::Misc: + break; // no specific cross-ref required + } + // timeLimitMs > 0 with non-time-sensitive criteria + // is suspicious — the engine ignores it for kinds + // like ReachLevel. + if (e.timeLimitMs != 0 && + (e.criteriaType == wowee::pipeline::WoweeAchievementCriteria::ReachLevel || + e.criteriaType == wowee::pipeline::WoweeAchievementCriteria::EarnGold)) { + warnings.push_back(ctx + + ": timeLimitMs " + std::to_string(e.timeLimitMs) + + " set on a non-time-sensitive criteria type — " + "engine will ignore"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.criteriaId) { + errors.push_back(ctx + ": duplicate criteriaId"); + break; + } + } + idsSeen.push_back(e.criteriaId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wacr"] = base + ".wacr"; + 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-wacr: %s.wacr\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu criteria, all criteriaIds unique\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 handleAchievementCriteriaCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-acr") == 0 && i + 1 < argc) { + outRc = handleGenKill(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-acr-quest") == 0 && i + 1 < argc) { + outRc = handleGenQuest(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-acr-mixed") == 0 && i + 1 < argc) { + outRc = handleGenMixed(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wacr") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wacr") == 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_achievement_criteria_catalog.hpp b/tools/editor/cli_achievement_criteria_catalog.hpp new file mode 100644 index 00000000..acf4d342 --- /dev/null +++ b/tools/editor/cli_achievement_criteria_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleAchievementCriteriaCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 374fbbb1..ddf97980 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -247,6 +247,8 @@ const char* const kArgRequired[] = { "--gen-spr", "--gen-spr-warlock", "--gen-spr-rez", "--info-wspr", "--validate-wspr", "--export-wspr-json", "--import-wspr-json", + "--gen-acr", "--gen-acr-quest", "--gen-acr-mixed", + "--info-wacr", "--validate-wacr", "--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 b8601f81..bdd0ef7f 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -121,6 +121,7 @@ #include "cli_talent_tabs_catalog.hpp" #include "cli_currency_types_catalog.hpp" #include "cli_spell_reagents_catalog.hpp" +#include "cli_achievement_criteria_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -283,6 +284,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTalentTabsCatalog, handleCurrencyTypesCatalog, handleSpellReagentsCatalog, + handleAchievementCriteriaCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 13313401..a51c9d9e 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -83,6 +83,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','T','L','E'}, ".wtle", "talents", "--info-wtle", "Talent tab / tree catalog"}, {{'W','C','T','R'}, ".wctr", "currency", "--info-wctr", "Currency type catalog"}, {{'W','S','P','R'}, ".wspr", "spells", "--info-wspr", "Spell reagent set catalog"}, + {{'W','A','C','R'}, ".wacr", "achieve", "--info-wacr", "Achievement criteria 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 2bfebde8..010cb694 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1879,6 +1879,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wspr to a human-editable JSON sidecar (defaults to .wspr.json). Reagent slot arrays exported as JSON arrays\n"); std::printf(" --import-wspr-json [out-base]\n"); std::printf(" Import a .wspr.json sidecar back into binary .wspr (accepts reagentKind int OR reagentKindName string; reagent arrays pad with zeros if shorter than 8)\n"); + std::printf(" --gen-acr [name]\n"); + std::printf(" Emit .wacr 5 kill-counting criteria (Defias 50 / Murloc 25 / Naga 100 / Dragon 1 / RareElite 1) under one composite achievement\n"); + std::printf(" --gen-acr-quest [name]\n"); + std::printf(" Emit .wacr 4 quest-completion criteria (tutorial / starting zone / daily / escort) progression chain\n"); + std::printf(" --gen-acr-mixed [name]\n"); + std::printf(" Emit .wacr 5 cross-type criteria (ReachLevel 80 / EarnGold 10k / GainHonor 5k / PvPKill 100 / ExploreZone Stormwind) showing CriteriaType variety\n"); + std::printf(" --info-wacr [--json]\n"); + std::printf(" Print WACR entries (id / achievementId / type / targetId / requiredCount / timeLimitMs / progressOrder / name)\n"); + std::printf(" --validate-wacr [--json]\n"); + std::printf(" Static checks: id+name+achievementId required, criteriaType 0..12, no duplicate ids; warns on missing targetId for type-specific kinds, ReachLevel>80, timeLimit on non-timed types, requiredCount=0\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 2497ef1d..36de9a27 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -105,6 +105,7 @@ constexpr FormatRow kFormats[] = { {"WTLE", ".wtle", "talents", "TalentTab.dbc", "Talent tab / tree catalog"}, {"WCTR", ".wctr", "currency", "CurrencyTypes.dbc + caps", "Currency type catalog"}, {"WSPR", ".wspr", "spells", "Spell.dbc Reagent[8]+Count[8]", "Spell reagent set catalog"}, + {"WACR", ".wacr", "achieve", "Achievement_Criteria.dbc", "Achievement criteria catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine