asobi_match_server (asobi v0.84.0)

View Source

Per-match gen_statem driving the configured game module.

Trust boundary (F-32): asobi is single-tenant by design. The configured game module (Mod:join/2, Mod:tick/1, Mod:handle_input/3, phase / vote callbacks, etc.) is trusted code. We do not wrap callbacks in try/catch — a crash in the game module is treated as a bug worth surfacing, not a security boundary to harden against. The match supervisor restarts the match server up to 10 times in 60s (transient + intensity 10) before the entire asobi_match_sup falls over, intentionally taking the lobby with it so an obviously broken game cannot keep churning silently.

In multi-tenant or sandboxed contexts (asobi_lua) callbacks are run through a Lua sandbox that has its own fault-isolation; that wrapper is the place to put callback hardening.

Summary

Functions

broadcast_event(Pid, Event, Payload)

-spec broadcast_event(pid(), atom() | binary(), map()) -> ok.

callback_mode()

-spec callback_mode() -> gen_statem:callback_mode_result().

cancel(Pid)

-spec cancel(pid()) -> ok.

cast_vote(Pid, PlayerId, VoteId, OptionId)

-spec cast_vote(pid(), binary(), binary(), binary()) -> ok | {error, term()}.

finished/3

-spec finished(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

get_info(Pid)

-spec get_info(pid()) -> map().

get_info/2

-spec get_info(pid(), listing) -> map().

handle_input(Pid, PlayerId, Input)

-spec handle_input(pid(), binary(), map()) -> ok.

init(Config)

-spec init(map()) -> {ok, atom(), map()}.

join(Pid, PlayerId)

-spec join(pid(), binary()) -> ok | {error, term()}.

join(Pid, PlayerId, Ctx)

-spec join(pid(), binary(), map()) -> ok | {error, term()}.

Join carrying an opaque join context from the client. asobi does not interpret it; it reaches the game module's join/3 if it exports one.

leave(Pid, PlayerId)

-spec leave(pid(), binary()) -> ok.

listing_info(Info)

-spec listing_info(map()) -> map().

Projection of get_info/1 for callers who are not in the match.

Mirrors asobi_world_server:listing_info/1. get_info/1 carries the full players roster and the server-side listed flag; neither reaches a browsing client. joinable does: a client browsing for a match to join has to be able to tell a match that will take it from one that has closed, and without it the only way to find out is to attempt the join and be refused.

pause(Pid)

-spec pause(pid()) -> ok | {error, term()}.

paused/3

-spec paused(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

reconnect(Pid, PlayerId)

-spec reconnect(pid(), binary()) -> ok | {error, term()}.

resume(Pid)

-spec resume(pid()) -> ok | {error, term()}.

running/3

-spec running(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

set_joinable(Pid, Joinable)

-spec set_joinable(pid(), boolean()) -> ok.

Open or close the match to new joins.

A closed match keeps running and keeps its roster; it answers every further join with match_locked. This is the runtime half of joinability - listed decides whether a match is advertised by asobi_match_lobby, and an unlisted match is still joinable by id, so hiding a match is not closing it.

Asynchronous because the caller is usually the match's own Lua VM (game.match.set_joinable), which runs inside this process. An Erlang game module is in the same position - its callbacks run here too - so it closes its own match with set_joinable(self(), false) from tick/1, join/2 or any other callback.

start_link(Config)

-spec start_link(map()) -> gen_statem:start_ret().

start_vote(Pid, VoteConfig)

-spec start_vote(pid(), map()) -> {ok, pid()} | {error, term()}.

terminate/3

-spec terminate(term(), atom(), map()) -> ok.

use_veto(Pid, PlayerId, VoteId)

-spec use_veto(pid(), binary(), binary()) -> ok | {error, term()}.

waiting/3

-spec waiting(gen_statem:event_type() | enter, term(), map()) -> gen_statem:state_enter_result(atom()).

whereis(MatchId)

-spec whereis(binary()) -> {ok, pid()} | error.