Skip to content

Commit 61eee6f

Browse files
committed
feat(Core/Events): Add dynamic holiday date calculator
Replaces static SQL-based holiday dates with dynamic C++ calculation. Holidays are now computed at runtime using: - Computus algorithm for Easter-based holidays (Noblegarden) - Nth weekday calculation for floating holidays (Pilgrim's Bounty) - Fixed dates for standard holidays Includes unit tests covering years 1900-2200. DB overrides still supported for custom servers.
1 parent f190e29 commit 61eee6f

File tree

4 files changed

+783
-28
lines changed

4 files changed

+783
-28
lines changed

src/server/game/Events/GameEventMgr.cpp

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "GameEventMgr.h"
1919
#include "BattlegroundMgr.h"
20+
#include "HolidayDateCalculator.h"
2021
#include "Chat.h"
2122
#include "DisableMgr.h"
2223
#include "GameObjectAI.h"
@@ -1071,50 +1072,81 @@ void GameEventMgr::LoadFromDB()
10711072
void GameEventMgr::LoadHolidayDates()
10721073
{
10731074
uint32 oldMSTime = getMSTime();
1075+
uint32 dynamicCount = 0;
1076+
uint32 dbCount = 0;
10741077

1075-
WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_GAME_EVENT_HOLIDAY_DATES);
1076-
PreparedQueryResult result = WorldDatabase.Query(stmt);
1078+
// Step 1: Generate dynamic holiday dates based on current year
1079+
time_t now = time(nullptr);
1080+
struct tm* timeinfo = localtime(&now);
1081+
int currentYear = timeinfo->tm_year + 1900;
10771082

1078-
if (!result)
1083+
for (auto const& rule : HolidayDateCalculator::GetHolidayRules())
10791084
{
1080-
LOG_WARN("server.loading", ">> Loaded 0 holiday dates. DB table `holiday_dates` is empty.");
1081-
return;
1082-
}
1083-
1084-
uint32 count = 0;
1085-
do
1086-
{
1087-
Field* fields = result->Fetch();
1088-
1089-
uint32 holidayId = fields[0].Get<uint32>();
1090-
HolidaysEntry* entry = const_cast<HolidaysEntry*>(sHolidaysStore.LookupEntry(holidayId));
1085+
HolidaysEntry* entry = const_cast<HolidaysEntry*>(sHolidaysStore.LookupEntry(rule.holidayId));
10911086
if (!entry)
1092-
{
1093-
LOG_ERROR("sql.sql", "holiday_dates entry has invalid holiday id {}.", holidayId);
10941087
continue;
1095-
}
10961088

1097-
uint8 dateId = fields[1].Get<uint8>();
1098-
if (dateId >= MAX_HOLIDAY_DATES)
1089+
// Generate dates for current year and next 10 years
1090+
for (int yearOffset = 0; yearOffset <= 10; ++yearOffset)
10991091
{
1100-
LOG_ERROR("sql.sql", "holiday_dates entry has out of range date_id {}.", dateId);
1101-
continue;
1102-
}
1103-
entry->Date[dateId] = fields[2].Get<uint32>();
1092+
int year = currentYear + yearOffset;
1093+
uint8 dateId = static_cast<uint8>(yearOffset);
11041094

1105-
if (uint32 duration = fields[3].Get<uint32>())
1106-
entry->Duration[0] = duration;
1095+
if (dateId >= MAX_HOLIDAY_DATES)
1096+
break;
1097+
1098+
entry->Date[dateId] = HolidayDateCalculator::GetPackedHolidayDate(rule.holidayId, year);
1099+
++dynamicCount;
1100+
}
11071101

11081102
auto itr = std::lower_bound(ModifiedHolidays.begin(), ModifiedHolidays.end(), entry->Id);
11091103
if (itr == ModifiedHolidays.end() || *itr != entry->Id)
11101104
{
11111105
ModifiedHolidays.insert(itr, entry->Id);
11121106
}
1107+
}
11131108

1114-
++count;
1115-
} while (result->NextRow());
1109+
// Step 2: Load DB overrides (allows custom servers to override calculated dates)
1110+
WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_GAME_EVENT_HOLIDAY_DATES);
1111+
PreparedQueryResult result = WorldDatabase.Query(stmt);
1112+
1113+
if (result)
1114+
{
1115+
do
1116+
{
1117+
Field* fields = result->Fetch();
1118+
1119+
uint32 holidayId = fields[0].Get<uint32>();
1120+
HolidaysEntry* entry = const_cast<HolidaysEntry*>(sHolidaysStore.LookupEntry(holidayId));
1121+
if (!entry)
1122+
{
1123+
LOG_ERROR("sql.sql", "holiday_dates entry has invalid holiday id {}.", holidayId);
1124+
continue;
1125+
}
1126+
1127+
uint8 dateId = fields[1].Get<uint8>();
1128+
if (dateId >= MAX_HOLIDAY_DATES)
1129+
{
1130+
LOG_ERROR("sql.sql", "holiday_dates entry has out of range date_id {}.", dateId);
1131+
continue;
1132+
}
1133+
entry->Date[dateId] = fields[2].Get<uint32>();
1134+
1135+
if (uint32 duration = fields[3].Get<uint32>())
1136+
entry->Duration[0] = duration;
1137+
1138+
auto itr = std::lower_bound(ModifiedHolidays.begin(), ModifiedHolidays.end(), entry->Id);
1139+
if (itr == ModifiedHolidays.end() || *itr != entry->Id)
1140+
{
1141+
ModifiedHolidays.insert(itr, entry->Id);
1142+
}
1143+
1144+
++dbCount;
1145+
} while (result->NextRow());
1146+
}
11161147

1117-
LOG_INFO("server.loading", ">> Loaded {} Holiday Dates in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
1148+
LOG_INFO("server.loading", ">> Loaded {} Holiday Dates ({} dynamic, {} DB overrides) in {} ms",
1149+
dynamicCount + dbCount, dynamicCount, dbCount, GetMSTimeDiffToNow(oldMSTime));
11181150
}
11191151

11201152
uint32 GameEventMgr::GetNPCFlag(Creature* cr)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12+
* more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along
15+
* with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include "HolidayDateCalculator.h"
19+
#include "SharedDefines.h"
20+
21+
// Static holiday rules configuration
22+
static const std::vector<HolidayRule> s_HolidayRules = {
23+
// Lunar Festival: Fixed Jan 22 (simplification; Blizzard varied slightly)
24+
{ HOLIDAY_LUNAR_FESTIVAL, HolidayCalculationType::FIXED_DATE, 1, 22, 0, 0 },
25+
26+
// Love is in the Air: Fixed Feb 6
27+
{ HOLIDAY_LOVE_IS_IN_THE_AIR, HolidayCalculationType::FIXED_DATE, 2, 6, 0, 0 },
28+
29+
// Noblegarden: Week after Easter (Easter + 7 days)
30+
{ HOLIDAY_NOBLEGARDEN, HolidayCalculationType::EASTER_OFFSET, 0, 0, 0, 7 },
31+
32+
// Children's Week: Fixed Apr 30
33+
{ HOLIDAY_CHILDRENS_WEEK, HolidayCalculationType::FIXED_DATE, 4, 30, 0, 0 },
34+
35+
// Harvest Festival: Fixed Sep 28
36+
{ HOLIDAY_HARVEST_FESTIVAL, HolidayCalculationType::FIXED_DATE, 9, 28, 0, 0 },
37+
38+
// Pilgrim's Bounty: 4th Thursday of November (Thanksgiving)
39+
{ HOLIDAY_PILGRIMS_BOUNTY, HolidayCalculationType::NTH_WEEKDAY, 11, 4, WEEKDAY_THURSDAY, 0 }
40+
};
41+
42+
const std::vector<HolidayRule>& HolidayDateCalculator::GetHolidayRules()
43+
{
44+
return s_HolidayRules;
45+
}
46+
47+
std::tm HolidayDateCalculator::CalculateEasterSunday(int year)
48+
{
49+
// Anonymous Gregorian algorithm (Computus)
50+
// This algorithm calculates the date of Easter Sunday for any year
51+
int a = year % 19;
52+
int b = year / 100;
53+
int c = year % 100;
54+
int d = b / 4;
55+
int e = b % 4;
56+
int f = (b + 8) / 25;
57+
int g = (b - f + 1) / 3;
58+
int h = (19 * a + b - d - g + 15) % 30;
59+
int i = c / 4;
60+
int k = c % 4;
61+
int l = (32 + 2 * e + 2 * i - h - k) % 7;
62+
int m = (a + 11 * h + 22 * l) / 451;
63+
int month = (h + l - 7 * m + 114) / 31;
64+
int day = ((h + l - 7 * m + 114) % 31) + 1;
65+
66+
std::tm result = {};
67+
result.tm_year = year - 1900;
68+
result.tm_mon = month - 1;
69+
result.tm_mday = day;
70+
mktime(&result); // Normalize and fill in other fields
71+
72+
return result;
73+
}
74+
75+
std::tm HolidayDateCalculator::CalculateNthWeekday(int year, int month, Weekday weekday, int n)
76+
{
77+
// Start with first day of the month
78+
std::tm date = {};
79+
date.tm_year = year - 1900;
80+
date.tm_mon = month - 1;
81+
date.tm_mday = 1;
82+
mktime(&date);
83+
84+
// Find first occurrence of the target weekday
85+
int daysUntilWeekday = (weekday - date.tm_wday + 7) % 7;
86+
date.tm_mday = 1 + daysUntilWeekday;
87+
88+
// Move to nth occurrence
89+
date.tm_mday += (n - 1) * 7;
90+
91+
mktime(&date); // Normalize (handles month overflow)
92+
return date;
93+
}
94+
95+
std::tm HolidayDateCalculator::CalculateHolidayDate(const HolidayRule& rule, int year)
96+
{
97+
std::tm result = {};
98+
99+
switch (rule.type)
100+
{
101+
case HolidayCalculationType::FIXED_DATE:
102+
{
103+
result.tm_year = year - 1900;
104+
result.tm_mon = rule.month - 1;
105+
result.tm_mday = rule.day;
106+
mktime(&result);
107+
break;
108+
}
109+
case HolidayCalculationType::NTH_WEEKDAY:
110+
{
111+
result = CalculateNthWeekday(year, rule.month, static_cast<Weekday>(rule.weekday), rule.day);
112+
break;
113+
}
114+
case HolidayCalculationType::EASTER_OFFSET:
115+
{
116+
result = CalculateEasterSunday(year);
117+
result.tm_mday += rule.offset;
118+
mktime(&result); // Normalize
119+
break;
120+
}
121+
}
122+
123+
return result;
124+
}
125+
126+
uint32_t HolidayDateCalculator::PackDate(const std::tm& date)
127+
{
128+
// WoW packed date format:
129+
// bits 14-19: day (0-indexed)
130+
// bits 20-23: month (0-indexed)
131+
// bits 24-28: year offset from 2000
132+
uint32_t yearOffset = static_cast<uint32_t>((date.tm_year + 1900) - 2000);
133+
uint32_t month = static_cast<uint32_t>(date.tm_mon); // Already 0-indexed
134+
uint32_t day = static_cast<uint32_t>(date.tm_mday - 1); // Convert to 0-indexed
135+
136+
return (yearOffset << 24) | (month << 20) | (day << 14);
137+
}
138+
139+
std::tm HolidayDateCalculator::UnpackDate(uint32_t packed)
140+
{
141+
std::tm result = {};
142+
result.tm_year = static_cast<int>(((packed >> 24) & 0x1F) + 2000 - 1900);
143+
result.tm_mon = static_cast<int>((packed >> 20) & 0xF);
144+
result.tm_mday = static_cast<int>(((packed >> 14) & 0x3F) + 1);
145+
mktime(&result);
146+
return result;
147+
}
148+
149+
uint32_t HolidayDateCalculator::GetPackedHolidayDate(uint32_t holidayId, int year)
150+
{
151+
for (auto const& rule : s_HolidayRules)
152+
{
153+
if (rule.holidayId == holidayId)
154+
{
155+
std::tm date = CalculateHolidayDate(rule, year);
156+
return PackDate(date);
157+
}
158+
}
159+
return 0; // Holiday not found
160+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12+
* more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along
15+
* with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#ifndef AZEROTHCORE_HOLIDAY_DATE_CALCULATOR_H
19+
#define AZEROTHCORE_HOLIDAY_DATE_CALCULATOR_H
20+
21+
#include <cstdint>
22+
#include <ctime>
23+
#include <vector>
24+
25+
enum class HolidayCalculationType
26+
{
27+
FIXED_DATE, // Same month/day every year (e.g., Dec 25)
28+
NTH_WEEKDAY, // Nth weekday of month (e.g., 4th Thursday of Nov)
29+
EASTER_OFFSET // Days relative to Easter Sunday
30+
};
31+
32+
enum Weekday
33+
{
34+
WEEKDAY_SUNDAY = 0,
35+
WEEKDAY_MONDAY,
36+
WEEKDAY_TUESDAY,
37+
WEEKDAY_WEDNESDAY,
38+
WEEKDAY_THURSDAY,
39+
WEEKDAY_FRIDAY,
40+
WEEKDAY_SATURDAY
41+
};
42+
43+
struct HolidayRule
44+
{
45+
uint32_t holidayId;
46+
HolidayCalculationType type;
47+
int month; // 1-12
48+
int day; // For FIXED_DATE: day of month. For NTH_WEEKDAY: which occurrence (1-5)
49+
int weekday; // For NTH_WEEKDAY: 0=Sunday through 6=Saturday
50+
int offset; // For EASTER_OFFSET: days after Easter (can be negative)
51+
};
52+
53+
class HolidayDateCalculator
54+
{
55+
public:
56+
// Calculate Easter Sunday for a given year (Computus algorithm)
57+
static std::tm CalculateEasterSunday(int year);
58+
59+
// Calculate Nth weekday of a month (e.g., 4th Thursday of November)
60+
static std::tm CalculateNthWeekday(int year, int month, Weekday weekday, int n);
61+
62+
// Calculate holiday start date for a given year
63+
static std::tm CalculateHolidayDate(const HolidayRule& rule, int year);
64+
65+
// Convert std::tm to WoW's packed date format
66+
static uint32_t PackDate(const std::tm& date);
67+
68+
// Convert WoW's packed date format to std::tm
69+
static std::tm UnpackDate(uint32_t packed);
70+
71+
// Get all holiday rules
72+
static const std::vector<HolidayRule>& GetHolidayRules();
73+
74+
// Calculate date for a specific holiday ID and year
75+
static uint32_t GetPackedHolidayDate(uint32_t holidayId, int year);
76+
};
77+
78+
#endif // AZEROTHCORE_HOLIDAY_DATE_CALCULATOR_H

0 commit comments

Comments
 (0)