Meta

Scheduler

Persistent fire-at-time callbacks that survive restarts. Server-only.

Server
services.AddCrossplayScheduler();
Depends on
core
Shape
server-only · zero wire

Seams you implement 1

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayScheduler(); and it wins.

IScheduleHandler game SPI

The SPI a game implements per KIND: when an entry of your kind comes due, you get its opaque payload. "respawn" → bring the boss back; "daily-reset" → roll the shops; "auction-expiry" → close the listing. Register any number, each answering one kind.

public sealed class RespawnHandler : IScheduleHandler
{
    public string Kind => "respawn";
    public void OnDue(ScheduledEntry entry)
    {
        int bossId = BitConverter.ToInt32(entry.Payload);
        _world.Spawn(bossId);
    }
}
  • string Kind { get; }

    The kind this handler answers (the routing key; must be unique across handlers).

  • void OnDue(ScheduledEntry entry)

    An entry of this kind came due — act on its opaque Payload.

Providers you can swap 2

Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.

IScheduleStore provider

Persistence seam: pending entries must survive restarts. In-memory default; the persistence package overrides with a document-backed store.

  • void Delete(long id)

    Removes a persisted entry by id (it fired or was cancelled).

  • IReadOnlyList<ScheduledEntry> LoadAll()

    Every persisted pending entry — read once at startup to rehydrate the queue.

  • long NextId()

    Allocates the next monotonic entry id.

  • void Save(ScheduledEntry entry)

    Persists (inserts or replaces) one pending entry.

IWallClock provider

Wall-clock seam (restart-safe time, unlike the uptime-based server clock). Fakeable.

  • long UtcNowMs { get; }

    Milliseconds since the Unix epoch, UTC.

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

ISchedulerService

Schedules persistent timers.

  • bool Cancel(long id)

    Cancels a pending entry. False when it already fired or never existed.

  • int PendingCount { get; }

    Pending entries (diagnostics).

  • long Schedule(string kind, byte[] payload, long fireAtUtcMs)

    Schedules an entry; returns its id. Fires on or shortly after fireAtUtcMs (sweep granularity) — including after a restart, if the store is durable.

  • long ScheduleIn(string kind, byte[] payload, double delaySeconds)

    Convenience: fire delaySeconds from now.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

SchedulerOptions

Configurable scheduler rules (defaults here, never inline).

  • int MaxFiresPerSweep { get; set; }

    Max entries fired per sweep (a backlog after downtime drains over several sweeps).

  • int MaxPayloadBytes { get; set; }

    Maximum payload bytes per entry.

  • float SweepIntervalSeconds { get; set; }

    Seconds between due-item sweeps (timers are coarse by design — not a frame clock).