From 0df50f9f72df72210dec157fe0cf22a6d15c3cb0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 03:03:27 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WANV=20(Anniversary=20&?= =?UTF-8?q?=20Recurring=20Events)=20=E2=80=94=20121st=20open=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the implicit recurring-event scheduler vanilla WoW encoded across the GameEvent SQL table + per-holiday script hooks. Each entry binds one calendar-driven recurring event (yearly holiday like Hallow's End, monthly tribute day, weekly Double XP Weekend, anniversary celebration) to its scheduling rule and its payload (a spell buff applied to all online players, a gift item granted on first event- window login). Eight eventKind values (Holiday / Anniversary / DoubleXP / DoubleHonor / PetBattleWeekend / BattlegroundBonus / SeasonalQuest / Misc) and four recurrenceKind values (Yearly / Monthly / Weekly / OneOff). The startDay field is polymorphic per recurrenceKind: Yearly/Monthly/OneOff use it as 1..31 day-of-month, Weekly uses it as 0..6 weekday (Sun..Sat) — the validator enforces both ranges per kind. Three preset emitters: makeStandardHolidays (5 yearly holidays with realistic spell+item payload bindings — Hallow's End spell 24710, Winter Veil 26157, Brewfest 42500, etc.), makeBonusEvents (4 weekly recurring bonuses — Friday triple-day weekends and Saturday- Sunday double-day pet-battle bonus), makeAnniversary (3 game-launch anniversaries — WoW Nov 23 / TBC Jan 16 / WotLK Nov 13 with overlapping celebration windows). Validator's most novel checks combine calendar + recurrence semantics: per-kind schedule validity (Weekly startDay 0..6 weekday, durationDays <= 7 to prevent self-overlap; Yearly/Monthly/OneOff startMonth 1..12, startDay 1..31 with calendar sanity — Feb cap at 29, Apr/Jun/Sep/Nov cap at 30 for "no Feb 30" / "no Apr 31" errors). Format count 120 -> 121. CLI flag count 1269 -> 1274. --- CMakeLists.txt | 3 + include/pipeline/wowee_anniversary_events.hpp | 150 ++++++++ src/pipeline/wowee_anniversary_events.cpp | 285 +++++++++++++++ .../editor/cli_anniversary_events_catalog.cpp | 337 ++++++++++++++++++ .../editor/cli_anniversary_events_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, 803 insertions(+) create mode 100644 include/pipeline/wowee_anniversary_events.hpp create mode 100644 src/pipeline/wowee_anniversary_events.cpp create mode 100644 tools/editor/cli_anniversary_events_catalog.cpp create mode 100644 tools/editor/cli_anniversary_events_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f72e269b..a92eb9d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -709,6 +709,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_loot_modes.cpp src/pipeline/wowee_sky_params.cpp src/pipeline/wowee_server_config.cpp + src/pipeline/wowee_anniversary_events.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1581,6 +1582,7 @@ add_executable(wowee_editor tools/editor/cli_loot_modes_catalog.cpp tools/editor/cli_sky_params_catalog.cpp tools/editor/cli_server_config_catalog.cpp + tools/editor/cli_anniversary_events_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1772,6 +1774,7 @@ add_executable(wowee_editor src/pipeline/wowee_loot_modes.cpp src/pipeline/wowee_sky_params.cpp src/pipeline/wowee_server_config.cpp + src/pipeline/wowee_anniversary_events.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_anniversary_events.hpp b/include/pipeline/wowee_anniversary_events.hpp new file mode 100644 index 00000000..72991b33 --- /dev/null +++ b/include/pipeline/wowee_anniversary_events.hpp @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Anniversary & Recurring Event catalog +// (.wanv) — novel replacement for the implicit +// recurring-event scheduler vanilla WoW encoded across +// the GameEvent SQL table + the per-holiday script +// hooks. Each entry binds one calendar-driven recurring +// event (holiday like Hallow's End, anniversary, double- +// XP weekend, brewfest) to its scheduling rule (yearly +// on a fixed date, monthly on a fixed day, weekly on a +// weekday) and its payload (a spell buff applied to all +// online players, a gift item granted on first login +// during the event window). +// +// Cross-references with previously-added formats: +// WSPL: payloadSpellId references the WSPL spell +// catalog (buff applied to online players for +// the event duration). +// WIT: payloadItemId references the WIT item +// catalog (gift item granted on first event- +// window login). +// +// Binary layout (little-endian): +// magic[4] = "WANV" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// eventId (uint32) +// nameLen + name +// descLen + description +// eventKind (uint8) — Holiday / Anniversary +// / DoubleXP / +// DoubleHonor / +// PetBattleWeekend / +// BattlegroundBonus / +// SeasonalQuest / +// Misc +// recurrenceKind (uint8) — Yearly / Monthly / +// Weekly / OneOff +// startMonth (uint8) — 1..12 for Yearly / +// Monthly; ignored for +// Weekly (use startDay +// = weekday 0..6) +// startDay (uint8) — 1..31 for Yearly / +// Monthly; 0..6 for +// Weekly (Sun..Sat) +// durationDays (uint16) — event window length +// pad0 (uint8) / pad1 (uint8) +// payloadSpellId (uint32) — 0 if no buff +// payloadItemId (uint32) — 0 if no gift item +// iconColorRGBA (uint32) +struct WoweeAnniversaryEvents { + enum EventKind : uint8_t { + Holiday = 0, // seasonal real-world + // holiday tie-in + Anniversary = 1, // game-launch + // anniversary + DoubleXP = 2, // experience boost + DoubleHonor = 3, // PvP boost + PetBattleWeekend = 4, // pet-battle bonus + BattlegroundBonus = 5, // BG honor / token + // boost + SeasonalQuest = 6, // limited-time quest + // chain + Misc = 255, + }; + + enum RecurrenceKind : uint8_t { + Yearly = 0, // same date every year + // (e.g. Hallow's End Oct 18) + Monthly = 1, // same day every month + // (e.g. monthly tribute day) + Weekly = 2, // same weekday every week + // (e.g. Tuesday maintenance) + OneOff = 3, // single occurrence at the + // specified date — Anniversary + // events stay this way until + // the next year manually + // re-schedules + }; + + struct Entry { + uint32_t eventId = 0; + std::string name; + std::string description; + uint8_t eventKind = Holiday; + uint8_t recurrenceKind = Yearly; + uint8_t startMonth = 1; + uint8_t startDay = 1; + uint16_t durationDays = 7; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint32_t payloadSpellId = 0; + uint32_t payloadItemId = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t eventId) const; + + // Returns all events of one kind. Used by the event + // scheduler to dispatch per-kind handlers (Holiday + // events spawn cosmetic NPCs, DoubleXP events + // multiply XP rates, BattlegroundBonus events boost + // honor accrual). + std::vector findByKind(uint8_t eventKind) const; +}; + +class WoweeAnniversaryEventsLoader { +public: + static bool save(const WoweeAnniversaryEvents& cat, + const std::string& basePath); + static WoweeAnniversaryEvents load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-anv* variants. + // + // makeStandardHolidays — 5 yearly holidays + // (Hallow's End / Winter + // Veil / Lunar Festival / + // Children's Week / + // Brewfest). + // makeBonusEvents — 4 weekly bonus events + // (Double XP Weekend / + // Double Honor / Pet + // Battle Weekend / BG + // Bonus). + // makeAnniversary — 3 game-launch + // anniversaries (WoW Nov + // 23 / TBC Jan 16 / WotLK + // Nov 13). + static WoweeAnniversaryEvents makeStandardHolidays(const std::string& catalogName); + static WoweeAnniversaryEvents makeBonusEvents(const std::string& catalogName); + static WoweeAnniversaryEvents makeAnniversary(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_anniversary_events.cpp b/src/pipeline/wowee_anniversary_events.cpp new file mode 100644 index 00000000..4955002d --- /dev/null +++ b/src/pipeline/wowee_anniversary_events.cpp @@ -0,0 +1,285 @@ +#include "pipeline/wowee_anniversary_events.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'A', 'N', 'V'}; +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) != ".wanv") { + base += ".wanv"; + } + 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 WoweeAnniversaryEvents::Entry* +WoweeAnniversaryEvents::findById(uint32_t eventId) const { + for (const auto& e : entries) + if (e.eventId == eventId) return &e; + return nullptr; +} + +std::vector +WoweeAnniversaryEvents::findByKind(uint8_t eventKind) const { + std::vector out; + for (const auto& e : entries) + if (e.eventKind == eventKind) out.push_back(&e); + return out; +} + +bool WoweeAnniversaryEventsLoader::save( + const WoweeAnniversaryEvents& 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.eventId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.eventKind); + writePOD(os, e.recurrenceKind); + writePOD(os, e.startMonth); + writePOD(os, e.startDay); + writePOD(os, e.durationDays); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.payloadSpellId); + writePOD(os, e.payloadItemId); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeAnniversaryEvents WoweeAnniversaryEventsLoader::load( + const std::string& basePath) { + WoweeAnniversaryEvents 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.eventId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.eventKind) || + !readPOD(is, e.recurrenceKind) || + !readPOD(is, e.startMonth) || + !readPOD(is, e.startDay) || + !readPOD(is, e.durationDays) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.payloadSpellId) || + !readPOD(is, e.payloadItemId) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeAnniversaryEventsLoader::exists( + const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeAnniversaryEvents +WoweeAnniversaryEventsLoader::makeStandardHolidays( + const std::string& catalogName) { + using A = WoweeAnniversaryEvents; + WoweeAnniversaryEvents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t month, uint8_t day, + uint16_t days, uint32_t spellId, + uint32_t itemId, const char* desc) { + A::Entry e; + e.eventId = id; e.name = name; e.description = desc; + e.eventKind = A::Holiday; + e.recurrenceKind = A::Yearly; + e.startMonth = month; + e.startDay = day; + e.durationDays = days; + e.payloadSpellId = spellId; + e.payloadItemId = itemId; + e.iconColorRGBA = packRgba(220, 200, 80); // holiday gold + c.entries.push_back(e); + }; + add(1, "HallowsEnd", 10, 18, 14, 24710, 33226, + "Hallow's End — Oct 18 to Nov 1 yearly. " + "14-day window with cosmetic costume buff " + "(spell 24710 Trick or Treat) and gift basket " + "item (33226)."); + add(2, "WintersVeil", 12, 16, 17, 26157, 21525, + "Feast of Winter Veil — Dec 16 to Jan 1 yearly. " + "17-day window with snowfall environmental buff " + "(spell 26157) and Smokywood Pastures gift " + "(21525)."); + add(3, "LunarFestival", 1, 22, 21, 8898, 21100, + "Lunar Festival — late Jan to mid Feb yearly. " + "21-day window with Coin of Ancestry quest " + "currency (item 21100). Buff: Spirit of Yu'lon " + "8898."); + add(4, "ChildrensWeek", 5, 1, 7, 0, 23007, + "Children's Week — May 1-7 yearly. 7-day " + "window with orphan companion-pet quest reward " + "(item 23007 Whiskers the Rat). No buff payload."); + add(5, "Brewfest", 9, 20, 17, 42500, 33927, + "Brewfest — Sep 20 to Oct 6 yearly. 17-day " + "window with Brewfest Buff (spell 42500) and " + "Brewfest gift box (33927)."); + return c; +} + +WoweeAnniversaryEvents +WoweeAnniversaryEventsLoader::makeBonusEvents( + const std::string& catalogName) { + using A = WoweeAnniversaryEvents; + WoweeAnniversaryEvents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t kind, uint8_t weekday, + uint16_t days, uint32_t spellId, + const char* desc) { + A::Entry e; + e.eventId = id; e.name = name; e.description = desc; + e.eventKind = kind; + e.recurrenceKind = A::Weekly; + e.startMonth = 0; // ignored for Weekly + e.startDay = weekday; // 0=Sunday..6=Saturday + e.durationDays = days; + e.payloadSpellId = spellId; + e.payloadItemId = 0; + e.iconColorRGBA = packRgba(140, 200, 255); // bonus blue + c.entries.push_back(e); + }; + add(100, "DoubleXPWeekend", A::DoubleXP, 5, 3, 0, + "Double XP Weekend — Friday through Sunday " + "(weekday 5, 3-day window). Server XP rate " + "doubled via WCFG override; no per-character " + "spell buff needed."); + add(101, "DoubleHonorWeekend", A::DoubleHonor, 5, 3, 0, + "Double Honor Weekend — Friday through Sunday. " + "PvP honor accrual doubled."); + add(102, "PetBattleWeekend", A::PetBattleWeekend, + 6, 2, 0, + "Pet Battle Weekend — Saturday-Sunday only " + "(weekday 6, 2-day window). +50%% pet XP from " + "battles. Anachronistic for WotLK (Pet Battles " + "came in MoP) but useful template for custom " + "servers."); + add(103, "BattlegroundBonus", A::BattlegroundBonus, + 2, 1, 0, + "Battleground Bonus Day — Tuesday only " + "(weekday 2). Random BG queue grants +100%% " + "tokens for that day. Tuesday chosen as a " + "weekday-traffic boost to balance the weekend " + "events."); + return c; +} + +WoweeAnniversaryEvents +WoweeAnniversaryEventsLoader::makeAnniversary( + const std::string& catalogName) { + using A = WoweeAnniversaryEvents; + WoweeAnniversaryEvents c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t month, uint8_t day, + uint16_t days, uint32_t spellId, + uint32_t itemId, const char* desc) { + A::Entry e; + e.eventId = id; e.name = name; e.description = desc; + e.eventKind = A::Anniversary; + e.recurrenceKind = A::Yearly; + e.startMonth = month; + e.startDay = day; + e.durationDays = days; + e.payloadSpellId = spellId; + e.payloadItemId = itemId; + e.iconColorRGBA = packRgba(180, 60, 60); // anniversary red + c.entries.push_back(e); + }; + add(200, "WoWLaunchAnniversary", 11, 23, 14, 71601, 49700, + "World of Warcraft launch anniversary — Nov 23 " + "yearly (US launch 2004). 14-day celebration " + "window with Anniversary Buff (spell 71601 " + "Bloody Anniversary) and gift item 49700."); + add(201, "TBCLaunchAnniversary", 1, 16, 7, 71601, 49701, + "The Burning Crusade launch anniversary — " + "Jan 16 yearly (2007). 7-day window. Same " + "Anniversary Buff spell, distinct gift item."); + add(202, "WotLKLaunchAnniversary", 11, 13, 7, 71601, 49702, + "Wrath of the Lich King launch anniversary — " + "Nov 13 yearly (2008). 7-day window. Overlaps " + "with WoW Launch Anniversary by 10 days — both " + "events run concurrently for combined celebration."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_anniversary_events_catalog.cpp b/tools/editor/cli_anniversary_events_catalog.cpp new file mode 100644 index 00000000..e7ac93c4 --- /dev/null +++ b/tools/editor/cli_anniversary_events_catalog.cpp @@ -0,0 +1,337 @@ +#include "cli_anniversary_events_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_anniversary_events.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWanvExt(std::string base) { + stripExt(base, ".wanv"); + return base; +} + +const char* eventKindName(uint8_t k) { + using A = wowee::pipeline::WoweeAnniversaryEvents; + switch (k) { + case A::Holiday: return "holiday"; + case A::Anniversary: return "anniversary"; + case A::DoubleXP: return "doublexp"; + case A::DoubleHonor: return "doublehonor"; + case A::PetBattleWeekend: return "petbattle"; + case A::BattlegroundBonus: return "bgbonus"; + case A::SeasonalQuest: return "seasonalquest"; + case A::Misc: return "misc"; + default: return "unknown"; + } +} + +const char* recurrenceKindName(uint8_t k) { + using A = wowee::pipeline::WoweeAnniversaryEvents; + switch (k) { + case A::Yearly: return "yearly"; + case A::Monthly: return "monthly"; + case A::Weekly: return "weekly"; + case A::OneOff: return "oneoff"; + default: return "unknown"; + } +} + +const char* weekdayName(uint8_t d) { + static const char* kDays[] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + }; + return d <= 6 ? kDays[d] : "?"; +} + +bool saveOrError(const wowee::pipeline::WoweeAnniversaryEvents& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeAnniversaryEventsLoader::save( + c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wanv\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeAnniversaryEvents& c, + const std::string& base) { + std::printf("Wrote %s.wanv\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" events : %zu\n", c.entries.size()); +} + +int handleGenHolidays(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StandardHolidays"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWanvExt(base); + auto c = wowee::pipeline::WoweeAnniversaryEventsLoader:: + makeStandardHolidays(name); + if (!saveOrError(c, base, "gen-anv")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBonus(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeeklyBonusEvents"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWanvExt(base); + auto c = wowee::pipeline::WoweeAnniversaryEventsLoader:: + makeBonusEvents(name); + if (!saveOrError(c, base, "gen-anv-bonus")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenAnniversary(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "GameLaunchAnniversaries"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWanvExt(base); + auto c = wowee::pipeline::WoweeAnniversaryEventsLoader:: + makeAnniversary(name); + if (!saveOrError(c, base, "gen-anv-launch")) 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 = stripWanvExt(base); + if (!wowee::pipeline::WoweeAnniversaryEventsLoader::exists( + base)) { + std::fprintf(stderr, "WANV not found: %s.wanv\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAnniversaryEventsLoader::load( + base); + if (jsonOut) { + nlohmann::json j; + j["wanv"] = base + ".wanv"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"eventId", e.eventId}, + {"name", e.name}, + {"description", e.description}, + {"eventKind", e.eventKind}, + {"eventKindName", eventKindName(e.eventKind)}, + {"recurrenceKind", e.recurrenceKind}, + {"recurrenceKindName", + recurrenceKindName(e.recurrenceKind)}, + {"startMonth", e.startMonth}, + {"startDay", e.startDay}, + {"durationDays", e.durationDays}, + {"payloadSpellId", e.payloadSpellId}, + {"payloadItemId", e.payloadItemId}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WANV: %s.wanv\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" events : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind recurrence schedule dur(d) spell item name\n"); + for (const auto& e : c.entries) { + char schedule[32]; + using A = wowee::pipeline::WoweeAnniversaryEvents; + if (e.recurrenceKind == A::Weekly) { + std::snprintf(schedule, sizeof(schedule), + "%-3s every week", weekdayName(e.startDay)); + } else if (e.recurrenceKind == A::Monthly) { + std::snprintf(schedule, sizeof(schedule), + "Day %u every month", e.startDay); + } else { + std::snprintf(schedule, sizeof(schedule), + "%02u/%02u %s", e.startMonth, e.startDay, + recurrenceKindName(e.recurrenceKind)); + } + std::printf(" %4u %-10s %-10s %-15s %4u %5u %5u %s\n", + e.eventId, eventKindName(e.eventKind), + recurrenceKindName(e.recurrenceKind), + schedule, e.durationDays, + e.payloadSpellId, e.payloadItemId, + 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 = stripWanvExt(base); + if (!wowee::pipeline::WoweeAnniversaryEventsLoader::exists( + base)) { + std::fprintf(stderr, + "validate-wanv: WANV not found: %s.wanv\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAnniversaryEventsLoader::load( + base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set 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.eventId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.eventId == 0) + errors.push_back(ctx + ": eventId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.eventKind > 6 && e.eventKind != 255) { + errors.push_back(ctx + ": eventKind " + + std::to_string(e.eventKind) + + " out of range (must be 0..6 or 255 Misc)"); + } + if (e.recurrenceKind > 3) { + errors.push_back(ctx + ": recurrenceKind " + + std::to_string(e.recurrenceKind) + + " out of range (must be 0..3)"); + } + if (e.durationDays == 0) { + errors.push_back(ctx + + ": durationDays is 0 — event would never " + "have an active window"); + } + // Per-recurrence schedule validity: Yearly / + // Monthly / OneOff need valid month + day; Weekly + // ignores month + uses day as weekday 0..6. + using A = wowee::pipeline::WoweeAnniversaryEvents; + if (e.recurrenceKind == A::Weekly) { + if (e.startDay > 6) { + errors.push_back(ctx + + ": Weekly recurrence with startDay " + + std::to_string(e.startDay) + + " > 6 — must be 0 (Sun) through 6 (Sat)"); + } + if (e.durationDays > 7) { + warnings.push_back(ctx + + ": Weekly recurrence with " + "durationDays > 7 — event would " + "overlap with itself across week " + "boundaries"); + } + } else { + if (e.startMonth < 1 || e.startMonth > 12) { + errors.push_back(ctx + + ": startMonth " + + std::to_string(e.startMonth) + + " out of range (must be 1..12 for " + "Yearly / Monthly / OneOff)"); + } + if (e.startDay < 1 || e.startDay > 31) { + errors.push_back(ctx + ": startDay " + + std::to_string(e.startDay) + + " out of range (must be 1..31)"); + } + // Calendar sanity: Feb has 28-29 days, etc. + // The validator doesn't try to be a full + // calendar — just catches the obvious "Feb 30" + // type errors. + if (e.startMonth == 2 && e.startDay > 29) { + errors.push_back(ctx + + ": startDay " + std::to_string(e.startDay) + + " for February — must be 1..29 (28 in " + "non-leap years; the schedule rolls " + "over to Mar 1 in those cases)"); + } + if ((e.startMonth == 4 || e.startMonth == 6 || + e.startMonth == 9 || e.startMonth == 11) && + e.startDay > 30) { + errors.push_back(ctx + + ": startDay 31 for month " + + std::to_string(e.startMonth) + + " — that month only has 30 days"); + } + } + if (!idsSeen.insert(e.eventId).second) { + errors.push_back(ctx + ": duplicate eventId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wanv"] = base + ".wanv"; + 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-wanv: %s.wanv\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu events, all eventIds " + "unique, calendar dates valid\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 handleAnniversaryEventsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-anv") == 0 && i + 1 < argc) { + outRc = handleGenHolidays(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-anv-bonus") == 0 && + i + 1 < argc) { + outRc = handleGenBonus(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-anv-launch") == 0 && + i + 1 < argc) { + outRc = handleGenAnniversary(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wanv") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wanv") == 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_anniversary_events_catalog.hpp b/tools/editor/cli_anniversary_events_catalog.hpp new file mode 100644 index 00000000..84986d58 --- /dev/null +++ b/tools/editor/cli_anniversary_events_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleAnniversaryEventsCatalog(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 7e3685e3..2b616076 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -371,6 +371,8 @@ const char* const kArgRequired[] = { "--gen-cfg", "--gen-cfg-perf", "--gen-cfg-sec", "--info-wcfg", "--validate-wcfg", "--export-wcfg-json", "--import-wcfg-json", + "--gen-anv", "--gen-anv-bonus", "--gen-anv-launch", + "--info-wanv", "--validate-wanv", "--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 2e923e99..59eaeb46 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -165,6 +165,7 @@ #include "cli_loot_modes_catalog.hpp" #include "cli_sky_params_catalog.hpp" #include "cli_server_config_catalog.hpp" +#include "cli_anniversary_events_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -375,6 +376,7 @@ constexpr DispatchFn kDispatchTable[] = { handleLootModesCatalog, handleSkyParamsCatalog, handleServerConfigCatalog, + handleAnniversaryEventsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 8ca95e9b..b2791172 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -123,6 +123,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','L','M','A'}, ".wlma", "loot", "--info-wlma", "Loot mode policy catalog"}, {{'W','S','K','P'}, ".wskp", "world", "--info-wskp", "Sky parameters catalog"}, {{'W','C','F','G'}, ".wcfg", "server", "--info-wcfg", "Server config catalog"}, + {{'W','A','N','V'}, ".wanv", "events", "--info-wanv", "Anniversary & recurring event 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 0f694e37..4e4af86c 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2447,6 +2447,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcfg to a human-editable JSON sidecar (defaults to .wcfg.json; emits configKind + valueKind as int+name; ALL three value carriers (floatValue/intValue/strValue) plus an activeValue derived field rendering only the meaningful one per kind)\n"); std::printf(" --import-wcfg-json [out-base]\n"); std::printf(" Import a .wcfg.json sidecar back into binary .wcfg (configKind int OR \"xprate\"/\"droprate\"/\"honorrate\"/\"restedxp\"/\"realmtype\"/\"worldflag\"/\"performance\"/\"security\"/\"misc\"; valueKind int OR \"float\"/\"int\"/\"bool\"/\"string\"; restartRequired bool OR int)\n"); + std::printf(" --gen-anv [name]\n"); + std::printf(" Emit .wanv 5 yearly holidays (Hallow's End / Winter Veil / Lunar Festival / Children's Week / Brewfest) with payload spell+item bindings\n"); + std::printf(" --gen-anv-bonus [name]\n"); + std::printf(" Emit .wanv 4 weekly bonus events (Double XP weekend / Double Honor / Pet Battle weekend / BG Bonus Day) with weekly recurrence\n"); + std::printf(" --gen-anv-launch [name]\n"); + std::printf(" Emit .wanv 3 game-launch anniversaries (WoW Nov 23 / TBC Jan 16 / WotLK Nov 13) with overlapping celebration windows\n"); + std::printf(" --info-wanv [--json]\n"); + std::printf(" Print WANV entries (id / kind / recurrence / schedule / duration / payload spell+item / name)\n"); + std::printf(" --validate-wanv [--json]\n"); + std::printf(" Static checks: id+name required, eventKind 0..6 OR 255 Misc, recurrenceKind 0..3, durationDays > 0, no duplicate eventIds; per-recurrence schedule validity (Weekly: startDay 0..6 weekday, durationDays <= 7; Yearly/Monthly/OneOff: startMonth 1..12, startDay 1..31 with calendar sanity — Feb < 30, Apr/Jun/Sep/Nov < 31)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 94898362..6c2aa118 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -145,6 +145,7 @@ constexpr FormatRow kFormats[] = { {"WLMA", ".wlma", "loot", "GroupLoot CMSG_LOOT_METHOD policy", "Loot mode policy catalog (FFA / RR / Master / NBG / Personal)"}, {"WSKP", ".wskp", "world", "LightParams.dbc + Light.dbc diurnal","Sky parameters catalog (per-zone keyframes)"}, {"WCFG", ".wcfg", "server", "worldserver.conf flat-text config", "Server config catalog (polymorphic Float/Int/Bool/String values)"}, + {"WANV", ".wanv", "events", "GameEvent SQL + per-holiday script", "Anniversary & recurring event catalog (cron-like scheduling)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine