From f95b3a8b3b7f0ac1a0706491efc8bb30fc67b5db Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 7 May 2025 22:42:44 -0700 Subject: [PATCH] fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178) --- .../Engines/Events/EventSchedulerTests.cs | 41 ++--- .../Engines/Events/RecurrencePatternTests.cs | 147 +++++++++++++++++- .../UOContent/Engines/Events/AllowedDays.cs | 22 +++ .../UOContent/Engines/Events/AllowedMonths.cs | 23 +++ .../Engines/Events/BaseScheduledEvent.cs | 67 ++++++++ .../Engines/Events/CallbackScheduledEvent.cs | 16 +- .../Events/CommonRecurrencePatterns.cs | 45 ++++-- .../Engines/Events/EventScheduler.cs | 51 ++---- .../Engines/Events/ScheduledEvent.cs | 40 +---- .../Events/YearlyCallbackScheduledEvent.cs | 14 +- .../Engines/Events/YearlyScheduledEvent.cs | 18 +-- 11 files changed, 347 insertions(+), 137 deletions(-) create mode 100644 Projects/UOContent/Engines/Events/AllowedDays.cs create mode 100644 Projects/UOContent/Engines/Events/AllowedMonths.cs create mode 100644 Projects/UOContent/Engines/Events/BaseScheduledEvent.cs diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs index 22a1ff65e..65da0271c 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs @@ -41,8 +41,8 @@ public class EventSchedulerTests public int CallCount { get; private set; } public Action Callback { get; } - public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null) - : base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc) + public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null) + : base(time, recurrence) { CallCount = 0; Callback = callback; @@ -105,25 +105,28 @@ public class EventSchedulerTests var executionOrder = new List(); // Create events with staggered occurrences - var evt3 = new TestScheduledEvent( - Core._now.AddSeconds(30), - () => executionOrder.Add(3) - ); - + var evt1Start = Core._now.AddSeconds(10); var evt1 = new TestScheduledEvent( - Core._now.AddSeconds(10), + TimeOnly.FromDateTime(Core._now.AddSeconds(10)), () => executionOrder.Add(1) ); + var evt2Start = Core._now.AddSeconds(20); var evt2 = new TestScheduledEvent( - Core._now.AddSeconds(20), + TimeOnly.FromDateTime(evt2Start), () => executionOrder.Add(2) ); + var evt3Start = Core._now.AddSeconds(30); + var evt3 = new TestScheduledEvent( + TimeOnly.FromDateTime(evt3Start), + () => executionOrder.Add(3) + ); + // Add out of order - EventScheduler.Shared.ScheduleEvent(evt3); - EventScheduler.Shared.ScheduleEvent(evt1); - EventScheduler.Shared.ScheduleEvent(evt2); + evt3.Schedule(evt3Start); + evt1.Schedule(evt1Start); + evt2.Schedule(evt2Start); // Advance time to after all events Core._now = Core._now.AddSeconds(40); @@ -157,12 +160,12 @@ public class EventSchedulerTests var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3); var evt = new TestScheduledEvent( - Core._now, + TimeOnly.FromDateTime(Core._now), () => callCount++, recurrence ); - EventScheduler.Shared.ScheduleEvent(evt); + evt.Schedule(Core._now); // Advance time to after all occurrences should have happened Core._now = Core._now.AddSeconds(50); @@ -271,14 +274,13 @@ public class EventSchedulerTests var endTime = Core._now.AddSeconds(36); var customEvent = new CustomEvent( - startTime, - endTime, TimeOnly.FromDateTime(startTime), + endTime, recurrence, () => callCount++ ); - EventScheduler.Shared.ScheduleEvent(customEvent); + customEvent.Schedule(startTime); Core._now = Core._now.AddSeconds(50); Timer.Slice(8); @@ -297,12 +299,11 @@ public class EventSchedulerTests private readonly Action _callback; public CustomEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, + DateTime endOn, IRecurrencePattern recurrence, Action callback - ) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback; + ) : base(time, endOn, recurrence) => _callback = callback; public override void OnEvent() { diff --git a/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs index 7917194c8..b08a49007 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Events/RecurrencePatternTests.cs @@ -51,6 +51,152 @@ public class RecurrencePatternTests Assert.Equal(expected, result); } + [Theory] + [InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.All, 2024, 5, 16, 12, 30)] // Next day (Thursday) + [InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.Monday, 2024, 5, 20, 12, 30)] // Next Monday + [InlineData(2024, 5, 15, 12, 30, 2, AllowedDays.Wednesday, 2024, 5, 29, 12, 30)] // Biweekly Wednesday + public void WeeklyRecurrencePattern_BasicPatterns_ReturnsCorrectDay( + int startYear, int startMonth, int startDay, int startHour, int startMinute, + int intervalWeeks, AllowedDays allowedDays, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute) + { + // Arrange + var tz = TimeZoneInfo.Utc; + var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays); + var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(2024, 12, 30, 12, 30, 1, AllowedDays.Monday, 2025, 1, 6, 12, 30)] // Across year boundary + [InlineData(2024, 5, 31, 12, 30, 1, AllowedDays.Saturday, 2024, 6, 1, 12, 30)] // Across month boundary + public void WeeklyRecurrencePattern_CrossBoundaries_ReturnsCorrectDay( + int startYear, int startMonth, int startDay, int startHour, int startMinute, + int intervalWeeks, AllowedDays allowedDays, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute) + { + // Arrange + var tz = TimeZoneInfo.Utc; + var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays); + var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(2024, 5, 15, 12, 30, AllowedMonths.January | AllowedMonths.February, AllowedDays.Wednesday, 2025, 1, 1, 12, 30)] // Skip to January + [InlineData(2024, 12, 15, 12, 30, AllowedMonths.January | AllowedMonths.December, AllowedDays.Wednesday, 2024, 12, 18, 12, 30)] // Same month + [InlineData(2024, 1, 31, 12, 30, AllowedMonths.February | AllowedMonths.April, AllowedDays.Saturday, 2024, 2, 3, 12, 30)] // Next month allowed + public void WeeklyRecurrencePattern_MonthFiltering_ReturnsCorrectDay( + int startYear, int startMonth, int startDay, int startHour, int startMinute, + AllowedMonths allowedMonths, AllowedDays allowedDays, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute) + { + // Arrange + var tz = TimeZoneInfo.Utc; + var pattern = new WeeklyRecurrencePattern(1, allowedMonths, allowedDays); + var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("America/New_York", 2024, 3, 9, 12, 0, 1, AllowedDays.Sunday, 2024, 3, 10, 12, 0)] // Before spring forward + [InlineData("America/New_York", 2024, 3, 10, 2, 30, 1, AllowedDays.Sunday, 2024, 3, 17, 2, 30)] // During spring forward (invalid time) + [InlineData("America/New_York", 2024, 11, 2, 12, 0, 1, AllowedDays.Sunday, 2024, 11, 3, 12, 0)] // Before fall back + [InlineData("America/New_York", 2024, 11, 3, 1, 30, 1, AllowedDays.Sunday, 2024, 11, 10, 1, 30)] // During fall back (ambiguous time) + public void WeeklyRecurrencePattern_DSTTransitions_HandlesCorrectly( + string tzId, int startYear, int startMonth, int startDay, int startHour, int startMinute, + int intervalWeeks, AllowedDays allowedDays, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute) + { + // Arrange + var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId); + var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays); + + var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0); + var afterUtc = startLocal.LocalToUtc(tz); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz); + Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0), resultLocal); + } + + [Theory] + [InlineData(2024, 5, 29, 12, 0, AllowedDays.None, 2024, 5, 29, 12, 30)] // Default to current day + [InlineData(2024, 5, 29, 12, 0, AllowedDays.Wednesday | AllowedDays.Friday, 2024, 5, 29, 12, 30)] // Current day is Wednesday + [InlineData(2024, 5, 29, 12, 0, AllowedDays.Monday | AllowedDays.Friday, 2024, 5, 31, 12, 30)] // Next allowed day (Friday) + public void WeeklyRecurrencePattern_DaysOfWeekHandling_ReturnsCorrectDay( + int startYear, int startMonth, int startDay, int startHour, int startMinute, + AllowedDays allowedDays, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute) + { + // Arrange + var tz = TimeZoneInfo.Utc; + var pattern = new WeeklyRecurrencePattern(1, AllowedMonths.All, allowedDays); + var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(2024, 1, 29, 12, 0, 1, 2024, 1, 29, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 1, 30, 12, 30)] // Current month allowed + [InlineData(2024, 1, 31, 12, 0, 1, 2024, 1, 31, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 3, 1, 12, 30)] // Skip February + public void WeeklyRecurrencePattern_WeekSpanningMonths_ReturnsCorrectDay( + int startYear, int startMonth, int startDay, int startHour, int startMinute, + int intervalWeeks, + int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute, + AllowedMonths allowedMonths, + int nextExpectedYear, int nextExpectedMonth, int nextExpectedDay, int nextExpectedHour, int nextExpectedMinute) + { + // Arrange + var tz = TimeZoneInfo.Utc; + var pattern = new WeeklyRecurrencePattern(intervalWeeks, allowedMonths); + var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc); + var time = new TimeOnly(expectedHour, expectedMinute); + + // Act - first call should hit expectedDate + var result = pattern.GetNextOccurrence(afterUtc, time, tz); + + // Assert + var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(expected, result); + + // Call again - should move to next available day in allowed months + var nextResult = pattern.GetNextOccurrence(result, time, tz); + var nextExpected = new DateTime(nextExpectedYear, nextExpectedMonth, nextExpectedDay, nextExpectedHour, nextExpectedMinute, 0, DateTimeKind.Utc); + Assert.Equal(nextExpected, nextResult); + } + [Theory] [InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)] [InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)] @@ -145,7 +291,6 @@ public class RecurrencePatternTests Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result); } - // Additional utility method tests [Theory] [InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time public void LocalToUtc_AmbiguousTime_HandlesConsistently( diff --git a/Projects/UOContent/Engines/Events/AllowedDays.cs b/Projects/UOContent/Engines/Events/AllowedDays.cs new file mode 100644 index 000000000..d662a2e11 --- /dev/null +++ b/Projects/UOContent/Engines/Events/AllowedDays.cs @@ -0,0 +1,22 @@ +using System; + +namespace Server.Engines.Events; + +[Flags] +public enum AllowedDays : byte +{ + None = 0, + Sunday = 1 << 0, + Monday = 1 << 1, + Tuesday = 1 << 2, + Wednesday = 1 << 3, + Thursday = 1 << 4, + Friday = 1 << 5, + Saturday = 1 << 6, + All = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday +} + +public static class DaysOfWeekExtension +{ + public static AllowedDays ToDaysOfWeek(this DayOfWeek dayOfWeek) => (AllowedDays)(1 << (int)dayOfWeek); +} diff --git a/Projects/UOContent/Engines/Events/AllowedMonths.cs b/Projects/UOContent/Engines/Events/AllowedMonths.cs new file mode 100644 index 000000000..1103e7b9e --- /dev/null +++ b/Projects/UOContent/Engines/Events/AllowedMonths.cs @@ -0,0 +1,23 @@ +using System; + +namespace Server.Engines.Events; + +[Flags] +public enum AllowedMonths +{ + None = 0, + January = 1 << 0, + February = 1 << 1, + March = 1 << 2, + April = 1 << 3, + May = 1 << 4, + June = 1 << 5, + July = 1 << 6, + August = 1 << 7, + September = 1 << 8, + October = 1 << 9, + November = 1 << 10, + December = 1 << 11, + + All = January | February | March | April | May | June | July | August | September | October | November | December +} diff --git a/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs b/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs new file mode 100644 index 000000000..279de8502 --- /dev/null +++ b/Projects/UOContent/Engines/Events/BaseScheduledEvent.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.CompilerServices; +using Server.Logging; + +namespace Server.Engines.Events; + +public abstract class BaseScheduledEvent +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseScheduledEvent)); + + public TimeZoneInfo TimeZone { get; private set; } = TimeZoneInfo.Utc; + public DateTime NextOccurrence { get; private set; } = DateTime.MaxValue; + public bool Cancelled { get; private set; } + public EventScheduler Scheduler { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Schedule(DateTime startAfter, TimeZoneInfo timeZone = null) => + Schedule(EventScheduler.Shared, startAfter, timeZone); + + public void Schedule(EventScheduler scheduler, DateTime startAfter, TimeZoneInfo timeZone = null) + { + Cancel(); + Scheduler = scheduler; + TimeZone = timeZone ?? TimeZoneInfo.Utc; + Schedule(startAfter, timeZone, true); + } + + private void Schedule(DateTime startAfter, TimeZoneInfo timeZone, bool isFirst) + { + Cancelled = false; + var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(timeZone); + + var next = GetNextOccurrence(afterUtc); + + // For the first occurrence, we should set it to the startAfter date if we have no recurrence. + NextOccurrence = next == DateTime.MaxValue && isFirst ? afterUtc : next; + + if (NextOccurrence != DateTime.MaxValue) + { + Scheduler.ScheduleEvent(this); + } + } + + protected abstract DateTime GetNextOccurrence(DateTime after); + + public void Cancel() + { + Cancelled = true; + Scheduler?.UnscheduleEvent(this); + } + + public virtual void Advance() + { + try + { + OnEvent(); + } + catch (Exception ex) + { + logger.Error(ex, "OnEvent failed to execute."); + } + + Schedule(NextOccurrence, TimeZone, false); + } + + public abstract void OnEvent(); +} diff --git a/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs index e142cf247..cab4e08fe 100644 --- a/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/CallbackScheduledEvent.cs @@ -7,23 +7,17 @@ public sealed class CallbackScheduledEvent : ScheduledEvent private readonly Action _callback; public CallbackScheduledEvent( - DateTime after, TimeOnly time, Action callback, - IRecurrencePattern recurrencePattern = null, - TimeZoneInfo timeZone = null - ) : base(after, time, recurrencePattern, timeZone) => - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + IRecurrencePattern recurrencePattern = null + ) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); public CallbackScheduledEvent( - DateTime after, - DateTime endOn, TimeOnly time, + DateTime endOn, Action callback, - IRecurrencePattern recurrencePattern = null, - TimeZoneInfo timeZone = null - ) : base(after, endOn, time, recurrencePattern, timeZone) => - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + IRecurrencePattern recurrencePattern = null + ) : base(time, endOn, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback)); public override void OnEvent() => _callback(); } diff --git a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs index 461f8e38e..f60358f3f 100644 --- a/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs +++ b/Projects/UOContent/Engines/Events/CommonRecurrencePatterns.cs @@ -35,32 +35,49 @@ public class DailyRecurrencePattern : IRecurrencePattern public class WeeklyRecurrencePattern : IRecurrencePattern { public int IntervalWeeks { get; } - public DaysOfWeek DaysOfWeek { get; } + public AllowedDays AllowedDays { get; } + public AllowedMonths AllowedMonths { get; } - public WeeklyRecurrencePattern(int intervalWeeks = 1, DaysOfWeek daysOfWeek = DaysOfWeek.None) + public WeeklyRecurrencePattern(int intervalWeeks = 1, AllowedMonths allowedMonths = AllowedMonths.All, AllowedDays allowedDays = AllowedDays.None) { IntervalWeeks = Math.Max(1, intervalWeeks); - DaysOfWeek = daysOfWeek; + AllowedDays = allowedDays == AllowedDays.None ? AllowedDays.All : allowedDays; + AllowedMonths = allowedMonths == AllowedMonths.None ? AllowedMonths.All : allowedMonths; } public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone) { var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone); - var daysOfWeek = DaysOfWeek == DaysOfWeek.None - ? (DaysOfWeek)(1 << (int)local.DayOfWeek) - : DaysOfWeek; + var daysOfWeek = AllowedDays == AllowedDays.None ? local.DayOfWeek.ToDaysOfWeek() : AllowedDays; var weekStart = local.Date.AddDays(-(int)local.DayOfWeek); // No more days in this week, jump IntervalWeeks ahead - for (int week = 0; week <= 100; week++) + for (var week = 0; week <= 52; week++) { - DateTime nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); + var nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week); + var weekEnd = nextWeekStart.AddDays(6); - for (int i = 0; i < 7; i++) + var startMonth = (AllowedMonths)(1 << (nextWeekStart.Month - 1)); + var endMonth = (AllowedMonths)(1 << (weekEnd.Month - 1)); + + // Skip the entire week if the start and end months are not in the allowed months + if ((AllowedMonths & (startMonth | endMonth)) == 0) + { + continue; + } + + for (var i = 0; i < 7; i++) { var day = nextWeekStart.AddDays(i); - var dayOfWeekFlag = (DaysOfWeek)(1 << (int)day.DayOfWeek); + + var currentMonth = (AllowedMonths)(1 << (day.Month - 1)); + if ((AllowedMonths & currentMonth) == 0) + { + continue; + } + + var dayOfWeekFlag = (AllowedDays)(1 << (int)day.DayOfWeek); if ((daysOfWeek & dayOfWeekFlag) != 0) { var candidate = new DateTime(day.Year, day.Month, day.Day, time.Hour, time.Minute, 0); @@ -94,7 +111,7 @@ public class MonthlyRecurrencePattern : IRecurrencePattern var month = local.Month; var day = DayOfMonth == -1 ? local.Day : DayOfMonth; - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var nextMonth = month + IntervalMonths * i; var candidate = new DateTime(year, 1, 1) @@ -132,7 +149,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern var year = local.Year; var month = local.Month; - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var nextMonth = month + IntervalMonths * i; var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset); @@ -147,7 +164,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern .Add(time.ToTimeSpan()); // Find the first occurrence of the desired day - int daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; + var daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7; if (daysOffset > 7) { daysOffset -= 7; @@ -168,7 +185,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern .Add(time.ToTimeSpan()); // Find the last occurrence of the desired day - int daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; + var daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7; if (daysOffset >= 7) { daysOffset -= 7; diff --git a/Projects/UOContent/Engines/Events/EventScheduler.cs b/Projects/UOContent/Engines/Events/EventScheduler.cs index 8d442fe20..992110371 100644 --- a/Projects/UOContent/Engines/Events/EventScheduler.cs +++ b/Projects/UOContent/Engines/Events/EventScheduler.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Server.Logging; namespace Server.Engines.Events; @@ -17,25 +16,9 @@ public interface IRecurrencePattern public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth } -[Flags] -public enum DaysOfWeek : byte -{ - None = 0, - Sunday = 1, - Monday = 2, - Tuesday = 4, - Wednesday = 8, - Thursday = 16, - Friday = 32, - Saturday = 64, - EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday -} - public class EventScheduler : Timer { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler)); - - private readonly PriorityQueue _schedule = new(); + private readonly PriorityQueue _schedule = new(); public static EventScheduler Shared { get; private set; } @@ -116,16 +99,24 @@ public class EventScheduler : Timer TimeZoneInfo timeZone = null ) { - var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone); - ScheduleEvent(scheduledEvent); + var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern); + scheduledEvent.Schedule(this, after, timeZone); return scheduledEvent; } - public void ScheduleEvent(ScheduledEvent entry) + public void ScheduleEvent(BaseScheduledEvent entry) + { + if (entry != null && entry.NextOccurrence < DateTime.MaxValue) + { + _schedule.Enqueue(entry, entry.NextOccurrence); + } + } + + public void UnscheduleEvent(BaseScheduledEvent entry) { if (entry != null) { - _schedule.Enqueue(entry, entry.NextOccurrence); + _schedule.Remove(entry, out _, out _); } } @@ -149,21 +140,7 @@ public class EventScheduler : Timer continue; } - DateTime nextOccurrence; - try - { - nextOccurrence = entry.Advance(); - } - catch (Exception e) - { - logger.Error(e, "Error while executing scheduled event."); - nextOccurrence = DateTime.MaxValue; - } - - if (nextOccurrence < DateTime.MaxValue) - { - _schedule.Enqueue(entry, nextOccurrence); - } + entry.Advance(); // Advances the event and self queues if necessary } } } diff --git a/Projects/UOContent/Engines/Events/ScheduledEvent.cs b/Projects/UOContent/Engines/Events/ScheduledEvent.cs index 082017e0e..3f90227cb 100644 --- a/Projects/UOContent/Engines/Events/ScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/ScheduledEvent.cs @@ -2,59 +2,31 @@ using System; namespace Server.Engines.Events; -public abstract class ScheduledEvent +public abstract class ScheduledEvent : BaseScheduledEvent { public IRecurrencePattern Recurrence { get; } - public TimeZoneInfo TimeZone { get; } public TimeOnly Time { get; } public DateTime EndDate { get; } - public DateTime NextOccurrence { get; protected set; } - public bool Cancelled { get; private set; } - public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null) - : this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone) - { - } - - public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null) - : this(startAfter, DateTime.MaxValue, time, recurrence, timeZone) + public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null) + : this(time, DateTime.MaxValue, recurrence) { } public ScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null + DateTime endOn, + IRecurrencePattern recurrence = null ) { Time = time; Recurrence = recurrence; - TimeZone = timeZone ?? TimeZoneInfo.Utc; EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone); - - var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone); - - var next = GetOccurrence(afterUtc); - // For the first occurrence, we should set it to the startAfter date if we have no recurrence. - NextOccurrence = next == DateTime.MaxValue ? afterUtc : next; } - protected virtual DateTime GetOccurrence(DateTime after) + protected override DateTime GetNextOccurrence(DateTime after) { var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue; return next >= EndDate ? DateTime.MaxValue : next; } - - public void Cancel() => Cancelled = true; - - public virtual DateTime Advance() - { - OnEvent(); - - return NextOccurrence = GetOccurrence(NextOccurrence); - } - - public abstract void OnEvent(); } diff --git a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs index 75036d226..f4ca34f65 100644 --- a/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/YearlyCallbackScheduledEvent.cs @@ -7,27 +7,23 @@ public class YearlyCallbackScheduledEvent : YearlyScheduledEvent private readonly Action _callback; protected YearlyCallbackScheduledEvent( - DateTime startAfter, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, Action callback, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone) + IRecurrencePattern recurrence + ) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence) { } protected YearlyCallbackScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, + DateTime endOn, Action callback, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _callback = callback; + IRecurrencePattern recurrence + ) : base(time, yearlyStart, yearlyEnd, endOn, recurrence) => _callback = callback; public override void OnEvent() => _callback(); } diff --git a/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs index d29356839..0deceaca8 100644 --- a/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs +++ b/Projects/UOContent/Engines/Events/YearlyScheduledEvent.cs @@ -9,33 +9,29 @@ public abstract class YearlyScheduledEvent : ScheduledEvent public MonthDay YearlyEnd { get; } protected YearlyScheduledEvent( - DateTime startAfter, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone) + IRecurrencePattern recurrence + ) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence) { } protected YearlyScheduledEvent( - DateTime startAfter, - DateTime endOn, TimeOnly time, MonthDay yearlyStart, MonthDay yearlyEnd, - IRecurrencePattern recurrence, - TimeZoneInfo timeZone = null - ) : base(startAfter, endOn, time, recurrence, timeZone) + DateTime endOn, + IRecurrencePattern recurrence + ) : base(time, endOn, recurrence) { YearlyStart = yearlyStart; YearlyEnd = yearlyEnd; } - protected override DateTime GetOccurrence(DateTime after) + protected override DateTime GetNextOccurrence(DateTime after) { - var next = base.GetOccurrence(after); + var next = base.GetNextOccurrence(after); if (next == DateTime.MaxValue) {