ModernUO/dev-docs/claude-skills/modernuo-event-scheduler.md
Kamron Batman 1391c563fe
chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347)
Summary

  - Adds CLAUDE.md at repo root with 14 terse code audit rules (always loaded, low token cost)
  - Adds pointer files for other AI tools: AGENTS.md (Codex), GEMINI.md, .github/COPILOT-INSTRUCTIONS.md (Copilot), .cursorrules (Cursor) — all redirect to CLAUDE.md as single source of truth
  - Gitignores /.claude so personal AI config isn't distributed
  - Moves Claude skills to dev-docs/claude-skills/ (opt-in, not auto-loaded)
  - Adds 14 dev-docs covering codebase conventions

  Code Audit Rules (in CLAUDE.md)

  1. LINQ tiered rules (Tier 1 free, Tier 2 warm, Tier 3 forbidden)
  2. No Console.WriteLine — use LogFactory.GetLogger()
  3. No concurrency primitives in game code
  4. No World.Mobiles/World.Items iteration
  5. Clean up refs in OnDelete()/OnAfterDelete()
  6. Cancel timers in OnDelete()/OnAfterDelete()
  7. STArrayPool<T>.Shared not ArrayPool<T>.Shared
  8. PooledRefList<T> not new List<T>() on hot paths
  9. Serialization: partial class, [Constructible], no serialized TimerExecutionToken
  10. No Task.Run/new Thread() in game code
  11. Never assume era — ask which expansion
  12. _camelCase fields, PascalCase properties/methods
  13. No empty gumps — use DisplayTo() pattern
  14. PropertyList string literals must be {} holes, cliloc-as-argument uses :#
2026-03-01 11:42:19 -08:00

150 lines
5.2 KiB
Markdown

---
name: modernuo-event-scheduler
description: >
Trigger when creating holiday events, seasonal content, scheduled maintenance, daily/weekly resets, or any wall-clock/calendar-based scheduling. When using EventScheduler, ScheduledEvent, YearlyScheduledEvent, or IRecurrencePattern.
---
# ModernUO EventScheduler (Wall-Clock / Calendar Scheduling)
## When This Activates
- Creating holiday or seasonal events (Halloween, Christmas, etc.)
- Scheduling daily/weekly/monthly resets or activities
- Any event that must fire at a real-world time or date
- Working with `EventScheduler`, `ScheduledEvent`, `YearlyScheduledEvent`, `CallbackScheduledEvent`
- Working with `IRecurrencePattern`, `AllowedDays`, `AllowedMonths`, `MonthDay`
## Key Rules
1. **EventScheduler for wall-clock/calendar, Timer for game-tick delays** — if the event is "at 9 AM every Monday," use EventScheduler; if it's "5 seconds from now," use Timer
2. **Always specify timezone** for local-time events — omitting defaults to UTC
3. **Prefer `CallbackScheduledEvent` (via static methods)** for simple recurring actions
4. **Use `YearlyScheduledEvent`** for seasonal windows (e.g., Oct 15 - Nov 1 each year)
5. **Cancel events on cleanup** — call `Cancel()` when disabling or shutting down
6. **1-second granularity** — EventScheduler ticks every second, not suitable for sub-second precision
## Timer vs EventScheduler Decision
| Need | Use |
|---|---|
| "Every 5 seconds" | `Timer.StartTimer` |
| "At 6:00 AM daily" | `EventScheduler.DailyAt` |
| "Delete after 10 seconds" | `Timer.StartTimer` |
| "Every Monday at noon" | `EventScheduler.WeeklyAt` |
| "Combat tick every 250ms" | `Timer.StartTimer` |
| "Oct 15 - Nov 1 each year" | `YearlyScheduledEvent` |
| "First Tuesday of each month" | `MonthlyOrdinalRecurrencePattern` |
## Quick Patterns
### Daily Event at a Specific Time
```csharp
var eastern = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var startOn = new DateTime(2025, 1, 1, 6, 0, 0); // 6:00 AM
EventScheduler.DailyAt(startOn, ResetDailyQuests, eastern);
```
### Weekly Event
```csharp
var startOn = new DateTime(2025, 1, 6, 18, 0, 0); // Monday 6:00 PM
EventScheduler.WeeklyAt(startOn, StartWeeklyTournament, eastern);
```
### Monthly Event on a Specific Day
```csharp
var startOn = new DateTime(2025, 1, 15, 12, 0, 0); // 15th at noon
EventScheduler.MonthlyAt(startOn, MonthlyRewards, eastern);
```
### Yearly Seasonal Event (with Window)
```csharp
var halloween = new YearlyCallbackScheduledEvent(
new TimeOnly(0, 0),
new MonthDay(2025, 10, 15), // Start: Oct 15
new MonthDay(2025, 11, 1), // End: Nov 1
SpawnHalloweenContent,
EventScheduler.Daily
);
halloween.Schedule(DateTime.UtcNow, eastern);
```
### Filtered Weekly (Specific Days/Months)
```csharp
var pattern = new WeeklyRecurrencePattern(
intervalWeeks: 1,
allowedMonths: AllowedMonths.June | AllowedMonths.July | AllowedMonths.August,
allowedDays: AllowedDays.Friday | AllowedDays.Saturday
);
var evt = new CallbackScheduledEvent(new TimeOnly(20, 0), SummerWeekendEvent, pattern);
evt.Schedule(DateTime.UtcNow, eastern);
```
### Ordinal Monthly (e.g., "Second Tuesday")
```csharp
var pattern = new MonthlyOrdinalRecurrencePattern(
OrdinalDayOccurrence.Second,
DayOfWeek.Tuesday
);
var evt = new CallbackScheduledEvent(new TimeOnly(12, 0), MonthlyMeeting, pattern);
evt.Schedule(DateTime.UtcNow, eastern);
```
## Custom Event Class Template
```csharp
using System;
using Server.Engines.Events;
public class MyScheduledEvent : ScheduledEvent
{
public MyScheduledEvent(TimeOnly time, IRecurrencePattern recurrence)
: base(time, recurrence)
{
}
public override void OnEvent()
{
// Your event logic here
}
}
// Schedule it:
var evt = new MyScheduledEvent(new TimeOnly(9, 0), EventScheduler.Daily);
evt.Schedule(DateTime.UtcNow, timeZone);
// Cancel it:
evt.Cancel();
```
### Custom Yearly Seasonal Event Template
```csharp
public class MySeasonalEvent : YearlyScheduledEvent
{
protected MySeasonalEvent(
TimeOnly time,
MonthDay yearlyStart,
MonthDay yearlyEnd,
IRecurrencePattern recurrence
) : base(time, yearlyStart, yearlyEnd, recurrence)
{
}
public override void OnEvent()
{
// Only fires when date is within [yearlyStart, yearlyEnd]
}
}
```
## Anti-Patterns
- **Using `Timer.StartTimer` for calendar events**: Timers drift across restarts and have no timezone support — use EventScheduler
- **Forgetting to specify timezone**: Event fires at UTC instead of expected local time — always pass `TimeZoneInfo`
- **Not cancelling events on cleanup**: Scheduled events keep firing after the system is disabled — call `Cancel()`
- **Using EventScheduler for sub-second timing**: 1-second granularity is too coarse — use `Timer.StartTimer`
- **Constructing `MonthDay` with invalid day**: Throws `ArgumentOutOfRangeException` — validate against `DateTime.DaysInMonth`
## See Also
- `dev-docs/event-scheduler.md` — Complete EventScheduler documentation
- `dev-docs/timers.md` — Game-tick timer system (Timer.StartTimer, TimerExecutionToken)
- `dev-docs/claude-skills/modernuo-timers.md` — Timer skill for game-tick delays