Progression

Daily Rewards

Interval claims with grace-window streaks; reward is an IDailyRewardGrantor game SPI.

Server
services.AddCrossplayDailyRewards();
Unity package
com.crossplay.dailyrewards
Depends on
core

Seams you implement 1

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

IDailyRewardGrantor game SPI

THE game hook: what a daily reward IS. The piece owns the interval/streak MECHANIC; this SPI owns the PAYOUT. Implement it to decide what a claim gives; the returned byte[] is the OPAQUE blob echoed to the client in DailyClaimResponse.RewardData — the framework never interprets it, so one game encodes coins, another a chest id, another a text line.

public sealed class MyGrantor : IDailyRewardGrantor
{
    private readonly IEconomyService _economy;
    public MyGrantor(IEconomyService economy) => _economy = economy;
    public byte[] Grant(ISession session, int streak)
    {
        int coins = 50 + (streak - 1) * 10;               // more per streak day
        _economy.TryCredit(session, GoldCurrencyId, coins, "dailyreward");
        return BitConverter.GetBytes(coins);              // opaque blob the client renders
    }
}
  • byte[] Grant(ISession session, int streak)

    Produces the reward for one successful claim and returns the opaque blob sent to the client.

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.

IDailyClock provider

Wall-clock seam (unix-epoch UTC milliseconds) so the interval/streak math is testable and restart-safe. Piece-local on purpose: feature pieces depend on Core only, never on the Scheduler piece. Override it (a fake clock) in tests to advance time without waiting; the default SystemDailyClock reads the real system clock.

  • long UtcNowMs { get; }

    The current time as unix-epoch milliseconds, UTC.

IDailyRewardStore provider

Claim-state persistence SPI, keyed by the owner (character) id. The default InMemoryDailyRewardStore keeps records in RAM (dev/tests); register a document-backed override (e.g. via AddCrossplayPersistentStores) BEFORE AddCrossplayDailyRewards to survive restarts — it is a TryAdd seam.

  • DailyClaimRecord Get(long characterId)

    Loads the owner's claim record, or null if they have never claimed.

  • void Save(DailyClaimRecord record)

    Upserts the owner's claim record (called after each successful claim).

Services you call 1

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

IDailyRewardService

Server-side claim/streak API — the interface game code CALLS (and the wire handlers sit on top of). Resolve it from DI: [Inject] IDailyRewardService rewards. All timing is server-authoritative; clients only request a claim or read status over the wire.

  • DailyStatus StatusOf(ISession session)

    Reads the claimability/streak/countdown for the session's owner without claiming.

  • bool TryClaim(ISession session, out int streak, out byte[] rewardData, out ushort reason)

    Attempts a claim for the session's owner, advancing or resetting the streak per the interval/grace rules.

Configuration 1

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

DailyRewardOptions

Tuning for the DailyRewards piece — the interval-claim + grace-window streak mechanic (defaults here, never inline). Passed to the configure callback of AddCrossplayDailyRewards.

  • long BridgeBaseAmount { get; set; }

    Base amount credited per claim.

  • ushort BridgeCurrencyId { get; set; }

    Currency the bridge credits per claim. 0 = the bridge is inert (SPI default applies).

  • string BridgeMailFrom { get; set; }

    Sender name on milestone mail (the game's fiction, not a framework string).

  • string BridgeMailSubject { get; set; }

    Subject on milestone mail.

  • int BridgeMilestoneEvery { get; set; }

    Every Nth streak day also sends a milestone mail (0 = never).

  • int BridgeMilestoneItemCount { get; set; }

    Attachment count for the milestone item.

  • byte BridgeMilestoneItemKind { get; set; }

    Opaque item kind attached to milestone mail (0 = mail without attachment).

  • long BridgePerStreakBonus { get; set; }

    Extra amount per streak day beyond the first (day N pays base + (N-1)×bonus).

  • double ClaimIntervalHours { get; set; }

    Minimum hours between successful claims. A claim inside this window is refused with AlreadyClaimed. Default 20 — a classic daily with slack for drifting play times.

  • int MaxStreak { get; set; }

    Upper bound the streak counter is clamped to. 0 = unbounded (grow forever).

  • double StreakGraceHours { get; set; }

    Grace window, in hours measured from the LAST claim: claiming within it continues the streak, claiming after it resets the streak to 1. Keep it longer than ClaimIntervalHours (default 48).

Wire messages 4

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

3001 DailyClaimRequest

Client → server: claim the current interval's reward for the selected character. Carries no fields — the server resolves the claimant's owner id from the authenticated session and applies the interval/streak rules. The reply is a DailyClaimResponse (plus a fresh DailyStatus).

3002 DailyClaimResponse

Server → client: the verdict for a DailyClaimRequest. RewardData is the game's opaque blob (the framework never knows what a reward IS — the game's IDailyRewardGrantor produced it, the game's client renders it: coins, a chest animation, a text line…).

3003 DailyStatusRequest

Client → server: ask for the current streak/claimability without claiming. Carries no fields (owner resolved from the session); the reply is a DailyStatus.

3004 DailyStatus

Server → client: the streak/countdown snapshot — sent as the reply to a DailyStatusRequest and, unsolicited, immediately after every successful claim so the client's countdown refreshes.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IDailyRewardClient

The DailyRewards piece, client side: claim the interval reward and follow the streak. The reward blob in the claim response is the GAME's vocabulary — this client never interprets it.

  • UniTask<DailyClaimResponse> ClaimAsync(CancellationToken ct = default)

    Claims today's reward (refusals carry the reason code).

  • UniTask<DailyStatus> StatusAsync(CancellationToken ct = default)

    Requests the current streak status.

  • event Action<DailyStatus> StatusChanged

    The latest status push (after requests and successful claims).

UI views you can replace 1

Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.

ICrossplayDailyRewardsView

Narrow contract the presenter drives. UI-only: render calls in, user intents out.