From 2666b976c990c30adc63e85104b26a7693286e21 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:05:33 -0700 Subject: [PATCH] feat(pipeline): add WHOL (Wowee Holiday) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 44th open format — replaces Holidays.dbc + HolidayDescriptions.dbc + HolidayNames.dbc plus the AzerothCore-style game_event SQL tables. Defines time-gated world events: seasonal holidays (Hallow's End / Brewfest / Winter Veil), weekly call-to-arms BG bonuses, world-PvP windows (Wintergrasp), one-shot specials, and recurring daily / weekly resets. Each holiday has a recurrence rule (Annual / Monthly / WeeklyRecur / OneTime), a calendar window (startMonth + startDay + durationHours), and optional cross-refs to a feature creature, an intro quest, and a token / item reward issued during the window. Cross-references with prior formats — holidayQuestId points at WQT.questId, bossCreatureId points at WCRT.creatureId, itemRewardId points at WIT.itemId, and areaIdGate / mapIdGate point at WMS.areaId / WMS.mapId. CLI: --gen-holidays (3 seasonal starter), --gen-holidays-weekly (3 weekly BG call-to-arms windows), --gen-holidays-special (3 world-PvP / lunar / children events), --info-whol, --validate-whol with --json variants. Validator catches id=0/duplicates, kind / recurrence out of range, durationHours=0, invalid month/day for non-weekly recurrence, and calendar-only events (no quest + boss + reward) as a warning. --- CMakeLists.txt | 3 + include/pipeline/wowee_holidays.hpp | 117 +++++++++++ src/pipeline/wowee_holidays.cpp | 290 ++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_holidays_catalog.cpp | 255 ++++++++++++++++++++++ tools/editor/cli_holidays_catalog.hpp | 11 + 8 files changed, 690 insertions(+) create mode 100644 include/pipeline/wowee_holidays.hpp create mode 100644 src/pipeline/wowee_holidays.cpp create mode 100644 tools/editor/cli_holidays_catalog.cpp create mode 100644 tools/editor/cli_holidays_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fe1fb8f..66c82681 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -631,6 +631,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_cinematics.cpp src/pipeline/wowee_glyphs.cpp src/pipeline/wowee_vehicles.cpp + src/pipeline/wowee_holidays.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1408,6 +1409,7 @@ add_executable(wowee_editor tools/editor/cli_cinematics_catalog.cpp tools/editor/cli_glyphs_catalog.cpp tools/editor/cli_vehicles_catalog.cpp + tools/editor/cli_holidays_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1517,6 +1519,7 @@ add_executable(wowee_editor src/pipeline/wowee_cinematics.cpp src/pipeline/wowee_glyphs.cpp src/pipeline/wowee_vehicles.cpp + src/pipeline/wowee_holidays.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_holidays.hpp b/include/pipeline/wowee_holidays.hpp new file mode 100644 index 00000000..b99f9095 --- /dev/null +++ b/include/pipeline/wowee_holidays.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Holiday catalog (.whol) — novel replacement for +// Blizzard's Holidays.dbc + HolidayDescriptions.dbc + +// HolidayNames.dbc plus the AzerothCore-style game_event SQL +// tables. The 44th open format added to the editor. +// +// Defines time-gated world events: seasonal holidays (Hallow's +// End, Brewfest, Winter Veil), weekly call-to-arms bonuses +// (BG / dungeon weekend buffs), one-shot special events, and +// recurring world PvP encouragement windows. Each holiday has +// a recurrence rule, a calendar window, optional cross-refs +// to a feature creature (e.g. Headless Horseman), an intro +// quest, and a token / item reward issued during the window. +// +// Cross-references with previously-added formats: +// WHOL.entry.holidayQuestId → WQT.entry.questId +// WHOL.entry.bossCreatureId → WCRT.creatureId +// WHOL.entry.itemRewardId → WIT.itemId +// WHOL.entry.areaIdGate → WMS.area.areaId +// WHOL.entry.mapIdGate → WMS.map.mapId +// +// Binary layout (little-endian): +// magic[4] = "WHOL" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// holidayId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// holidayKind (uint8) / recurrence (uint8) / +// startMonth (uint8) / startDay (uint8) +// durationHours (uint16) / pad[2] +// holidayQuestId (uint32) +// bossCreatureId (uint32) +// itemRewardId (uint32) +// areaIdGate (uint32) +// mapIdGate (uint32) +struct WoweeHoliday { + enum HolidayKind : uint8_t { + Seasonal = 0, // Hallow's End, Brewfest, ... + Weekly = 1, // call-to-arms BG bonuses + Daily = 2, // daily quest reset window + WorldPvp = 3, // Wintergrasp / Tol Barad windows + OneShot = 4, // anniversary, beta launch + Special = 5, // Children's Week, anniversary + }; + + enum Recurrence : uint8_t { + Annual = 0, // once per year + Monthly = 1, // once per month + WeeklyRecur = 2, // every week + OneTime = 3, // single occurrence + }; + + struct Entry { + uint32_t holidayId = 0; + std::string name; + std::string description; + std::string iconPath; + uint8_t holidayKind = Seasonal; + uint8_t recurrence = Annual; + uint8_t startMonth = 1; // 1-12 (0 = unused) + uint8_t startDay = 1; // 1-31 + uint16_t durationHours = 168; // default 1 week + uint32_t holidayQuestId = 0; // WQT cross-ref + uint32_t bossCreatureId = 0; // WCRT cross-ref + uint32_t itemRewardId = 0; // WIT cross-ref + uint32_t areaIdGate = 0; // WMS cross-ref + uint32_t mapIdGate = 0; // WMS cross-ref + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t holidayId) const; + + static const char* holidayKindName(uint8_t k); + static const char* recurrenceName(uint8_t r); +}; + +class WoweeHolidayLoader { +public: + static bool save(const WoweeHoliday& cat, + const std::string& basePath); + static WoweeHoliday load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-holidays* variants. + // + // makeStarter — 3 seasonal holidays (Hallow's End / + // Brewfest / Winter Veil) with boss / + // reward / quest cross-refs. + // makeWeekly — 3 weekly call-to-arms windows + // (Warsong Gulch / Arathi Basin / Eye + // of the Storm BG bonuses). + // makeSpecial — 3 special / world-PvP events + // (Wintergrasp / Lunar Festival / + // Children's Week). + static WoweeHoliday makeStarter(const std::string& catalogName); + static WoweeHoliday makeWeekly(const std::string& catalogName); + static WoweeHoliday makeSpecial(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_holidays.cpp b/src/pipeline/wowee_holidays.cpp new file mode 100644 index 00000000..7410227d --- /dev/null +++ b/src/pipeline/wowee_holidays.cpp @@ -0,0 +1,290 @@ +#include "pipeline/wowee_holidays.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'H', 'O', 'L'}; +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) != ".whol") { + base += ".whol"; + } + return base; +} + +} // namespace + +const WoweeHoliday::Entry* +WoweeHoliday::findById(uint32_t holidayId) const { + for (const auto& e : entries) if (e.holidayId == holidayId) return &e; + return nullptr; +} + +const char* WoweeHoliday::holidayKindName(uint8_t k) { + switch (k) { + case Seasonal: return "seasonal"; + case Weekly: return "weekly"; + case Daily: return "daily"; + case WorldPvp: return "world-pvp"; + case OneShot: return "one-shot"; + case Special: return "special"; + default: return "unknown"; + } +} + +const char* WoweeHoliday::recurrenceName(uint8_t r) { + switch (r) { + case Annual: return "annual"; + case Monthly: return "monthly"; + case WeeklyRecur: return "weekly"; + case OneTime: return "one-time"; + default: return "unknown"; + } +} + +bool WoweeHolidayLoader::save(const WoweeHoliday& 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.holidayId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writePOD(os, e.holidayKind); + writePOD(os, e.recurrence); + writePOD(os, e.startMonth); + writePOD(os, e.startDay); + writePOD(os, e.durationHours); + uint8_t pad[2] = {0, 0}; + os.write(reinterpret_cast(pad), 2); + writePOD(os, e.holidayQuestId); + writePOD(os, e.bossCreatureId); + writePOD(os, e.itemRewardId); + writePOD(os, e.areaIdGate); + writePOD(os, e.mapIdGate); + } + return os.good(); +} + +WoweeHoliday WoweeHolidayLoader::load(const std::string& basePath) { + WoweeHoliday 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.holidayId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.holidayKind) || + !readPOD(is, e.recurrence) || + !readPOD(is, e.startMonth) || + !readPOD(is, e.startDay) || + !readPOD(is, e.durationHours)) { + out.entries.clear(); return out; + } + uint8_t pad[2]; + is.read(reinterpret_cast(pad), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.holidayQuestId) || + !readPOD(is, e.bossCreatureId) || + !readPOD(is, e.itemRewardId) || + !readPOD(is, e.areaIdGate) || + !readPOD(is, e.mapIdGate)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeHolidayLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeHoliday WoweeHolidayLoader::makeStarter(const std::string& catalogName) { + WoweeHoliday c; + c.name = catalogName; + { + WoweeHoliday::Entry e; + e.holidayId = 1; e.name = "Hallow's End"; + e.description = "Trick-or-treat through cities, then defeat " + "the Headless Horseman in Scarlet Monastery."; + e.iconPath = "Interface/Calendar/Holidays/Calendar_HallowsEnd.blp"; + e.holidayKind = WoweeHoliday::Seasonal; + e.recurrence = WoweeHoliday::Annual; + e.startMonth = 10; e.startDay = 18; // Oct 18 — Nov 1 + e.durationHours = 14 * 24; + e.bossCreatureId = 23682; // Headless Horseman + e.itemRewardId = 33226; // Tricky Treat + e.holidayQuestId = 11131; // intro quest + c.entries.push_back(e); + } + { + WoweeHoliday::Entry e; + e.holidayId = 2; e.name = "Brewfest"; + e.description = "Sample brews, ride a barrel, and defeat " + "Coren Direbrew in Blackrock Depths."; + e.iconPath = "Interface/Calendar/Holidays/Calendar_Brewfest.blp"; + e.holidayKind = WoweeHoliday::Seasonal; + e.recurrence = WoweeHoliday::Annual; + e.startMonth = 9; e.startDay = 20; // Sep 20 — Oct 6 + e.durationHours = 16 * 24; + e.bossCreatureId = 23872; // Coren Direbrew + e.itemRewardId = 37829; // Brewfest Prize Token + e.holidayQuestId = 11293; + c.entries.push_back(e); + } + { + WoweeHoliday::Entry e; + e.holidayId = 3; e.name = "Feast of Winter Veil"; + e.description = "Decorate Ironforge / Orgrimmar, exchange " + "gifts, and rescue Metzen the Reindeer."; + e.iconPath = "Interface/Calendar/Holidays/Calendar_WinterVeil.blp"; + e.holidayKind = WoweeHoliday::Seasonal; + e.recurrence = WoweeHoliday::Annual; + e.startMonth = 12; e.startDay = 15; // Dec 15 — Jan 2 + e.durationHours = 19 * 24; + e.itemRewardId = 21525; // Green Winter Hat + e.holidayQuestId = 7062; + c.entries.push_back(e); + } + return c; +} + +WoweeHoliday WoweeHolidayLoader::makeWeekly(const std::string& catalogName) { + WoweeHoliday c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t bgZone, + uint32_t questId, const char* desc) { + WoweeHoliday::Entry e; + e.holidayId = id; e.name = name; e.description = desc; + e.iconPath = "Interface/Icons/Inv_Misc_PvpTrinket_05.blp"; + e.holidayKind = WoweeHoliday::Weekly; + e.recurrence = WoweeHoliday::WeeklyRecur; + // Start any Friday at 00:00 — duration 4 days (Fri-Mon). + e.startMonth = 0; e.startDay = 0; + e.durationHours = 4 * 24; + e.areaIdGate = bgZone; + e.holidayQuestId = questId; + c.entries.push_back(e); + }; + add(100, "Call to Arms: Warsong Gulch", 3277, 11335, + "Bonus honor and reputation in Warsong Gulch this weekend."); + add(101, "Call to Arms: Arathi Basin", 3358, 11336, + "Bonus honor and reputation in Arathi Basin this weekend."); + add(102, "Call to Arms: Eye of the Storm", 3820, 11337, + "Bonus honor and reputation in Eye of the Storm this weekend."); + return c; +} + +WoweeHoliday WoweeHolidayLoader::makeSpecial(const std::string& catalogName) { + WoweeHoliday c; + c.name = catalogName; + { + WoweeHoliday::Entry e; + e.holidayId = 200; e.name = "Wintergrasp Battle"; + e.description = "World-PvP siege of Wintergrasp Fortress — " + "control rewards Stone Keeper's Shards."; + e.iconPath = "Interface/Icons/Inv_Misc_PvpTrinket_03.blp"; + e.holidayKind = WoweeHoliday::WorldPvp; + e.recurrence = WoweeHoliday::WeeklyRecur; + // 2.5 hours, every 3 hours. + e.startMonth = 0; e.startDay = 0; + e.durationHours = 3; + e.itemRewardId = 43228; // Stone Keeper's Shard + e.mapIdGate = 571; // Northrend + e.areaIdGate = 4197; // Wintergrasp + c.entries.push_back(e); + } + { + WoweeHoliday::Entry e; + e.holidayId = 201; e.name = "Lunar Festival"; + e.description = "Honor the Elders across Azeroth, " + "collect Coins of Ancestry, and earn " + "Festive Lanterns."; + e.iconPath = "Interface/Calendar/Holidays/Calendar_LunarFestival.blp"; + e.holidayKind = WoweeHoliday::Seasonal; + e.recurrence = WoweeHoliday::Annual; + e.startMonth = 1; e.startDay = 26; // late Jan — early Feb + e.durationHours = 16 * 24; + e.itemRewardId = 21100; // Coin of Ancestry + e.holidayQuestId = 8867; + c.entries.push_back(e); + } + { + WoweeHoliday::Entry e; + e.holidayId = 202; e.name = "Children's Week"; + e.description = "Escort an orphan around Azeroth and earn " + "rare non-combat pets and a tabard."; + e.iconPath = "Interface/Calendar/Holidays/Calendar_ChildrensWeek.blp"; + e.holidayKind = WoweeHoliday::Special; + e.recurrence = WoweeHoliday::Annual; + e.startMonth = 5; e.startDay = 1; // first week of May + e.durationHours = 7 * 24; + e.itemRewardId = 23007; // Curmudgeon's Payoff + e.holidayQuestId = 10942; + 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 0f6c8196..292574a6 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -128,6 +128,8 @@ const char* const kArgRequired[] = { "--gen-vehicles", "--gen-vehicles-siege", "--gen-vehicles-flying", "--info-wvhc", "--validate-wvhc", "--export-wvhc-json", "--import-wvhc-json", + "--gen-holidays", "--gen-holidays-weekly", "--gen-holidays-special", + "--info-whol", "--validate-whol", "--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 34b885f4..0f62b541 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -71,6 +71,7 @@ #include "cli_cinematics_catalog.hpp" #include "cli_glyphs_catalog.hpp" #include "cli_vehicles_catalog.hpp" +#include "cli_holidays_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -183,6 +184,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCinematicsCatalog, handleGlyphsCatalog, handleVehiclesCatalog, + handleHolidaysCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 70bb9a4e..d4b1109b 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1319,6 +1319,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wvhc to a human-editable JSON sidecar with nested seat arrays (defaults to .wvhc.json)\n"); std::printf(" --import-wvhc-json [out-base]\n"); std::printf(" Import a .wvhc.json sidecar back into binary .wvhc (accepts vehicleKind/movementKind/powerType int OR name string)\n"); + std::printf(" --gen-holidays [name]\n"); + std::printf(" Emit .whol starter: 3 seasonal holidays (Hallow's End / Brewfest / Winter Veil) with boss+reward+quest cross-refs\n"); + std::printf(" --gen-holidays-weekly [name]\n"); + std::printf(" Emit .whol 3 weekly call-to-arms BG bonus windows (Warsong Gulch / Arathi Basin / Eye of the Storm)\n"); + std::printf(" --gen-holidays-special [name]\n"); + std::printf(" Emit .whol 3 special events (Wintergrasp world-PvP / Lunar Festival / Children's Week)\n"); + std::printf(" --info-whol [--json]\n"); + std::printf(" Print WHOL entries (id / kind / recurrence / start MM/DD / duration / quest+boss+reward IDs / name)\n"); + std::printf(" --validate-whol [--json]\n"); + std::printf(" Static checks: id>0+unique, kind 0..5, recurrence 0..3, duration>0, valid month/day for non-weekly, calendar-only warning\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_holidays_catalog.cpp b/tools/editor/cli_holidays_catalog.cpp new file mode 100644 index 00000000..509ccdd2 --- /dev/null +++ b/tools/editor/cli_holidays_catalog.cpp @@ -0,0 +1,255 @@ +#include "cli_holidays_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_holidays.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWholExt(std::string base) { + stripExt(base, ".whol"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeHoliday& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeHolidayLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.whol\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeHoliday& c, + const std::string& base) { + std::printf("Wrote %s.whol\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" holidays : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterHolidays"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWholExt(base); + auto c = wowee::pipeline::WoweeHolidayLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-holidays")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWeekly(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WeeklyHolidays"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWholExt(base); + auto c = wowee::pipeline::WoweeHolidayLoader::makeWeekly(name); + if (!saveOrError(c, base, "gen-holidays-weekly")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenSpecial(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "SpecialHolidays"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWholExt(base); + auto c = wowee::pipeline::WoweeHolidayLoader::makeSpecial(name); + if (!saveOrError(c, base, "gen-holidays-special")) 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 = stripWholExt(base); + if (!wowee::pipeline::WoweeHolidayLoader::exists(base)) { + std::fprintf(stderr, "WHOL not found: %s.whol\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeHolidayLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["whol"] = base + ".whol"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"holidayId", e.holidayId}, + {"name", e.name}, + {"description", e.description}, + {"iconPath", e.iconPath}, + {"holidayKind", e.holidayKind}, + {"holidayKindName", wowee::pipeline::WoweeHoliday::holidayKindName(e.holidayKind)}, + {"recurrence", e.recurrence}, + {"recurrenceName", wowee::pipeline::WoweeHoliday::recurrenceName(e.recurrence)}, + {"startMonth", e.startMonth}, + {"startDay", e.startDay}, + {"durationHours", e.durationHours}, + {"holidayQuestId", e.holidayQuestId}, + {"bossCreatureId", e.bossCreatureId}, + {"itemRewardId", e.itemRewardId}, + {"areaIdGate", e.areaIdGate}, + {"mapIdGate", e.mapIdGate}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WHOL: %s.whol\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" holidays : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind recur start dur(h) quest boss item name\n"); + for (const auto& e : c.entries) { + char start[16]; + std::snprintf(start, sizeof(start), "%02u/%02u", + e.startMonth, e.startDay); + std::printf(" %4u %-9s %-8s %-7s %5u %5u %5u %5u %s\n", + e.holidayId, + wowee::pipeline::WoweeHoliday::holidayKindName(e.holidayKind), + wowee::pipeline::WoweeHoliday::recurrenceName(e.recurrence), + start, e.durationHours, + e.holidayQuestId, e.bossCreatureId, + e.itemRewardId, 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 = stripWholExt(base); + if (!wowee::pipeline::WoweeHolidayLoader::exists(base)) { + std::fprintf(stderr, + "validate-whol: WHOL not found: %s.whol\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeHolidayLoader::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.holidayId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.holidayId == 0) + errors.push_back(ctx + ": holidayId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.holidayKind > wowee::pipeline::WoweeHoliday::Special) { + errors.push_back(ctx + ": holidayKind " + + std::to_string(e.holidayKind) + " not in 0..5"); + } + if (e.recurrence > wowee::pipeline::WoweeHoliday::OneTime) { + errors.push_back(ctx + ": recurrence " + + std::to_string(e.recurrence) + " not in 0..3"); + } + if (e.durationHours == 0) { + errors.push_back(ctx + + ": durationHours=0 (holiday window has no length)"); + } + // Annual / Monthly / OneTime require a real calendar + // start. WeeklyRecur is exempt — it triggers based on + // weekday rather than fixed date. + if (e.recurrence != wowee::pipeline::WoweeHoliday::WeeklyRecur) { + if (e.startMonth == 0 || e.startMonth > 12) { + errors.push_back(ctx + ": startMonth " + + std::to_string(e.startMonth) + + " not in 1..12 (required for non-weekly recurrence)"); + } + if (e.startDay == 0 || e.startDay > 31) { + errors.push_back(ctx + ": startDay " + + std::to_string(e.startDay) + " not in 1..31"); + } + } + // Holidays with no quest, no boss, AND no reward have + // no in-game presence beyond a calendar entry — useful + // for simple banner-only events but worth flagging. + if (e.holidayQuestId == 0 && e.bossCreatureId == 0 && + e.itemRewardId == 0) { + warnings.push_back(ctx + + ": no quest, boss, or reward — calendar-only event"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.holidayId) { + errors.push_back(ctx + ": duplicate holidayId"); + break; + } + } + idsSeen.push_back(e.holidayId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["whol"] = base + ".whol"; + 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-whol: %s.whol\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu holidays, all holidayIds 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 handleHolidaysCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-holidays") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-holidays-weekly") == 0 && i + 1 < argc) { + outRc = handleGenWeekly(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-holidays-special") == 0 && i + 1 < argc) { + outRc = handleGenSpecial(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-whol") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-whol") == 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_holidays_catalog.hpp b/tools/editor/cli_holidays_catalog.hpp new file mode 100644 index 00000000..39194988 --- /dev/null +++ b/tools/editor/cli_holidays_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleHolidaysCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee