fix(Core/Events): Fix multi-stage holiday events ending early on restart (#25032)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: sudlud <sudlud@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-03-10 18:47:17 -05:00
committed by GitHub
parent 6f7547bf52
commit 10b4f04c44
4 changed files with 199 additions and 18 deletions

View File

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

View File

@@ -580,3 +580,27 @@ std::vector<uint32_t> 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<int>(((date >> 24) & 0x1F)) + 100;
timeInfo.tm_mon = static_cast<int>((date >> 20) & 0xF);
timeInfo.tm_mday = static_cast<int>(((date >> 14) & 0x3F)) + 1;
timeInfo.tm_hour = static_cast<int>((date >> 6) & 0x1F);
timeInfo.tm_min = static_cast<int>(date & 0x3F);
timeInfo.tm_sec = 0;
timeInfo.tm_isdst = -1;
time_t startTime = mktime(&timeInfo);
if (curTime < startTime + stageOffset + static_cast<time_t>(stageLengthMinutes) * 60)
return startTime + stageOffset;
}
return 0;
}

View File

@@ -100,6 +100,13 @@ public:
// Returns packed dates for all occurrences in the year range
static std::vector<uint32_t> 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);

View File

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