/*
* 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 .
*/
#include "HolidayDateCalculator.h"
#include "SharedDefines.h"
#include
// 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 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(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(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: Fixed Sept 20 main event, prep starts Sept 13
{ HOLIDAY_BREWFEST, HolidayCalculationType::FIXED_DATE, 9, 13, 0, 0 },
// 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(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& 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(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(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(jd);
int A = Z;
if (Z >= 2299161)
{
int const alpha = static_cast((Z - 1867216.25) / 36524.25);
A = Z + 1 + alpha - (alpha / 4);
}
int const B = A + 1524;
int const C = static_cast((B - 122.1) / 365.25);
int const D = static_cast(365.25 * C);
int const E = static_cast((B - D) / 30.6001);
day = B - D - static_cast(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(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(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(year - 2000);
uint32_t const month = static_cast(date.tm_mon); // Already 0-indexed
uint32_t const day = static_cast(date.tm_mday - 1); // Convert to 0-indexed
uint32_t const weekday = static_cast(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(((packed >> 24) & 0x1F) + 2000 - 1900);
result.tm_mon = static_cast((packed >> 20) & 0xF);
result.tm_mday = static_cast(((packed >> 14) & 0x3F) + 1);
result.tm_wday = static_cast((packed >> 11) & 0x7);
result.tm_hour = static_cast((packed >> 6) & 0x1F);
result.tm_min = static_cast(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 HolidayDateCalculator::GetDarkmoonFaireDates(int locationOffset, int startYear, int numYears, int dayOffset)
{
std::vector 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;
}