diff --git a/src/server/game/Events/GameEventMgr.cpp b/src/server/game/Events/GameEventMgr.cpp index b4e6919af..bde9ba819 100644 --- a/src/server/game/Events/GameEventMgr.cpp +++ b/src/server/game/Events/GameEventMgr.cpp @@ -1961,21 +1961,23 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) bool singleDate = ((holiday->Date[0] >> 24) & 0x1F) == 31; // Events with fixed date within year have - 1 time_t curTime = GameTime::GetGameTime().count(); - for (uint8 i = 0; i < MAX_HOLIDAY_DATES && holiday->Date[i]; ++i) + if (!singleDate) + { + time_t start = HolidayDateCalculator::FindStartTimeForStage( + holiday->Date, MAX_HOLIDAY_DATES, stageOffset, event.Length, curTime); + if (start) + event.Start = start; + + return; + } + + for (uint8 i = 0; i < MAX_HOLIDAY_DATES && holiday->Date[i]; ++i) { uint32 date = holiday->Date[i]; - tm timeInfo; - if (singleDate) - { - timeInfo = Acore::Time::TimeBreakdown(curTime); - timeInfo.tm_year -= 1; // First try last year (event active through New Year) - } - else - { - timeInfo.tm_year = ((date >> 24) & 0x1F) + 100; - } + tm timeInfo = Acore::Time::TimeBreakdown(curTime); + timeInfo.tm_year -= 1; // First try last year (event active through New Year) timeInfo.tm_mon = (date >> 20) & 0xF; timeInfo.tm_mday = ((date >> 14) & 0x3F) + 1; @@ -1986,12 +1988,12 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) // try to get next start time (skip past dates) time_t startTime = mktime(&timeInfo); - if (curTime < startTime + event.Length * MINUTE) + if (curTime < startTime + stageOffset + event.Length * MINUTE) { event.Start = startTime + stageOffset; break; } - else if (singleDate) + else { tm tmCopy = Acore::Time::TimeBreakdown(curTime); int year = tmCopy.tm_year; // This year @@ -2000,11 +2002,6 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) event.Start = mktime(&tmCopy) + stageOffset; break; } - else - { - // 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 - } } } diff --git a/src/server/game/Events/HolidayDateCalculator.cpp b/src/server/game/Events/HolidayDateCalculator.cpp index 4789d7063..06552a3cf 100644 --- a/src/server/game/Events/HolidayDateCalculator.cpp +++ b/src/server/game/Events/HolidayDateCalculator.cpp @@ -580,3 +580,27 @@ std::vector HolidayDateCalculator::GetDarkmoonFaireDates(int locationO return dates; } + +time_t HolidayDateCalculator::FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates, + time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime) +{ + for (uint8_t i = 0; i < numDates && packedDates[i]; ++i) + { + uint32_t date = packedDates[i]; + + std::tm timeInfo = {}; + timeInfo.tm_year = static_cast(((date >> 24) & 0x1F)) + 100; + timeInfo.tm_mon = static_cast((date >> 20) & 0xF); + timeInfo.tm_mday = static_cast(((date >> 14) & 0x3F)) + 1; + timeInfo.tm_hour = static_cast((date >> 6) & 0x1F); + timeInfo.tm_min = static_cast(date & 0x3F); + timeInfo.tm_sec = 0; + timeInfo.tm_isdst = -1; + + time_t startTime = mktime(&timeInfo); + if (curTime < startTime + stageOffset + static_cast(stageLengthMinutes) * 60) + return startTime + stageOffset; + } + + return 0; +} diff --git a/src/server/game/Events/HolidayDateCalculator.h b/src/server/game/Events/HolidayDateCalculator.h index 7ba3eae4f..6ed4f6c67 100644 --- a/src/server/game/Events/HolidayDateCalculator.h +++ b/src/server/game/Events/HolidayDateCalculator.h @@ -100,6 +100,13 @@ public: // Returns packed dates for all occurrences in the year range static std::vector GetDarkmoonFaireDates(int locationOffset, int startYear, int numYears, int dayOffset = 0); + // Find the correct stage start time from a list of packed holiday dates. + // For multi-stage holidays, stageOffset is the cumulative duration (in seconds) of all prior stages. + // stageLengthMinutes is the duration of the current stage in minutes. + // Returns the computed stage start time (startTime + stageOffset), or 0 if no valid date found. + static time_t FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates, + time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime); + private: // Julian Date conversions for lunar calculations static double DateToJulianDay(int year, int month, double day); diff --git a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp index 2b0cd9e54..c60139fe4 100644 --- a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp +++ b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp @@ -1236,3 +1236,156 @@ TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_InHolidayRules) EXPECT_TRUE(foundMulgore) << "Darkmoon Faire Mulgore (375) should be in HolidayRules"; EXPECT_TRUE(foundTerokkar) << "Darkmoon Faire Terokkar (376) should be in HolidayRules"; } + +// ============================================================================ +// FindStartTimeForStage tests +// ============================================================================ + +class FindStartTimeForStageTest : public ::testing::Test +{ +protected: + // Helper to create a time_t from year/month/day/hour + static time_t MakeTime(int year, int month, int day, int hour = 0) + { + std::tm t = {}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_isdst = -1; + return mktime(&t); + } + + // Pack two dates into an array (rest zeroed) + void PackTwoDates(uint32_t* dates, int y1, int m1, int d1, int y2, int m2, int d2) + { + std::tm t1 = {}; + t1.tm_year = y1 - 1900; + t1.tm_mon = m1 - 1; + t1.tm_mday = d1; + t1.tm_isdst = -1; + mktime(&t1); + dates[0] = HolidayDateCalculator::PackDate(t1); + + std::tm t2 = {}; + t2.tm_year = y2 - 1900; + t2.tm_mon = m2 - 1; + t2.tm_mday = d2; + t2.tm_isdst = -1; + mktime(&t2); + dates[1] = HolidayDateCalculator::PackDate(t2); + + for (int i = 2; i < 26; ++i) + dates[i] = 0; + } +}; + +// Stage 1 (no offset): curTime before event starts -> selects first date +TEST_F(FindStartTimeForStageTest, Stage1_BeforeStart_SelectsFirstDate) +{ + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3); + + time_t curTime = MakeTime(2026, 3, 1); + time_t stageOffset = 0; + uint32_t stageLengthMin = 72 * 60; // 72 hours = 3 days + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + EXPECT_EQ(result, MakeTime(2026, 3, 6)); +} + +// Stage 1 (no offset): curTime during event -> selects current date +TEST_F(FindStartTimeForStageTest, Stage1_DuringEvent_SelectsCurrentDate) +{ + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3); + + time_t curTime = MakeTime(2026, 3, 7, 12); // mid-event + time_t stageOffset = 0; + uint32_t stageLengthMin = 72 * 60; // 3 days + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + EXPECT_EQ(result, MakeTime(2026, 3, 6)); +} + +// Stage 1 (no offset): curTime after event ends -> selects next date +TEST_F(FindStartTimeForStageTest, Stage1_AfterEnd_SelectsNextDate) +{ + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3); + + time_t curTime = MakeTime(2026, 3, 20); // well past first event + time_t stageOffset = 0; + uint32_t stageLengthMin = 72 * 60; // 3 days + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + EXPECT_EQ(result, MakeTime(2026, 4, 3)); +} + +// THE BUG: Stage 2 with stageOffset > 0, curTime in the window that the old +// code would incorrectly skip (between startTime + stageLength and +// startTime + stageOffset + stageLength). Without the fix, this would +// return the NEXT occurrence's start instead of the current one. +TEST_F(FindStartTimeForStageTest, Stage2_DuringLateWindow_SelectsCurrentDate) +{ + // Simulate Darkmoon Faire: + // Holiday starts Mar 6 (Friday, building phase) + // Stage 1 (building): 72 hours = 3 days (Mar 6-9) + // Stage 2 (active): 168 hours = 7 days (Mar 9-16) + // Total holiday: Mar 6 - Mar 16 (10 days) + // Next occurrence: Apr 3 + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3); + + time_t stageOffset = 72 * 3600; // Stage 1 = 72 hours in seconds + uint32_t stageLengthMin = 168 * 60; // Stage 2 = 168 hours in minutes + + // curTime = Mar 14 (day 8 of holiday, day 5 of stage 2) + // Old bug: startTime(Mar 6) + 168h = Mar 13, so curTime > that -> SKIP to Apr 3! + // Fixed: startTime(Mar 6) + 72h + 168h = Mar 16, so curTime < that -> correct + time_t curTime = MakeTime(2026, 3, 14, 12); + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + // Should return Mar 6 + stageOffset = Mar 9 (stage 2 start) + EXPECT_EQ(result, MakeTime(2026, 3, 6) + stageOffset); +} + +// Stage 2: curTime after entire holiday ends -> selects next occurrence +TEST_F(FindStartTimeForStageTest, Stage2_AfterHolidayEnds_SelectsNextDate) +{ + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3); + + time_t stageOffset = 72 * 3600; + uint32_t stageLengthMin = 168 * 60; + + // curTime = Mar 20 (well past the entire holiday) + time_t curTime = MakeTime(2026, 3, 20); + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + EXPECT_EQ(result, MakeTime(2026, 4, 3) + stageOffset); +} + +// No valid dates -> returns 0 +TEST_F(FindStartTimeForStageTest, NoDates_ReturnsZero) +{ + uint32_t dates[26] = {}; + time_t curTime = MakeTime(2026, 6, 1); + + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, 0, 168 * 60, curTime); + EXPECT_EQ(result, 0); +} + +// All dates in the past -> returns 0 +TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero) +{ + uint32_t dates[26] = {}; + PackTwoDates(dates, 2026, 1, 5, 2026, 2, 6); + + time_t stageOffset = 0; + uint32_t stageLengthMin = 72 * 60; + + time_t curTime = MakeTime(2026, 6, 1); // way after both dates + time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); + EXPECT_EQ(result, 0); +}