diff --git a/CMakeLists.txt b/CMakeLists.txt index 207b5cd7..34161e4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -608,6 +608,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_locks.cpp src/pipeline/wowee_skills.cpp src/pipeline/wowee_spells.cpp + src/pipeline/wowee_achievements.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1362,6 +1363,7 @@ add_executable(wowee_editor tools/editor/cli_locks_catalog.cpp tools/editor/cli_skills_catalog.cpp tools/editor/cli_spells_catalog.cpp + tools/editor/cli_achievements_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1448,6 +1450,7 @@ add_executable(wowee_editor src/pipeline/wowee_locks.cpp src/pipeline/wowee_skills.cpp src/pipeline/wowee_spells.cpp + src/pipeline/wowee_achievements.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_achievements.hpp b/include/pipeline/wowee_achievements.hpp new file mode 100644 index 00000000..4ec7c3b3 --- /dev/null +++ b/include/pipeline/wowee_achievements.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Achievement Catalog (.wach) — novel replacement +// for Blizzard's Achievement.dbc + AchievementCriteria.dbc + +// AchievementCategory.dbc + the AzerothCore-style +// character_achievement / character_achievement_progress +// SQL tables. The 21st open format added to the editor. +// +// Each achievement carries display metadata (name, description, +// icon, points, faction restriction) plus a list of criteria +// the player must satisfy. Criteria mirror the WQT objective +// model (kind + targetId + quantity), which means the runtime +// can reuse the same progress-tracking machinery for both. +// +// Cross-references with previously-added formats: +// WACH.criteria.targetId (kind=KillCreature) → WCRT.creatureId +// WACH.criteria.targetId (kind=CompleteQuest) → WQT.questId +// WACH.criteria.targetId (kind=LootItem) → WIT.itemId +// WACH.criteria.targetId (kind=CastSpell) → WSPL.spellId +// WACH.criteria.targetId (kind=ReachSkillLevel) → WSKL.skillId +// WACH.criteria.targetId (kind=EarnReputation) → WFAC.factionId +// WACH.entry.categoryId → WACH.entry.achievementId (for header +// rows; achievements can be parented +// to other achievements as headers) +// +// Binary layout (little-endian): +// magic[4] = "WACH" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// achievementId (uint32) +// categoryId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// titleLen + titleReward +// points (uint32) +// minLevel (uint16) / faction (uint8) / criteriaCount (uint8) +// flags (uint32) +// criteria (criteriaCount × { +// criteriaId (uint32) +// kind (uint8) + pad[3] +// targetId (uint32) +// quantity (uint32) +// descLen + description +// }) +struct WoweeAchievement { + enum CriteriaKind : uint8_t { + KillCreature = 0, + CompleteQuest = 1, + LootItem = 2, + ReachLevel = 3, + EarnReputation = 4, + CastSpell = 5, + ReachSkillLevel = 6, + VisitArea = 7, + CompleteAchievement = 8, // meta-achievements + }; + + enum Faction : uint8_t { + FactionBoth = 0, + FactionAlliance = 1, + FactionHorde = 2, + }; + + enum Flags : uint32_t { + HiddenUntilEarned = 0x01, // not shown in panel until completed + ServerFirst = 0x02, // first-on-server rewards a global tag + RealmFirst = 0x04, + Tracking = 0x08, // shows progress UI in the panel + Counter = 0x10, // counts up forever (Pet Battles wins) + Account = 0x20, // account-wide, not per-character + }; + + struct Criterion { + uint32_t criteriaId = 0; + uint8_t kind = KillCreature; + uint32_t targetId = 0; + uint32_t quantity = 1; + std::string description; + }; + + struct Entry { + uint32_t achievementId = 0; + uint32_t categoryId = 0; // 0 = top-level + std::string name; + std::string description; + std::string iconPath; + std::string titleReward; // empty = no title + uint32_t points = 10; + uint16_t minLevel = 1; + uint8_t faction = FactionBoth; + uint32_t flags = 0; + std::vector criteria; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Lookup by achievementId — nullptr if not present. + const Entry* findById(uint32_t achievementId) const; + + static const char* criteriaKindName(uint8_t k); + static const char* factionName(uint8_t f); +}; + +class WoweeAchievementLoader { +public: + static bool save(const WoweeAchievement& cat, + const std::string& basePath); + static WoweeAchievement load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-achievements* variants. + // + // makeStarter — 3 demo achievements covering kill / quest + // completion / level reached criteria. + // makeBandit — bandit-themed: "Slay 50 Defias Bandits" + // + "Loot the Bandit Strongbox" + "Complete + // Bandit Trouble" — all referencing the + // WCRT/WGOT/WQT/WIT cross-referenced demo IDs. + // makeMeta — 3 base achievements + 1 meta-achievement + // that requires completing the others. + static WoweeAchievement makeStarter(const std::string& catalogName); + static WoweeAchievement makeBandit(const std::string& catalogName); + static WoweeAchievement makeMeta(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_achievements.cpp b/src/pipeline/wowee_achievements.cpp new file mode 100644 index 00000000..364b20f9 --- /dev/null +++ b/src/pipeline/wowee_achievements.cpp @@ -0,0 +1,307 @@ +#include "pipeline/wowee_achievements.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'A', 'C', 'H'}; +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) != ".wach") { + base += ".wach"; + } + return base; +} + +} // namespace + +const WoweeAchievement::Entry* WoweeAchievement::findById(uint32_t achievementId) const { + for (const auto& e : entries) { + if (e.achievementId == achievementId) return &e; + } + return nullptr; +} + +const char* WoweeAchievement::criteriaKindName(uint8_t k) { + switch (k) { + case KillCreature: return "kill"; + case CompleteQuest: return "quest"; + case LootItem: return "loot"; + case ReachLevel: return "level"; + case EarnReputation: return "rep"; + case CastSpell: return "cast"; + case ReachSkillLevel: return "skill"; + case VisitArea: return "visit"; + case CompleteAchievement: return "meta"; + default: return "unknown"; + } +} + +const char* WoweeAchievement::factionName(uint8_t f) { + switch (f) { + case FactionBoth: return "both"; + case FactionAlliance: return "alliance"; + case FactionHorde: return "horde"; + default: return "unknown"; + } +} + +bool WoweeAchievementLoader::save(const WoweeAchievement& 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.achievementId); + writePOD(os, e.categoryId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writeStr(os, e.titleReward); + writePOD(os, e.points); + writePOD(os, e.minLevel); + writePOD(os, e.faction); + uint8_t critCount = static_cast( + e.criteria.size() > 255 ? 255 : e.criteria.size()); + writePOD(os, critCount); + writePOD(os, e.flags); + for (uint8_t k = 0; k < critCount; ++k) { + const auto& cr = e.criteria[k]; + writePOD(os, cr.criteriaId); + writePOD(os, cr.kind); + uint8_t pad[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad), 3); + writePOD(os, cr.targetId); + writePOD(os, cr.quantity); + writeStr(os, cr.description); + } + } + return os.good(); +} + +WoweeAchievement WoweeAchievementLoader::load(const std::string& basePath) { + WoweeAchievement 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.achievementId) || + !readPOD(is, e.categoryId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath) || !readStr(is, e.titleReward)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.points) || + !readPOD(is, e.minLevel) || + !readPOD(is, e.faction)) { + out.entries.clear(); return out; + } + uint8_t critCount = 0; + if (!readPOD(is, critCount)) { out.entries.clear(); return out; } + if (!readPOD(is, e.flags)) { out.entries.clear(); return out; } + e.criteria.resize(critCount); + for (uint8_t k = 0; k < critCount; ++k) { + auto& cr = e.criteria[k]; + if (!readPOD(is, cr.criteriaId) || + !readPOD(is, cr.kind)) { + out.entries.clear(); return out; + } + uint8_t pad[3]; + is.read(reinterpret_cast(pad), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, cr.targetId) || + !readPOD(is, cr.quantity) || + !readStr(is, cr.description)) { + out.entries.clear(); return out; + } + } + } + return out; +} + +bool WoweeAchievementLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeAchievement WoweeAchievementLoader::makeStarter(const std::string& catalogName) { + WoweeAchievement c; + c.name = catalogName; + { + WoweeAchievement::Entry e; + e.achievementId = 1; + e.name = "First Blood"; + e.description = "Kill your first hostile creature."; + e.points = 5; + e.criteria.push_back({1, WoweeAchievement::KillCreature, + 1000, 1, "Kill any hostile creature"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 2; + e.name = "Helping Hand"; + e.description = "Complete your first quest."; + e.points = 5; + e.criteria.push_back({2, WoweeAchievement::CompleteQuest, + 1, 1, "Complete the Bandit Trouble quest"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 3; + e.name = "Coming of Age"; + e.description = "Reach character level 10."; + e.points = 10; + e.criteria.push_back({3, WoweeAchievement::ReachLevel, + 0, 10, "Reach level 10"}); + c.entries.push_back(e); + } + return c; +} + +WoweeAchievement WoweeAchievementLoader::makeBandit(const std::string& catalogName) { + WoweeAchievement c; + c.name = catalogName; + { + WoweeAchievement::Entry e; + e.achievementId = 100; + e.name = "Bandit Hunter"; + e.description = "Slay 50 Defias Bandits."; + e.points = 10; + // creatureId 1000 matches WCRT.makeBandit + WSPN.makeCamp + // + WLOT.makeBandit + WQT.makeStarter target. + e.criteria.push_back({100, WoweeAchievement::KillCreature, + 1000, 50, "Defias Bandits slain"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 101; + e.name = "Strongbox Cracked"; + e.description = "Loot the Bandit Strongbox."; + e.points = 5; + // objectId 2000 matches WGOT.makeDungeon's bandit chest. + e.criteria.push_back({101, WoweeAchievement::LootItem, + 2000, 1, "Loot the Bandit Strongbox"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 102; + e.name = "Brotherhood Down"; + e.description = "Complete the Bandit Trouble quest line."; + e.points = 15; + e.criteria.push_back({102, WoweeAchievement::CompleteQuest, + 1, 1, "Quest 1: Bandit Trouble"}); + c.entries.push_back(e); + } + return c; +} + +WoweeAchievement WoweeAchievementLoader::makeMeta(const std::string& catalogName) { + WoweeAchievement c; + c.name = catalogName; + { + WoweeAchievement::Entry e; + e.achievementId = 200; e.name = "Mining Apprentice"; + e.description = "Reach 100 in Mining."; + e.points = 10; + // skillId 186 matches WSKL.makeProfessions + WGOT.makeGather. + e.criteria.push_back({200, WoweeAchievement::ReachSkillLevel, + 186, 100, "Mining at rank 100"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 201; e.name = "Lockbreaker"; + e.description = "Reach 100 in Lockpicking."; + e.points = 10; + // skillId 633 matches WSKL.makeStarter + WLCK.makeDungeon. + e.criteria.push_back({201, WoweeAchievement::ReachSkillLevel, + 633, 100, "Lockpicking at rank 100"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 202; e.name = "Frostbinder"; + e.description = "Cast Frostbolt 100 times."; + e.points = 5; + // spellId 116 matches WSPL.makeMage's Frostbolt. + e.criteria.push_back({202, WoweeAchievement::CastSpell, + 116, 100, "Frostbolt cast count"}); + c.entries.push_back(e); + } + { + WoweeAchievement::Entry e; + e.achievementId = 250; e.name = "Jack of All Trades"; + e.description = "Complete all 3 sub-achievements."; + e.points = 25; + e.titleReward = "the Versatile"; + e.flags = WoweeAchievement::HiddenUntilEarned; + e.criteria.push_back({250, WoweeAchievement::CompleteAchievement, + 200, 1, "Mining Apprentice"}); + e.criteria.push_back({251, WoweeAchievement::CompleteAchievement, + 201, 1, "Lockbreaker"}); + e.criteria.push_back({252, WoweeAchievement::CompleteAchievement, + 202, 1, "Frostbinder"}); + c.entries.push_back(e); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_achievements_catalog.cpp b/tools/editor/cli_achievements_catalog.cpp new file mode 100644 index 00000000..b84e6827 --- /dev/null +++ b/tools/editor/cli_achievements_catalog.cpp @@ -0,0 +1,288 @@ +#include "cli_achievements_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_achievements.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWachExt(std::string base) { + stripExt(base, ".wach"); + return base; +} + +void appendAchFlagsStr(std::string& s, uint32_t flags) { + if (flags & wowee::pipeline::WoweeAchievement::HiddenUntilEarned) s += "hidden "; + if (flags & wowee::pipeline::WoweeAchievement::ServerFirst) s += "server-first "; + if (flags & wowee::pipeline::WoweeAchievement::RealmFirst) s += "realm-first "; + if (flags & wowee::pipeline::WoweeAchievement::Tracking) s += "tracking "; + if (flags & wowee::pipeline::WoweeAchievement::Counter) s += "counter "; + if (flags & wowee::pipeline::WoweeAchievement::Account) s += "account "; + if (s.empty()) s = "-"; + else if (s.back() == ' ') s.pop_back(); +} + +bool saveOrError(const wowee::pipeline::WoweeAchievement& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeAchievementLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wach\n", + cmd, base.c_str()); + return false; + } + return true; +} + +uint32_t totalCriteria(const wowee::pipeline::WoweeAchievement& c) { + uint32_t n = 0; + for (const auto& e : c.entries) n += static_cast(e.criteria.size()); + return n; +} + +void printGenSummary(const wowee::pipeline::WoweeAchievement& c, + const std::string& base) { + std::printf("Wrote %s.wach\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" achievements : %zu (%u criteria total)\n", + c.entries.size(), totalCriteria(c)); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterAchievements"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWachExt(base); + auto c = wowee::pipeline::WoweeAchievementLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-achievements")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBandit(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BanditAchievements"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWachExt(base); + auto c = wowee::pipeline::WoweeAchievementLoader::makeBandit(name); + if (!saveOrError(c, base, "gen-achievements-bandit")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMeta(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MetaAchievements"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWachExt(base); + auto c = wowee::pipeline::WoweeAchievementLoader::makeMeta(name); + if (!saveOrError(c, base, "gen-achievements-meta")) 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 = stripWachExt(base); + if (!wowee::pipeline::WoweeAchievementLoader::exists(base)) { + std::fprintf(stderr, "WACH not found: %s.wach\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAchievementLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wach"] = base + ".wach"; + j["name"] = c.name; + j["count"] = c.entries.size(); + j["totalCriteria"] = totalCriteria(c); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + std::string fs; + appendAchFlagsStr(fs, e.flags); + nlohmann::json je; + je["achievementId"] = e.achievementId; + je["categoryId"] = e.categoryId; + je["name"] = e.name; + je["description"] = e.description; + je["iconPath"] = e.iconPath; + je["titleReward"] = e.titleReward; + je["points"] = e.points; + je["minLevel"] = e.minLevel; + je["faction"] = e.faction; + je["factionName"] = wowee::pipeline::WoweeAchievement::factionName(e.faction); + je["flags"] = e.flags; + je["flagsStr"] = fs; + nlohmann::json ca = nlohmann::json::array(); + for (const auto& cr : e.criteria) { + ca.push_back({ + {"criteriaId", cr.criteriaId}, + {"kind", cr.kind}, + {"kindName", wowee::pipeline::WoweeAchievement::criteriaKindName(cr.kind)}, + {"targetId", cr.targetId}, + {"quantity", cr.quantity}, + {"description", cr.description}, + }); + } + je["criteria"] = ca; + arr.push_back(je); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WACH: %s.wach\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" achievements : %zu (%u criteria total)\n", + c.entries.size(), totalCriteria(c)); + if (c.entries.empty()) return 0; + for (const auto& e : c.entries) { + std::string fs; + appendAchFlagsStr(fs, e.flags); + std::printf("\n achievementId=%u points=%u faction=%s flags=%s%s%s\n", + e.achievementId, e.points, + wowee::pipeline::WoweeAchievement::factionName(e.faction), + fs.c_str(), + e.titleReward.empty() ? "" : " title=", + e.titleReward.c_str()); + std::printf(" name : %s\n", e.name.c_str()); + if (e.criteria.empty()) { + std::printf(" *no criteria*\n"); + continue; + } + for (const auto& cr : e.criteria) { + std::printf(" [%-5s] target=%-5u qty=%u %s\n", + wowee::pipeline::WoweeAchievement::criteriaKindName(cr.kind), + cr.targetId, cr.quantity, + cr.description.c_str()); + } + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWachExt(base); + if (!wowee::pipeline::WoweeAchievementLoader::exists(base)) { + std::fprintf(stderr, + "validate-wach: WACH not found: %s.wach\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAchievementLoader::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.achievementId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.achievementId == 0) { + errors.push_back(ctx + ": achievementId is 0"); + } + if (e.name.empty()) { + errors.push_back(ctx + ": name is empty"); + } + if (e.faction > wowee::pipeline::WoweeAchievement::FactionHorde) { + errors.push_back(ctx + ": faction " + + std::to_string(e.faction) + " not in 0..2"); + } + if (e.criteria.empty()) { + warnings.push_back(ctx + + ": no criteria (achievement can never be earned)"); + } + for (size_t ci = 0; ci < e.criteria.size(); ++ci) { + const auto& cr = e.criteria[ci]; + std::string cctx = ctx + " criterion " + std::to_string(ci); + if (cr.kind > wowee::pipeline::WoweeAchievement::CompleteAchievement) { + errors.push_back(cctx + ": kind " + + std::to_string(cr.kind) + " not in 0..8"); + } + if (cr.quantity == 0) { + errors.push_back(cctx + ": quantity is 0"); + } + // ReachLevel and Counter-style criteria can have + // targetId=0; everything else needs a real target. + bool needsTarget = + cr.kind != wowee::pipeline::WoweeAchievement::ReachLevel; + if (needsTarget && cr.targetId == 0) { + errors.push_back(cctx + ": targetId is 0 (no resource referenced)"); + } + } + for (uint32_t prev : idsSeen) { + if (prev == e.achievementId) { + errors.push_back(ctx + ": duplicate achievementId"); + break; + } + } + idsSeen.push_back(e.achievementId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wach"] = base + ".wach"; + 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-wach: %s.wach\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu achievements (%u criteria), all IDs unique\n", + c.entries.size(), totalCriteria(c)); + 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 handleAchievementsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-achievements") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-achievements-bandit") == 0 && i + 1 < argc) { + outRc = handleGenBandit(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-achievements-meta") == 0 && i + 1 < argc) { + outRc = handleGenMeta(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wach") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wach") == 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_achievements_catalog.hpp b/tools/editor/cli_achievements_catalog.hpp new file mode 100644 index 00000000..c018f3ae --- /dev/null +++ b/tools/editor/cli_achievements_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleAchievementsCatalog(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 0113a71f..b59d1767 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -58,6 +58,8 @@ const char* const kArgRequired[] = { "--export-wskl-json", "--import-wskl-json", "--gen-spells", "--gen-spells-mage", "--gen-spells-warrior", "--info-wspl", "--validate-wspl", + "--gen-achievements", "--gen-achievements-bandit", "--gen-achievements-meta", + "--info-wach", "--validate-wach", "--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 96900490..c52219ca 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -48,6 +48,7 @@ #include "cli_locks_catalog.hpp" #include "cli_skills_catalog.hpp" #include "cli_spells_catalog.hpp" +#include "cli_achievements_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -137,6 +138,7 @@ constexpr DispatchFn kDispatchTable[] = { handleLocksCatalog, handleSkillsCatalog, handleSpellsCatalog, + handleAchievementsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 6ae98c87..32420b5b 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -989,6 +989,16 @@ void printUsage(const char* argv0) { std::printf(" Print WSPL spell entries (id / school / effect / cast/cd / mana / range / damage range / name)\n"); std::printf(" --validate-wspl [--json]\n"); std::printf(" Static checks: spellId>0+unique, name not empty, school 0..6, range/value min<=max, friendly+hostile incoherent\n"); + std::printf(" --gen-achievements [name]\n"); + std::printf(" Emit .wach starter: 3 achievements (First Blood / Helping Hand / Coming of Age)\n"); + std::printf(" --gen-achievements-bandit [name]\n"); + std::printf(" Emit .wach bandit-themed: 3 achievements with cross-refs to WCRT/WGOT/WQT bandit IDs\n"); + std::printf(" --gen-achievements-meta [name]\n"); + std::printf(" Emit .wach 3 base achievements + 1 meta-achievement (Jack of All Trades, title 'the Versatile')\n"); + std::printf(" --info-wach [--json]\n"); + std::printf(" Print WACH entries (id / points / faction / flags / criteria with kind+target+qty)\n"); + std::printf(" --validate-wach [--json]\n"); + std::printf(" Static checks: id>0+unique, name not empty, faction 0..2, criteria need targetId+quantity>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");