feat(Core/Events): Add dynamic holiday date calculator (#24038)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: sudlud <sudlud@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-01-02 14:35:23 -06:00
committed by GitHub
parent 4d4747808c
commit c1d753f4ef
7 changed files with 2082 additions and 31 deletions

View File

@@ -0,0 +1,30 @@
-- correctly link brewfest building event in ironforge to brewfest
UPDATE `game_event` SET `holiday` = 372, `holidayStage` = 1 WHERE `eventEntry` = 70;
-- remove start and end times from automatically handled events
UPDATE `game_event` SET `start_time` = NULL, `end_time` = NULL WHERE `eventEntry` IN
(
1, -- Midsummer Fire Festival
2, -- Winter Veil
3, -- Darkmoon Faire (Terokkar Forest)
4, -- Darkmoon Faire (Elwynn Forest)
5, -- Darkmoon Faire (Mulgore)
7, -- Lunar Festival
8, -- Love is in the Air
9, -- Noblegarden
10, -- Children's Week
11, -- Harvest Festival
12, -- Hallow's End
23, -- Darkmoon Faire Building (Elwynn Forest)
24, -- Brewfest
26, -- Pilgrim's Bounty
50, -- Pirates' Day
51, -- Day of the Dead
70, -- Brewfest Building (Iron Forge)
71, -- Darkmoon Faire Building (Mulgore)
72, -- Fireworks Spectacular,
77 -- Darkmoon Faire Building (Terokkar Forest)
);
-- drop unused table
DROP TABLE IF EXISTS `holiday_dates`;

View File

@@ -98,7 +98,6 @@ void WorldDatabaseConnection::DoPrepareStatements()
PrepareStatement(WORLD_SEL_GAME_EVENT_BATTLEGROUND_DATA, "SELECT eventEntry, bgflag FROM game_event_battleground_holiday", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_GAME_EVENT_POOL_DATA, "SELECT pool_template.entry, game_event_pool.eventEntry FROM pool_template JOIN game_event_pool ON pool_template.entry = game_event_pool.pool_entry", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_GAME_EVENT_ARENA_SEASON, "SELECT eventEntry FROM game_event_arena_seasons WHERE season = ?", CONNECTION_SYNCH);
PrepareStatement(WORLD_SEL_GAME_EVENT_HOLIDAY_DATES, "SELECT id, date_id, date_value, holiday_duration FROM holiday_dates", CONNECTION_SYNCH);
PrepareStatement(WORLD_DEL_GAME_EVENT_CREATURE, "DELETE FROM game_event_creature WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(WORLD_DEL_GAME_EVENT_MODEL_EQUIP, "DELETE FROM game_event_model_equip WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(WORLD_SEL_GAME_EVENT_NPC_VENDOR, "SELECT eventEntry, guid, item, maxcount, incrtime, ExtendedCost FROM game_event_npc_vendor ORDER BY guid, slot ASC", CONNECTION_SYNCH);

View File

@@ -104,7 +104,6 @@ enum WorldDatabaseStatements : uint32
WORLD_SEL_GAME_EVENT_BATTLEGROUND_DATA,
WORLD_SEL_GAME_EVENT_POOL_DATA,
WORLD_SEL_GAME_EVENT_ARENA_SEASON,
WORLD_SEL_GAME_EVENT_HOLIDAY_DATES,
WORLD_DEL_GAME_EVENT_CREATURE,
WORLD_DEL_GAME_EVENT_MODEL_EQUIP,
WORLD_SEL_GAME_EVENT_NPC_VENDOR,

View File

@@ -21,6 +21,7 @@
#include "DisableMgr.h"
#include "GameObjectAI.h"
#include "GameTime.h"
#include "HolidayDateCalculator.h"
#include "Language.h"
#include "Log.h"
#include "MapMgr.h"
@@ -34,7 +35,7 @@
#include "WorldSessionMgr.h"
#include "WorldState.h"
#include "WorldStatePackets.h"
#include <time.h>
#include <chrono>
GameEventMgr* GameEventMgr::instance()
{
@@ -1070,51 +1071,126 @@ void GameEventMgr::LoadFromDB()
void GameEventMgr::LoadHolidayDates()
{
uint32 oldMSTime = getMSTime();
uint32 const oldMSTime = getMSTime();
uint32 dynamicCount = 0;
uint32 dbCount = 0;
WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_GAME_EVENT_HOLIDAY_DATES);
PreparedQueryResult result = WorldDatabase.Query(stmt);
// Step 1: Generate dynamic holiday dates based on current year
std::chrono::system_clock::time_point const now = std::chrono::system_clock::now();
std::time_t const nowTime = std::chrono::system_clock::to_time_t(now);
std::tm localTime = {};
#ifdef _WIN32
localtime_s(&localTime, &nowTime);
#else
localtime_r(&nowTime, &localTime);
#endif
int const currentYear = localTime.tm_year + 1900;
if (!result)
for (auto const& rule : HolidayDateCalculator::GetHolidayRules())
{
LOG_WARN("server.loading", ">> Loaded 0 holiday dates. DB table `holiday_dates` is empty.");
return;
HolidaysEntry* entry = const_cast<HolidaysEntry*>(sHolidaysStore.LookupEntry(rule.holidayId));
if (!entry)
{
LOG_INFO("server.loading", ">> Holiday {} not found in DBC - cannot set dynamic dates", rule.holidayId);
continue;
}
uint32 count = 0;
// Special handling for Darkmoon Faire - needs multiple dates per year (4 occurrences)
if (rule.type == HolidayCalculationType::DARKMOON_FAIRE)
{
int const locationOffset = rule.month;
std::vector<uint32_t> const dates = HolidayDateCalculator::GetDarkmoonFaireDates(locationOffset, currentYear - 1, 4, rule.offset);
uint8 dateId = 0;
for (auto const& packedDate : dates)
{
if (dateId >= MAX_HOLIDAY_DATES)
break;
entry->Date[dateId++] = packedDate;
++dynamicCount;
}
// Darkmoon Faire lasts 7 days (168 hours) - set Duration if not already set
if (!entry->Duration[0])
entry->Duration[0] = 168; // 7 days in hours
auto itr = std::lower_bound(ModifiedHolidays.begin(), ModifiedHolidays.end(), entry->Id);
if (itr == ModifiedHolidays.end() || *itr != entry->Id)
ModifiedHolidays.insert(itr, entry->Id);
continue;
}
// Generate dates for current year + 2 ahead (year capped at 2030 due to 5-bit client limitation)
for (int yearOffset = -1; yearOffset <= 2; ++yearOffset)
{
int const year = currentYear + yearOffset;
if (year > 2030)
break;
uint8 const dateId = static_cast<uint8>(yearOffset + 1);
if (dateId >= MAX_HOLIDAY_DATES)
break;
uint32_t const packedDate = HolidayDateCalculator::GetPackedHolidayDate(rule.holidayId, year);
entry->Date[dateId] = packedDate;
// Debug: decode and log the date
std::tm const date = HolidayDateCalculator::UnpackDate(packedDate);
LOG_DEBUG("server.loading", ">> Holiday {} Date[{}] = {}-{:02d}-{:02d}",
rule.holidayId, dateId, date.tm_year + 1900, date.tm_mon + 1, date.tm_mday);
++dynamicCount;
}
auto itr = std::lower_bound(ModifiedHolidays.begin(), ModifiedHolidays.end(), entry->Id);
if (itr == ModifiedHolidays.end() || *itr != entry->Id)
ModifiedHolidays.insert(itr, entry->Id);
}
// Step 2: Check game_event.start_time for overrides (allows custom servers to override calculated dates)
// Only use as override if start_time year >= current year (ignore old static dates)
QueryResult result = WorldDatabase.Query("SELECT holiday, UNIX_TIMESTAMP(start_time) FROM game_event WHERE holiday != 0 AND start_time > '2000-12-31'");
if (result)
{
do
{
Field* fields = result->Fetch();
uint32 holidayId = fields[0].Get<uint32>();
uint32 const holidayId = fields[0].Get<uint32>();
HolidaysEntry* entry = const_cast<HolidaysEntry*>(sHolidaysStore.LookupEntry(holidayId));
if (!entry)
{
LOG_ERROR("sql.sql", "holiday_dates entry has invalid holiday id {}.", holidayId);
continue;
}
uint8 dateId = fields[1].Get<uint8>();
if (dateId >= MAX_HOLIDAY_DATES)
{
LOG_ERROR("sql.sql", "holiday_dates entry has out of range date_id {}.", dateId);
if (fields[1].IsNull())
continue;
}
entry->Date[dateId] = fields[2].Get<uint32>();
if (uint32 duration = fields[3].Get<uint32>())
entry->Duration[0] = duration;
time_t const startTime = fields[1].Get<uint64>();
if (startTime == 0)
continue;
auto itr = std::lower_bound(ModifiedHolidays.begin(), ModifiedHolidays.end(), entry->Id);
if (itr == ModifiedHolidays.end() || *itr != entry->Id)
{
ModifiedHolidays.insert(itr, entry->Id);
}
std::tm const timeInfo = Acore::Time::TimeBreakdown(startTime);
++count;
int const year = timeInfo.tm_year + 1900;
// Only override if start_time is current year or later (ignore old static dates)
if (year < currentYear || year > 2030)
continue;
// Pack the date in WoW format and override Date[0]
uint32_t const yearOffset = static_cast<uint32_t>(year - 2000);
uint32_t const month = static_cast<uint32_t>(timeInfo.tm_mon);
uint32_t const day = static_cast<uint32_t>(timeInfo.tm_mday - 1);
uint32_t const weekday = static_cast<uint32_t>(timeInfo.tm_wday);
entry->Date[0] = (yearOffset << 24) | (month << 20) | (day << 14) | (weekday << 11);
++dbCount;
} while (result->NextRow());
}
LOG_INFO("server.loading", ">> Loaded {} Holiday Dates in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", ">> Loaded {} Holiday Dates ({} dynamic, {} game_event overrides) in {} ms",
dynamicCount + dbCount, dynamicCount, dbCount, GetMSTimeDiffToNow(oldMSTime));
}
uint32 GameEventMgr::GetNPCFlag(Creature* cr)
@@ -1926,7 +2002,7 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event)
}
else
{
// date is due and not a singleDate event, try with next DBC date (modified by holiday_dates)
// date is due and not a singleDate event, try with next DBC date (dynamically calculated or overridden by game_event.start_time)
// if none is found we don't modify start date and use the one in game_event
}
}

View File

@@ -0,0 +1,582 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "HolidayDateCalculator.h"
#include "SharedDefines.h"
#include <cmath>
// Constants for astronomical calculations
constexpr double PI = 3.14159265358979323846;
constexpr double DEG_TO_RAD = PI / 180.0;
// Helper: sin/cos in degrees
inline double sind(double deg) { return std::sin(deg * DEG_TO_RAD); }
// Static holiday rules configuration
static const std::vector<HolidayRule> HolidayRules = {
// Lunar Festival: Chinese New Year - 1 day (event starts day before CNY)
{ HOLIDAY_LUNAR_FESTIVAL, HolidayCalculationType::LUNAR_NEW_YEAR, 0, 0, 0, -1 },
// Love is in the Air: First Monday on or after Feb 3
{ HOLIDAY_LOVE_IS_IN_THE_AIR, HolidayCalculationType::WEEKDAY_ON_OR_AFTER, 2, 3, static_cast<int>(Weekday::MONDAY), 0 },
// Noblegarden: Day after Easter Sunday (Easter + 1 day)
{ HOLIDAY_NOBLEGARDEN, HolidayCalculationType::EASTER_OFFSET, 0, 0, 0, 1 },
// Children's Week: First Monday on or after Apr 25 (Monday closest to May 1)
{ HOLIDAY_CHILDRENS_WEEK, HolidayCalculationType::WEEKDAY_ON_OR_AFTER, 4, 25, static_cast<int>(Weekday::MONDAY), 0 },
// Midsummer Fire Festival: Fixed Jun 21
{ HOLIDAY_FIRE_FESTIVAL, HolidayCalculationType::FIXED_DATE, 6, 21, 0, 0 },
// Fireworks Spectacular: Fixed Jul 4
{ HOLIDAY_FIREWORKS_SPECTACULAR, HolidayCalculationType::FIXED_DATE, 7, 4, 0, 0 },
// Pirates' Day: Fixed Sep 19
{ HOLIDAY_PIRATES_DAY, HolidayCalculationType::FIXED_DATE, 9, 19, 0, 0 },
// Brewfest: Oktoberfest rule - first Saturday on/after Sept 15, minus 7 for holidayStage offset
{ HOLIDAY_BREWFEST, HolidayCalculationType::WEEKDAY_ON_OR_AFTER, 9, 15, static_cast<int>(Weekday::SATURDAY), -7 },
// Harvest Festival: 2 days before autumn equinox (Sept 20-21)
{ HOLIDAY_HARVEST_FESTIVAL, HolidayCalculationType::AUTUMN_EQUINOX, 0, 0, 0, -2 },
// Hallow's End: Fixed Oct 18
{ HOLIDAY_HALLOWS_END, HolidayCalculationType::FIXED_DATE, 10, 18, 0, 0 },
// Day of the Dead: Fixed Nov 1
{ HOLIDAY_DAY_OF_DEAD, HolidayCalculationType::FIXED_DATE, 11, 1, 0, 0 },
// Pilgrim's Bounty: Sunday before Thanksgiving (4th Thursday - 4 days)
{ HOLIDAY_PILGRIMS_BOUNTY, HolidayCalculationType::NTH_WEEKDAY, 11, 4, static_cast<int>(Weekday::THURSDAY), -4 },
// Winter Veil: 6 days before winter solstice (Dec 15-16)
{ HOLIDAY_FEAST_OF_WINTER_VEIL, HolidayCalculationType::WINTER_SOLSTICE, 0, 0, 0, -6 },
// Darkmoon Faire: First Sunday of months matching (month % 3 == locationOffset)
// Rotates monthly: Mulgore (Jan) -> Terokkar (Feb) -> Elwynn (Mar) -> repeat
// rule.month stores the location offset
// rule.offset is -2 (building phase starts Friday, 2 days before faire opens on Sunday)
{ HOLIDAY_DARKMOON_FAIRE_ELWYNN, HolidayCalculationType::DARKMOON_FAIRE, 0, 0, 0, -2 }, // Mar, Jun, Sep, Dec
{ HOLIDAY_DARKMOON_FAIRE_THUNDER, HolidayCalculationType::DARKMOON_FAIRE, 1, 0, 0, -2 }, // Jan, Apr, Jul, Oct
{ HOLIDAY_DARKMOON_FAIRE_SHATTRATH, HolidayCalculationType::DARKMOON_FAIRE, 2, 0, 0, -2 } // Feb, May, Aug, Nov
};
const std::vector<HolidayRule>& HolidayDateCalculator::GetHolidayRules()
{
return HolidayRules;
}
std::tm HolidayDateCalculator::CalculateEasterSunday(int year)
{
// Anonymous Gregorian algorithm (Computus)
// Reference: https://en.wikipedia.org/wiki/Date_of_Easter#Anonymous_Gregorian_algorithm
int const a = year % 19;
int const b = year / 100;
int const c = year % 100;
int const d = b / 4;
int const e = b % 4;
int const f = (b + 8) / 25;
int const g = (b - f + 1) / 3;
int const h = (19 * a + b - d - g + 15) % 30;
int const i = c / 4;
int const k = c % 4;
int const l = (32 + 2 * e + 2 * i - h - k) % 7;
int const m = (a + 11 * h + 22 * l) / 451;
int const month = (h + l - 7 * m + 114) / 31;
int const day = ((h + l - 7 * m + 114) % 31) + 1;
std::tm result = {};
result.tm_year = year - 1900;
result.tm_mon = month - 1;
result.tm_mday = day;
mktime(&result); // Normalize and fill in other fields
return result;
}
std::tm HolidayDateCalculator::CalculateNthWeekday(int year, int month, Weekday weekday, int n)
{
// Start with first day of the month
std::tm date = {};
date.tm_year = year - 1900;
date.tm_mon = month - 1;
date.tm_mday = 1;
mktime(&date);
// Find first occurrence of the target weekday
int const daysUntilWeekday = (static_cast<int>(weekday) - date.tm_wday + 7) % 7;
date.tm_mday = 1 + daysUntilWeekday;
// Move to nth occurrence
date.tm_mday += (n - 1) * 7;
mktime(&date); // Normalize (handles month overflow)
return date;
}
std::tm HolidayDateCalculator::CalculateWeekdayOnOrAfter(int year, int month, int day, Weekday weekday)
{
// Start with the specified date
std::tm date = {};
date.tm_year = year - 1900;
date.tm_mon = month - 1;
date.tm_mday = day;
mktime(&date);
// Find days until the target weekday (0 if already on that day)
int const daysUntilWeekday = (static_cast<int>(weekday) - date.tm_wday + 7) % 7;
date.tm_mday += daysUntilWeekday;
mktime(&date); // Normalize
return date;
}
// ============================================================================
// LUNAR NEW YEAR CALCULATION
// Based on Jean Meeus "Astronomical Algorithms" (1991), Chapter 49
// Reference: https://celestialprogramming.com/moonphases.html
// Chinese New Year = new moon falling between January 21 and February 20
// ============================================================================
double HolidayDateCalculator::DateToJulianDay(int year, int month, double day)
{
if (month <= 2)
{
year -= 1;
month += 12;
}
int const A = year / 100;
int const B = 2 - A + (A / 4);
return std::floor(365.25 * (year + 4716)) + std::floor(30.6001 * (month + 1)) + day + B - 1524.5;
}
void HolidayDateCalculator::JulianDayToDate(double jd, int& year, int& month, int& day)
{
jd += 0.5;
int const Z = static_cast<int>(jd);
int A = Z;
if (Z >= 2299161)
{
int const alpha = static_cast<int>((Z - 1867216.25) / 36524.25);
A = Z + 1 + alpha - (alpha / 4);
}
int const B = A + 1524;
int const C = static_cast<int>((B - 122.1) / 365.25);
int const D = static_cast<int>(365.25 * C);
int const E = static_cast<int>((B - D) / 30.6001);
day = B - D - static_cast<int>(30.6001 * E);
month = (E < 14) ? E - 1 : E - 13;
year = (month > 2) ? C - 4716 : C - 4715;
}
double HolidayDateCalculator::CalculateNewMoon(double k)
{
// Meeus "Astronomical Algorithms" Chapter 49
double const T = k / 1236.85;
double const T2 = T * T;
double const T3 = T2 * T;
double const T4 = T3 * T;
// Mean phase (Eq 49.1)
double const JDE = 2451550.09766 + 29.530588861 * k + 0.00015437 * T2
- 0.000000150 * T3 + 0.00000000073 * T4;
// Eccentricity correction
double const E = 1.0 - 0.002516 * T - 0.0000074 * T2;
double const E2 = E * E;
// Sun's mean anomaly (Eq 49.4)
double const M = 2.5534 + 29.10535670 * k - 0.0000014 * T2 - 0.00000011 * T3;
// Moon's mean anomaly (Eq 49.5)
double const MPrime = 201.5643 + 385.81693528 * k + 0.0107582 * T2
+ 0.00001238 * T3 - 0.000000058 * T4;
// Moon's argument of latitude (Eq 49.6)
double const F = 160.7108 + 390.67050284 * k - 0.0016118 * T2
- 0.00000227 * T3 + 0.000000011 * T4;
// Longitude of ascending node (Eq 49.7)
double const Omega = 124.7746 - 1.56375588 * k + 0.0020672 * T2 + 0.00000215 * T3;
// New Moon corrections (Table 49.A)
double correction =
- 0.40720 * sind(MPrime)
+ 0.17241 * E * sind(M)
+ 0.01608 * sind(2 * MPrime)
+ 0.01039 * sind(2 * F)
+ 0.00739 * E * sind(MPrime - M)
- 0.00514 * E * sind(MPrime + M)
+ 0.00208 * E2 * sind(2 * M)
- 0.00111 * sind(MPrime - 2 * F)
- 0.00057 * sind(MPrime + 2 * F)
+ 0.00056 * E * sind(2 * MPrime + M)
- 0.00042 * sind(3 * MPrime)
+ 0.00042 * E * sind(M + 2 * F)
+ 0.00038 * E * sind(M - 2 * F)
- 0.00024 * E * sind(2 * MPrime - M)
- 0.00017 * sind(Omega);
// Additional planetary corrections (Table 49.B)
double const A1 = 299.77 + 0.107408 * k - 0.009173 * T2;
double const A2 = 251.88 + 0.016321 * k;
double const A3 = 251.83 + 26.651886 * k;
double const A4 = 349.42 + 36.412478 * k;
double const A5 = 84.66 + 18.206239 * k;
double const A6 = 141.74 + 53.303771 * k;
double const A7 = 207.14 + 2.453732 * k;
double const A8 = 154.84 + 7.306860 * k;
double const A9 = 34.52 + 27.261239 * k;
double const A10 = 207.19 + 0.121824 * k;
double const A11 = 291.34 + 1.844379 * k;
double const A12 = 161.72 + 24.198154 * k;
double const A13 = 239.56 + 25.513099 * k;
double const A14 = 331.55 + 3.592518 * k;
correction += 0.000325 * sind(A1) + 0.000165 * sind(A2) + 0.000164 * sind(A3)
+ 0.000126 * sind(A4) + 0.000110 * sind(A5) + 0.000062 * sind(A6)
+ 0.000060 * sind(A7) + 0.000056 * sind(A8) + 0.000047 * sind(A9)
+ 0.000042 * sind(A10) + 0.000040 * sind(A11) + 0.000037 * sind(A12)
+ 0.000035 * sind(A13) + 0.000023 * sind(A14);
return JDE + correction;
}
std::tm HolidayDateCalculator::CalculateLunarNewYear(int year)
{
// Chinese New Year always falls on the new moon between Jan 21 and Feb 20
double const jan21JD = DateToJulianDay(year, 1, 21.0);
double const feb21JD = DateToJulianDay(year, 2, 21.0);
// Approximate lunation number k for January of target year
double const approxK = (year - 2000.0) * 12.3685;
double const k = std::floor(approxK);
// Search for the new moon in the valid range
for (int i = -2; i <= 2; ++i)
{
double const nmJDE = CalculateNewMoon(k + i);
// Convert TT (Terrestrial Time) to UT (approximate DeltaT ~70s for 2020s)
double nmJD = nmJDE - 70.0 / 86400.0;
// Add 8 hours for China Standard Time (UTC+8)
nmJD += 8.0 / 24.0;
if (nmJD >= jan21JD && nmJD < feb21JD)
{
int cnyYear, cnyMonth, cnyDay;
JulianDayToDate(nmJD, cnyYear, cnyMonth, cnyDay);
std::tm result = {};
result.tm_year = cnyYear - 1900;
result.tm_mon = cnyMonth - 1;
result.tm_mday = cnyDay;
mktime(&result);
return result;
}
}
// Fallback (should never happen for years 2000-2031)
std::tm fallback = {};
fallback.tm_year = year - 1900;
fallback.tm_mon = 0; // January
fallback.tm_mday = 25;
mktime(&fallback);
return fallback;
}
// ============================================================================
// AUTUMN EQUINOX CALCULATION
// Based on Jean Meeus "Astronomical Algorithms" (1991), Chapter 27
// Reference: https://en.wikipedia.org/wiki/Equinox#Calculation
// ============================================================================
std::tm HolidayDateCalculator::CalculateAutumnEquinox(int year)
{
// Meeus algorithm for mean September equinox (Table 27.C)
// Valid for years 2000-3000
double const Y = (year - 2000.0) / 1000.0;
double const Y2 = Y * Y;
double const Y3 = Y2 * Y;
double const Y4 = Y3 * Y;
// Mean equinox JDE0 (Eq 27.1 for September equinox after 2000)
double const JDE0 = 2451810.21715 + 365242.01767 * Y - 0.11575 * Y2
+ 0.00337 * Y3 + 0.00078 * Y4;
// Periodic terms for correction (Table 27.B)
double const T = (JDE0 - 2451545.0) / 36525.0;
double const W = 35999.373 * T - 2.47;
double const deltaLambda = 1.0 + 0.0334 * std::cos(W * DEG_TO_RAD)
+ 0.0007 * std::cos(2.0 * W * DEG_TO_RAD);
// Simplified correction (sum of periodic terms from Table 27.C)
// Using first few significant terms
double const S = 485 * std::cos((324.96 + 1934.136 * T) * DEG_TO_RAD)
+ 203 * std::cos((337.23 + 32964.467 * T) * DEG_TO_RAD)
+ 199 * std::cos((342.08 + 20.186 * T) * DEG_TO_RAD)
+ 182 * std::cos((27.85 + 445267.112 * T) * DEG_TO_RAD)
+ 156 * std::cos((73.14 + 45036.886 * T) * DEG_TO_RAD)
+ 136 * std::cos((171.52 + 22518.443 * T) * DEG_TO_RAD)
+ 77 * std::cos((222.54 + 65928.934 * T) * DEG_TO_RAD)
+ 74 * std::cos((296.72 + 3034.906 * T) * DEG_TO_RAD)
+ 70 * std::cos((243.58 + 9037.513 * T) * DEG_TO_RAD)
+ 58 * std::cos((119.81 + 33718.147 * T) * DEG_TO_RAD)
+ 52 * std::cos((297.17 + 150.678 * T) * DEG_TO_RAD)
+ 50 * std::cos((21.02 + 2281.226 * T) * DEG_TO_RAD);
double const JDE = JDE0 + (0.00001 * S) / deltaLambda;
// Convert JDE to calendar date
int eqYear;
int eqMonth;
int eqDay;
JulianDayToDate(JDE, eqYear, eqMonth, eqDay);
std::tm result = {};
result.tm_year = eqYear - 1900;
result.tm_mon = eqMonth - 1;
result.tm_mday = eqDay;
mktime(&result);
return result;
}
// ============================================================================
// WINTER SOLSTICE CALCULATION
// Based on Jean Meeus "Astronomical Algorithms" (1991), Chapter 27
// ============================================================================
std::tm HolidayDateCalculator::CalculateWinterSolstice(int year)
{
// Meeus algorithm for mean December solstice (Table 27.C)
// Valid for years 2000-3000
double const Y = (year - 2000.0) / 1000.0;
double const Y2 = Y * Y;
double const Y3 = Y2 * Y;
double const Y4 = Y3 * Y;
// Mean solstice JDE0 (Eq 27.1 for December solstice after 2000)
double const JDE0 = 2451900.05952 + 365242.74049 * Y - 0.06223 * Y2
- 0.00823 * Y3 + 0.00032 * Y4;
// Periodic terms for correction (Table 27.B)
double const T = (JDE0 - 2451545.0) / 36525.0;
double const W = 35999.373 * T - 2.47;
double const deltaLambda = 1.0 + 0.0334 * std::cos(W * DEG_TO_RAD)
+ 0.0007 * std::cos(2.0 * W * DEG_TO_RAD);
// Simplified correction (sum of periodic terms from Table 27.C)
double const S = 485 * std::cos((324.96 + 1934.136 * T) * DEG_TO_RAD)
+ 203 * std::cos((337.23 + 32964.467 * T) * DEG_TO_RAD)
+ 199 * std::cos((342.08 + 20.186 * T) * DEG_TO_RAD)
+ 182 * std::cos((27.85 + 445267.112 * T) * DEG_TO_RAD)
+ 156 * std::cos((73.14 + 45036.886 * T) * DEG_TO_RAD)
+ 136 * std::cos((171.52 + 22518.443 * T) * DEG_TO_RAD)
+ 77 * std::cos((222.54 + 65928.934 * T) * DEG_TO_RAD)
+ 74 * std::cos((296.72 + 3034.906 * T) * DEG_TO_RAD)
+ 70 * std::cos((243.58 + 9037.513 * T) * DEG_TO_RAD)
+ 58 * std::cos((119.81 + 33718.147 * T) * DEG_TO_RAD)
+ 52 * std::cos((297.17 + 150.678 * T) * DEG_TO_RAD)
+ 50 * std::cos((21.02 + 2281.226 * T) * DEG_TO_RAD);
double const JDE = JDE0 + (0.00001 * S) / deltaLambda;
// Convert JDE to calendar date
int solYear;
int solMonth;
int solDay;
JulianDayToDate(JDE, solYear, solMonth, solDay);
std::tm result = {};
result.tm_year = solYear - 1900;
result.tm_mon = solMonth - 1;
result.tm_mday = solDay;
mktime(&result);
return result;
}
std::tm HolidayDateCalculator::CalculateHolidayDate(const HolidayRule& rule, int year)
{
std::tm result = {};
switch (rule.type)
{
case HolidayCalculationType::FIXED_DATE:
{
result.tm_year = year - 1900;
result.tm_mon = rule.month - 1;
result.tm_mday = rule.day;
mktime(&result);
break;
}
case HolidayCalculationType::NTH_WEEKDAY:
{
result = CalculateNthWeekday(year, rule.month, static_cast<Weekday>(rule.weekday), rule.day);
if (rule.offset != 0)
{
result.tm_mday += rule.offset;
mktime(&result); // Normalize
}
break;
}
case HolidayCalculationType::EASTER_OFFSET:
{
result = CalculateEasterSunday(year);
result.tm_mday += rule.offset;
mktime(&result); // Normalize
break;
}
case HolidayCalculationType::LUNAR_NEW_YEAR:
{
result = CalculateLunarNewYear(year);
if (rule.offset != 0)
{
result.tm_mday += rule.offset;
mktime(&result); // Normalize
}
break;
}
case HolidayCalculationType::WEEKDAY_ON_OR_AFTER:
{
result = CalculateWeekdayOnOrAfter(year, rule.month, rule.day, static_cast<Weekday>(rule.weekday));
if (rule.offset != 0)
{
result.tm_mday += rule.offset;
mktime(&result); // Normalize
}
break;
}
case HolidayCalculationType::AUTUMN_EQUINOX:
{
result = CalculateAutumnEquinox(year);
if (rule.offset != 0)
{
result.tm_mday += rule.offset;
mktime(&result); // Normalize
}
break;
}
case HolidayCalculationType::WINTER_SOLSTICE:
{
result = CalculateWinterSolstice(year);
if (rule.offset != 0)
{
result.tm_mday += rule.offset;
mktime(&result); // Normalize
}
break;
}
case HolidayCalculationType::DARKMOON_FAIRE:
{
// Return first occurrence for the year
// rule.month contains the location offset (0, 1, or 2)
int const locationOffset = rule.month;
// Find first month in the year where month % 3 == locationOffset
for (int month = 1; month <= 12; ++month)
{
if (month % 3 == locationOffset)
{
result = CalculateNthWeekday(year, month, Weekday::SUNDAY, 1);
break;
}
}
break;
}
}
return result;
}
uint32_t HolidayDateCalculator::PackDate(const std::tm& date)
{
// WoW packed date format (same as ByteBuffer::AppendPackedTime):
// bits 24-28: year offset from 2000 (5 bits = 0-31, valid years 2000-2031)
// bits 20-23: month (0-indexed)
// bits 14-19: day (0-indexed)
// bits 11-13: weekday (0=Sunday, 6=Saturday - POSIX tm_wday)
// bits 6-10: hour
// bits 0-5: minute
int const year = date.tm_year + 1900;
// Client uses 5-bit year offset from 2000, so years before 2000 clamp to 0.
// If client is patched to support earlier years, update this logic.
uint32_t const yearOffset = (year < 2000) ? 0 : static_cast<uint32_t>(year - 2000);
uint32_t const month = static_cast<uint32_t>(date.tm_mon); // Already 0-indexed
uint32_t const day = static_cast<uint32_t>(date.tm_mday - 1); // Convert to 0-indexed
uint32_t const weekday = static_cast<uint32_t>(date.tm_wday); // 0=Sunday, 6=Saturday
return (yearOffset << 24) | (month << 20) | (day << 14) | (weekday << 11);
}
std::tm HolidayDateCalculator::UnpackDate(uint32_t packed)
{
std::tm result = {};
result.tm_year = static_cast<int>(((packed >> 24) & 0x1F) + 2000 - 1900);
result.tm_mon = static_cast<int>((packed >> 20) & 0xF);
result.tm_mday = static_cast<int>(((packed >> 14) & 0x3F) + 1);
result.tm_wday = static_cast<int>((packed >> 11) & 0x7);
result.tm_hour = static_cast<int>((packed >> 6) & 0x1F);
result.tm_min = static_cast<int>(packed & 0x3F);
mktime(&result); // Normalize and fill in tm_yday, tm_isdst
return result;
}
uint32_t HolidayDateCalculator::GetPackedHolidayDate(uint32_t holidayId, int year)
{
for (auto const& rule : HolidayRules)
{
if (rule.holidayId == holidayId)
{
std::tm const date = CalculateHolidayDate(rule, year);
return PackDate(date);
}
}
return 0; // Holiday not found
}
std::vector<uint32_t> HolidayDateCalculator::GetDarkmoonFaireDates(int locationOffset, int startYear, int numYears, int dayOffset)
{
std::vector<uint32_t> dates;
// Darkmoon Faire is first Sunday of months where (month % 3) == locationOffset
// locationOffset 0: Mar, Jun, Sep, Dec - Elwynn (Alliance)
// locationOffset 1: Jan, Apr, Jul, Oct - Mulgore (Horde)
// locationOffset 2: Feb, May, Aug, Nov - Terokkar (Outland)
for (int year = startYear; year < startYear + numYears && year <= 2030; ++year)
{
for (int month = 1; month <= 12; ++month)
{
if (month % 3 == locationOffset)
{
// Calculate first Sunday of this month, then apply day offset
std::tm date = CalculateNthWeekday(year, month, Weekday::SUNDAY, 1);
if (dayOffset != 0)
{
date.tm_mday += dayOffset;
mktime(&date); // Normalize
}
dates.push_back(PackDate(date));
}
}
}
return dates;
}

View File

@@ -0,0 +1,112 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ACORE_HOLIDAY_DATE_CALCULATOR_H
#define ACORE_HOLIDAY_DATE_CALCULATOR_H
#include <cstdint>
#include <ctime>
#include <vector>
enum class HolidayCalculationType
{
FIXED_DATE, // Same month/day every year (e.g., Dec 25)
NTH_WEEKDAY, // Nth weekday of month (e.g., 4th Thursday of Nov)
EASTER_OFFSET, // Days relative to Easter Sunday
LUNAR_NEW_YEAR, // Chinese New Year (new moon between Jan 21 - Feb 20)
WEEKDAY_ON_OR_AFTER, // First weekday on or after a date (e.g., first Monday on or after Feb 3)
AUTUMN_EQUINOX, // Days relative to autumn equinox (offset in days)
WINTER_SOLSTICE, // Days relative to winter solstice (offset in days)
DARKMOON_FAIRE // First Sunday of months matching (month % 3 == locationOffset)
};
enum class Weekday
{
SUNDAY = 0,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
struct HolidayRule
{
uint32_t holidayId;
HolidayCalculationType type;
int month; // 1-12
int day; // For FIXED_DATE: day of month. For NTH_WEEKDAY: which occurrence (1-5)
int weekday; // For NTH_WEEKDAY: 0=Sunday through 6=Saturday
int offset; // For EASTER_OFFSET: days after Easter (can be negative)
};
class HolidayDateCalculator
{
public:
// Calculate Easter Sunday for a given year (Computus algorithm)
static std::tm CalculateEasterSunday(int year);
// Calculate Nth weekday of a month (e.g., 4th Thursday of November)
static std::tm CalculateNthWeekday(int year, int month, Weekday weekday, int n);
// Calculate first weekday on or after a specific date (e.g., first Monday on or after Feb 3)
static std::tm CalculateWeekdayOnOrAfter(int year, int month, int day, Weekday weekday);
// Calculate Chinese New Year (Lunar New Year) using astronomical algorithm
// Based on Jean Meeus "Astronomical Algorithms" - finds new moon between Jan 21 - Feb 20
static std::tm CalculateLunarNewYear(int year);
// Calculate Autumn Equinox using astronomical algorithm
// Based on Jean Meeus "Astronomical Algorithms" Chapter 27
static std::tm CalculateAutumnEquinox(int year);
// Calculate Winter Solstice using astronomical algorithm
// Based on Jean Meeus "Astronomical Algorithms" Chapter 27
static std::tm CalculateWinterSolstice(int year);
// Calculate holiday start date for a given year
static std::tm CalculateHolidayDate(const HolidayRule& rule, int year);
// Convert std::tm to WoW's packed date format
static uint32_t PackDate(const std::tm& date);
// Convert WoW's packed date format to std::tm
static std::tm UnpackDate(uint32_t packed);
// Get all holiday rules
static const std::vector<HolidayRule>& GetHolidayRules();
// Calculate date for a specific holiday ID and year
static uint32_t GetPackedHolidayDate(uint32_t holidayId, int year);
// Calculate Darkmoon Faire dates for a location over a year range
// locationOffset: 0=Elwynn (months 3,6,9,12), 1=Mulgore (months 1,4,7,10), 2=Terokkar (months 2,5,8,11)
// dayOffset: days to offset from first Sunday (e.g., -2 for Friday start)
// Returns packed dates for all occurrences in the year range
static std::vector<uint32_t> GetDarkmoonFaireDates(int locationOffset, int startYear, int numYears, int dayOffset = 0);
private:
// Julian Date conversions for lunar calculations
static double DateToJulianDay(int year, int month, double day);
static void JulianDayToDate(double jd, int& year, int& month, int& day);
// Calculate new moon Julian Date for lunation k (Meeus algorithm)
static double CalculateNewMoon(double k);
};
#endif // ACORE_HOLIDAY_DATE_CALCULATOR_H

File diff suppressed because it is too large Load Diff