GameServer.ReadyChecks (game_server_sdk v1.0.1070)

Copy Markdown View Source

Ready checks: these players must each answer before this proceeds.

One primitive, two kinds — the only differences are what a "no" means and whether an answer can be taken back:

"accept""ready"
Answerone-shot, irrevocablea toggle
A "no"fails the check for everyoneleaves it pending
Deadlinemandatoryoptional
On timeoutfailsfails, naming who stalled

"ready" is the lobby's ready-up and the party's standing ready board; "accept" is matchmaking's match confirmation (see docs/specs/ready-check.md).

Two lanes

A player can be in at most one open check per lane: the match lane (lobby ready or matchmaking accept — one match at a time) and the party lane. The lanes are independent, so a party's standing board never blocks the party's lobby from opening its own check.

What core does not do

A failed check kicks nobody, deletes no lobby and moves no lobby state. Core records who did not answer (not_ready/1); the host — or the game, in after_ready_check_failed — decides what that is worth.

Usage

{:ok, check} = ReadyChecks.open(lobby, member_ids, opened_by: host.id)
{:ok, check} = ReadyChecks.respond(user, true)
ReadyChecks.passed?(lobby)

Concurrency

Answering is a single-row write, so no two players can lose each other's flag. Evaluating the result is the part that races: two players answering at once can each count the other as still pending, and nobody passes. So respond/3 holds a per-check advisory lock (:ready_check) around write-then-evaluate. Hooks and broadcasts fire after the lock is released — never inside the transaction.

Note: This is an SDK stub. Calling these functions will raise an error. The actual implementation runs on the GameServer.

Summary

Functions

Adds a member to the lobby's open check, if there is one.

Adds a member to the party's open check, if there is one.

Answers on behalf of a member — for bots and AI-controlled players, which cannot press anything.

Cancels a pending check — the host called it off, or the subject went away.

Cancels the lobby's pending check, if it has one.

Cancels the party's pending check, if it has one.

Counts checks matching the same filters as list_checks/1.

Fails one check on its deadline. A no-op if it already resolved.

Fails every pending check whose deadline has passed.

The caller's open check, with participants preloaded, or nil.

Fetches a check by id (with participants), or nil.

Lists checks for the admin views, newest first.

The participants who did not answer ready — the host's kick list, and what after_ready_check_failed is handed.

Opens a check over user_ids and notifies them.

True when the subject's most recent check passed.

The lobby's open check, with participants preloaded, or nil.

The party's open check, with participants preloaded, or nil.

Drops a member from the lobby's open check and re-evaluates it.

Drops a member from the party's open check and re-evaluates it.

Resets the subject's board: quietly cancels its pending check (no failed event, no hook — the fresh ready_check_started replaces it on clients) and opens a new one over user_ids.

Records the caller's answer to their open check in scope and re-evaluates it.

Counts by status over the last hours — the accept-rate and dodge-rate numbers on the admin page.

Types

answer()

@type answer() :: boolean()

scope()

@type scope() :: :match | :party

subject()

@type subject() ::
  GameServer.Lobbies.Lobby.t() | GameServer.Parties.Party.t() | :matchmaking

Functions

add_member(lobby_id, user_id)

@spec add_member(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok

Adds a member to the lobby's open check, if there is one.

add_party_member(party_id, user_id)

@spec add_party_member(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok

Adds a member to the party's open check, if there is one.

answer_for(check, user_id, ready?)

@spec answer_for(GameServer.ReadyChecks.Check.t(), Ecto.UUID.t(), answer()) ::
  {:ok, GameServer.ReadyChecks.Check.t()} | {:error, term()}

Answers on behalf of a member — for bots and AI-controlled players, which cannot press anything.

Server-side only: this is in internal_hooks(), so a client cannot reach it over RPC and mark someone else ready.

cancel(check, reason)

@spec cancel(GameServer.ReadyChecks.Check.t(), String.t()) ::
  {:ok, GameServer.ReadyChecks.Check.t()} | {:error, term()}

Cancels a pending check — the host called it off, or the subject went away.

cancel_for_lobby(lobby_id)

@spec cancel_for_lobby(Ecto.UUID.t()) :: :ok

Cancels the lobby's pending check, if it has one.

cancel_for_party(party_id)

@spec cancel_for_party(Ecto.UUID.t()) :: :ok

Cancels the party's pending check, if it has one.

count_checks(opts)

@spec count_checks(keyword()) :: non_neg_integer()

Counts checks matching the same filters as list_checks/1.

expire(check)

@spec expire(GameServer.ReadyChecks.Check.t()) :: :ok | :noop

Fails one check on its deadline. A no-op if it already resolved.

expire_due(now)

@spec expire_due(DateTime.t()) :: non_neg_integer()

Fails every pending check whose deadline has passed.

Each still-unanswered participant becomes timed_out. Returns how many checks were expired. Idempotent, so the durable expiry job and the matchmaking sweep's backstop can both run it.

for_user(user, scope)

@spec for_user(GameServer.Accounts.User.t() | Ecto.UUID.t(), scope() | :any) ::
  GameServer.ReadyChecks.Check.t() | nil

The caller's open check, with participants preloaded, or nil.

scope narrows to one lane: :match (lobby or matchmaking) or :party. :any returns the newest across both lanes — the admin's view, not the API's.

get_check(id)

@spec get_check(Ecto.UUID.t()) :: GameServer.ReadyChecks.Check.t() | nil

Fetches a check by id (with participants), or nil.

list_checks(opts)

@spec list_checks(keyword()) :: [GameServer.ReadyChecks.Check.t()]

Lists checks for the admin views, newest first.

Options: :status, :kind, :lobby_id, :party_id, :page, :page_size.

not_ready(check)

The participants who did not answer ready — the host's kick list, and what after_ready_check_failed is handed.

open(subject, user_ids, opts)

@spec open(subject(), [Ecto.UUID.t()], keyword()) ::
  {:ok, GameServer.ReadyChecks.Check.t()} | {:error, term()}

Opens a check over user_ids and notifies them.

subject is a %Lobby{} or %Party{} (kind "ready") or :matchmaking (kind "accept"). Options:

* `:kind`  override the kind implied by the subject
* `:timeout_ms`  answering window; `nil` leaves a `"ready"` check open
  until it passes or is cancelled. Defaults to `ready_check_timeout_ms`.
* `:opened_by`  the user who asked for it; they are pre-marked ready,
  since clicking the button is their answer
* `:ready`  user ids to pre-mark ready (bots, an auto-ready mode)
* `:tickets`  `%{user_id => ticket_id}` for matchmaking checks
* `:metadata`  game payload echoed to clients (match params, mode)

Fails with {:error, :already_pending} when the subject already has an open check or any player is in one in the same lane, {:error, :no_participants}, and {:error, :too_many_participants} past max_ready_check_participants.

passed?(lobby_id)

@spec passed?(
  GameServer.Lobbies.Lobby.t()
  | GameServer.Parties.Party.t()
  | Ecto.UUID.t()
) :: boolean()

True when the subject's most recent check passed.

What a game calls from before_lobby_state_change to gate its own start. A reset opens a fresh pending check, which makes this false again — so a rematch cannot ride the previous match's pass.

pending_for_lobby(lobby_id)

@spec pending_for_lobby(Ecto.UUID.t()) :: GameServer.ReadyChecks.Check.t() | nil

The lobby's open check, with participants preloaded, or nil.

pending_for_party(party_id)

@spec pending_for_party(Ecto.UUID.t()) :: GameServer.ReadyChecks.Check.t() | nil

The party's open check, with participants preloaded, or nil.

remove_member(lobby_id, user_id)

@spec remove_member(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok

Drops a member from the lobby's open check and re-evaluates it.

Called when someone leaves or is kicked: kicking the one player who never answered is a legitimate way to pass a check.

remove_party_member(party_id, user_id)

@spec remove_party_member(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok

Drops a member from the party's open check and re-evaluates it.

reset(subject, user_ids, opts)

@spec reset(subject(), [Ecto.UUID.t()], keyword()) ::
  {:ok, GameServer.ReadyChecks.Check.t()} | {:error, term()}

Resets the subject's board: quietly cancels its pending check (no failed event, no hook — the fresh ready_check_started replaces it on clients) and opens a new one over user_ids.

The one verb behind every "answers are stale now" moment: a match ended (rematch needs a fresh board), the game mode changed, a member joined a party whose board had already resolved, or the host wants everyone to re-confirm on a deadline ("force ready"). Same options as open/3.

respond(user, ready?, scope)

@spec respond(GameServer.Accounts.User.t() | Ecto.UUID.t(), answer(), scope()) ::
  {:ok, GameServer.ReadyChecks.Check.t()} | {:error, term()}

Records the caller's answer to their open check in scope and re-evaluates it.

scope is :match (the lobby ready-up or matchmaking accept — the default) or :party (the party's standing board): a player can hold one open check in each lane, so the answer needs to say which one it is for.

true is "ready"/"accept"; false is "not ready"/"decline". In an accept check a decline fails the whole check; in a ready check it just leaves the check pending and can be taken back.

Returns the check as it stands after the answer. Fails with {:error, :no_open_check} and, for an accept check the caller already answered, {:error, :not_revocable}.

stats(hours)

@spec stats(pos_integer()) :: %{required(String.t()) => non_neg_integer()}

Counts by status over the last hours — the accept-rate and dodge-rate numbers on the admin page.