diff --git a/src/server/game/Events/GameEventMgr.cpp b/src/server/game/Events/GameEventMgr.cpp index ee987592e..fb1137076 100644 --- a/src/server/game/Events/GameEventMgr.cpp +++ b/src/server/game/Events/GameEventMgr.cpp @@ -1960,6 +1960,17 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) time_t curTime = GameTime::GetGameTime().count(); + if (holiday->Looping) + { + // Looping events (Battleground Call to Arms) carry a single past anchor in the DBC and + // recur via event.Occurence. Anchor to that DBC date so the window keeps the correct phase + // instead of inheriting the wrong time of day from a stale game_event.start_time. + if (time_t start = HolidayDateCalculator::FindLoopingStartTime(holiday->Date[0], stageOffset, event.Occurence, curTime)) + event.Start = start; + + return; + } + if (!singleDate) { time_t start = HolidayDateCalculator::FindStartTimeForStage( diff --git a/src/server/game/Events/HolidayDateCalculator.cpp b/src/server/game/Events/HolidayDateCalculator.cpp index c507f5413..367fd7056 100644 --- a/src/server/game/Events/HolidayDateCalculator.cpp +++ b/src/server/game/Events/HolidayDateCalculator.cpp @@ -671,3 +671,28 @@ time_t HolidayDateCalculator::FindStartTimeForStage(const uint32_t* packedDates, return 0; } + +time_t HolidayDateCalculator::FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, time_t curTime) +{ + if (!packedAnchor) + return 0; + + std::tm timeInfo = {}; + timeInfo.tm_year = static_cast((packedAnchor >> 24) & 0x1F) + 100; // years since 1900 (2000-2031) + timeInfo.tm_mon = static_cast((packedAnchor >> 20) & 0xF); + timeInfo.tm_mday = static_cast((packedAnchor >> 14) & 0x3F) + 1; + timeInfo.tm_hour = static_cast((packedAnchor >> 6) & 0x1F); + timeInfo.tm_min = static_cast(packedAnchor & 0x3F); + timeInfo.tm_sec = 0; + timeInfo.tm_isdst = -1; + + time_t anchor = mktime(&timeInfo) + stageOffset; + + // Roll forward by whole periods to the most recent occurrence, preserving phase. + time_t const period = static_cast(occurenceMinutes) * 60; + if (period > 0 && anchor < curTime) + anchor += ((curTime - anchor) / period) * period; + + return anchor; +} diff --git a/src/server/game/Events/HolidayDateCalculator.h b/src/server/game/Events/HolidayDateCalculator.h index 6ed4f6c67..50b9eb909 100644 --- a/src/server/game/Events/HolidayDateCalculator.h +++ b/src/server/game/Events/HolidayDateCalculator.h @@ -107,6 +107,11 @@ public: static time_t FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates, time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime); + // Start time for a looping holiday event (Battleground Call to Arms): rolls the packed anchor + // forward by whole occurenceMinutes periods to the most recent occurrence. Returns 0 if anchor is 0. + static time_t FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, 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 19084eb6f..fb16fce9f 100644 --- a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp +++ b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp @@ -1428,3 +1428,86 @@ TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero) time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); EXPECT_EQ(result, 0); } + +// ============================================================================ +// FindLoopingStartTime tests (Battleground Call to Arms) +// ============================================================================ + +class FindLoopingStartTimeTest : public ::testing::Test +{ +protected: + 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); + } + + static uint32_t PackAnchor(int year, int month, int day, int hour, int minute) + { + uint32_t packed = 0; + packed |= (static_cast(year - 2000) & 0x1F) << 24; + packed |= (static_cast(month - 1) & 0xF) << 20; + packed |= (static_cast(day - 1) & 0x3F) << 14; + packed |= (static_cast(hour) & 0x1F) << 6; + packed |= (static_cast(minute) & 0x3F); + return packed; + } + + static constexpr uint32_t OCCURENCE_MIN = 60480; // 42 day cycle (AV Call to Arms) +}; + +TEST_F(FindLoopingStartTimeTest, ZeroAnchor_ReturnsZero) +{ + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(0, 0, OCCURENCE_MIN, curTime), 0); +} + +TEST_F(FindLoopingStartTimeTest, FutureAnchor_NotRolled) +{ + uint32_t anchor = PackAnchor(2026, 10, 30, 0, 0); + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime), + MakeTime(2026, 10, 30)); +} + +// The bug: an ancient midnight anchor must roll forward preserving phase, not fall back to a stale start_time. +TEST_F(FindLoopingStartTimeTest, AncientAnchor_RollsToMostRecentOccurrence) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime); + + time_t const period = static_cast(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26); + + EXPECT_LE(result, curTime); + EXPECT_GT(result + period, curTime); + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, StageOffsetApplied) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t stageOffset = 96 * 3600; + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, stageOffset, OCCURENCE_MIN, curTime); + + time_t const period = static_cast(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26) + stageOffset; + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, ZeroOccurence_ReturnsAnchor) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, 0, curTime), + MakeTime(2007, 10, 26)); +}