fix(Core/Events): correct looping holiday event start time (BG Call to Arms) (#26328)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-07-14 20:34:21 -04:00
committed by GitHub
parent 3487db8e52
commit 81cf7f92de
4 changed files with 124 additions and 0 deletions

View File

@@ -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(

View File

@@ -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<int>((packedAnchor >> 24) & 0x1F) + 100; // years since 1900 (2000-2031)
timeInfo.tm_mon = static_cast<int>((packedAnchor >> 20) & 0xF);
timeInfo.tm_mday = static_cast<int>((packedAnchor >> 14) & 0x3F) + 1;
timeInfo.tm_hour = static_cast<int>((packedAnchor >> 6) & 0x1F);
timeInfo.tm_min = static_cast<int>(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<time_t>(occurenceMinutes) * 60;
if (period > 0 && anchor < curTime)
anchor += ((curTime - anchor) / period) * period;
return anchor;
}

View File

@@ -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);

View File

@@ -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<uint32_t>(year - 2000) & 0x1F) << 24;
packed |= (static_cast<uint32_t>(month - 1) & 0xF) << 20;
packed |= (static_cast<uint32_t>(day - 1) & 0x3F) << 14;
packed |= (static_cast<uint32_t>(hour) & 0x1F) << 6;
packed |= (static_cast<uint32_t>(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<time_t>(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<time_t>(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));
}