Meta

Draft (Pick/Ban)

Server-authoritative timed ordered selection over opaque option ids: the game supplies the step order (snake/alternating/captains — all just step lists) and the pool; the piece runs the clocks, validates turn + availability, resolves timeouts, and pushes idempotent snapshots.

Server
services.AddCrossplayDraft();
Unity package
com.crossplay.draft
Depends on
core

Services you call 1

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

IDraftService

Server-authoritative timed ordered selection — the pick/ban phase every MOBA runs, the auto- battler carousel, the fantasy-sports draft, the board-game setup auction. The GAME supplies the step order (who picks, in what sequence, with what clock — snake, alternating, captains: all just step lists) and the opaque option pool; the piece runs the clock, validates turn + availability, resolves timeouts, and pushes idempotent snapshots. Keyed by a game-supplied scope id the piece never interprets.

  • DraftContext ContextOf(long scopeId)

    The live context of the scope's draft, or null when none runs. Read it synchronously; never cache it past the current callback.

  • event Action<DraftContext> Ended

    Raised when a draft finishes (all steps resolved, ended early, or abandoned). The context is still fully readable inside the handler — this is where the game consumes the result (build the teams, load the map, start the match).

  • event Action<DraftContext, int> SelectionMade

    Raised after every resolved step: (context, stepIndex — read the selection from Steps). The server-side glue seam.

  • DraftResult TryEnd(long scopeId)

    Ends the draft NOW, leaving unresolved steps skipped (-1) — a lobby collapse, a game event pre-empting the phase.

  • DraftResult TryForceSelect(long scopeId, long optionId)

    Resolves the CURRENT step with optionId regardless of whose turn it is — the trusted game path (a server-driven pick, an admin override, a bot captain).

  • DraftResult TrySelect(ISession picker, long scopeId, long optionId)

    Selects an option for the CURRENT step — the wire path. Only the step's assigned picker is honoured; a failed wire pick also pushes the coded notification.

  • DraftResult TryStart(long scopeId, IReadOnlyList<DraftStepInfo> steps, IReadOnlyList<long> options, IReadOnlyList<ISession> participants, byte[] data = null)

    Starts a draft for scopeId: the game's step order over the opaque option pool, pushed to every participant. Steps run strictly in order; each step's clock starts when it becomes current.

Configuration 1

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

DraftOptions

Configurable draft parameters, passed to AddCrossplayDraft. Defaults live here, never inline in handlers/services (the no-hardcoded-values rule).

  • bool AutoPickOnTimeout { get; set; }

    When true (the default) a step whose clock expires unanswered is auto-resolved with a random still-available option (the MOBA rule — a sleeping captain still fields a full team). When false the step is skipped (its selection records as -1) and the game decides what a hole in the draft means.

  • bool EndWhenEmpty { get; set; }

    When true (the default) a draft whose participants have ALL gone offline is ended automatically (checked on the push cadence) — no leaked timers for abandoned lobbies.

  • bool ExclusiveSelections { get; set; }

    When true (the default) a selected option leaves the pool — every selection is unique across the whole draft (picks AND bans consume). When false the full pool stays selectable at every step (blind-pick modes where both sides may field the same option).

  • int MaxDataBytes { get; set; }

    Maximum size of the opaque draft Data payload, in bytes.

  • int MaxOptions { get; set; }

    Maximum options in a draft's pool; starts past the cap are refused.

  • int MaxSteps { get; set; }

    Maximum steps per draft; starts past the cap are refused.

  • float StatePushIntervalSeconds { get; set; }

    Seconds between periodic DraftState pushes while a draft runs. The interval push keeps countdowns honest and re-syncs reconnectors (the snapshot is idempotent — a returning session is at most one interval away from a complete render).

Wire messages 4

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

5301 DraftState

Server → participants: the FULL draft snapshot — step order, who picks what and when, the remaining pool, the live clock. Pushed on every change and on a fixed interval, and deliberately idempotent: a reconnector is one push away from a complete render, and a missed push heals on the next one. Everything is the game's vocabulary — an option id is a hero in one game, a map in another, a player in a fantasy-sports draft; an action id distinguishes pick from ban (or whatever the game means); the framework never knows.

5302 DraftPickRequest

Client → server: select OptionId for the CURRENT step of the scope's draft. Only the step's assigned picker is honoured; everyone else is refused with NotYourTurn — selections are server-validated, never client-trusted.

5303 DraftPickResponse

Server → client: result of a DraftPickRequest. On failure ReasonCode carries a DraftNotificationCodes value (the same code is also pushed through the Core notification service). The state snapshot that follows a successful pick is the authoritative confirmation.

5304 DraftEnded

Server → participants: the draft finished — every step resolved (picked, auto-picked, or skipped), or the game ended it early. Carries the final step list so clients can render the result without having cached anything.

Unity services you inject 1

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

IDraftClient

Data-layer draft service (no UI): the server's idempotent draft snapshots as events + cached state, and the single request — the async pick. Drafts start server-side only; a snapshot is a complete render (steps, selections, pool, clock), so a reconnector recovers from any single push. Self-registers under the Crossplay client context.

  • DraftState Current

    The most recently pushed snapshot across all scopes, or null (a client is normally in one draft at a time; use StateOf for multi-scope games).

  • DraftState StateOf(long scopeId)

    The last snapshot pushed for a specific scope, or null (also null after its draft ended).

  • event Action<DraftState> StateChanged

    Raised on every snapshot push (step changes + the periodic countdown refresh).

  • event Action<DraftEnded> Ended

    Raised when a draft finishes (its cached snapshot is dropped first).

  • UniTask<DraftPickResponse> PickAsync(long scopeId, long optionId, CancellationToken ct = default)

    Selects an option for the scope's current step (honoured only for the step's picker; the snapshot push that follows is the authoritative confirmation).

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.

ICrossplayDraftView

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