Packages

Multiplayer game server framework

Current section

Files

Jump to
overworld src ow_session.erl
Raw

src/ow_session.erl

%%=========================================================================
%% Overworld Session
%%
%% This module defines and holds a session's state
%%
%%=========================================================================
-module(ow_session).
-behaviour(gen_statem).
% API
-export([
start/1, start/0,
stop/1,
id/2, id/1,
connect/1,
disconnect/1,
proxy/2, proxy/1,
serializer/2, serializer/1,
latency/2, latency/1,
game_data/2, game_data/1,
disconnect_callback/2, disconnect_callback/1,
token/2, token/1,
zone/2, zone/1
]).
% gen_statem required callbacks
-export([
init/1,
callback_mode/0,
terminate/3
]).
% state functions
-export([
disconnected/3,
connected/3,
active/3
]).
-type serializer() :: undefined | protobuf.
-type id() :: pos_integer().
-type token() :: binary() | undefined.
-type time_ms() :: non_neg_integer().
-type mfargs() :: {atom(), atom(), list()}.
-type proxy_pid() :: pid() | undefined.
-type zone_pid() :: pid() | undefined.
-record(session, {
% internal use only
id :: id(),
proxy :: proxy_pid(),
serializer :: serializer(),
latency :: time_ms(),
game_data :: any(),
disconnect_callback :: mfargs() | undefined,
disconnect_timeout :: time_ms(),
shutdown_delay :: time_ms(),
token :: token(),
zone :: zone_pid()
}).
-export_type([serializer/0]).
-export_type([id/0]).
-define(DEFAULT_DISCONNECT_TIMEOUT, 5000).
% A bit hackish, but gives the zone some time to deal with shutdowns
-define(DEFAULT_SHUTDOWN_DELAY, 1000).
%%===========================================================================
%% API
%%===========================================================================
%%----------------------------------------------------------------------------
%% @doc Start the session server and create a new session with this ID
%% @end
%%----------------------------------------------------------------------------
-spec start() -> {ok, pid()}.
start() ->
start([]).
-spec start([tuple()]) -> {ok, pid()}.
start(Config) ->
gen_statem:start(?MODULE, [Config], []).
%%----------------------------------------------------------------------------
%% @doc Stop the session server
%% @end
%%----------------------------------------------------------------------------
-spec stop(pid()) -> ok.
stop(Pid) ->
gen_statem:stop(Pid).
%%----------------------------------------------------------------------------
%% @doc Set the session ID for this session (network serializable)
%% @end
%%----------------------------------------------------------------------------
-spec id(pid(), id()) -> {ok, id()}.
id(PID, SessionID) ->
gen_statem:call(PID, {set_id, SessionID}).
%%----------------------------------------------------------------------------
%% @doc Get the session id of this session (network serializable)
%% @end
%%----------------------------------------------------------------------------
-spec id(pid()) -> id() | undefined.
id(PID) ->
gen_statem:call(PID, get_id).
%%----------------------------------------------------------------------------
%% @doc Alias for proxy(PID, self())
%% @end
%%----------------------------------------------------------------------------
-spec connect(pid()) -> {ok, proxy_pid()}.
connect(PID) ->
proxy(PID, self()).
%%----------------------------------------------------------------------------
%% @doc Alias for proxy(PID, undefined)
%% @end
%%----------------------------------------------------------------------------
-spec disconnect(pid()) -> {ok, proxy_pid()}.
disconnect(PID) ->
proxy(PID, undefined).
%%----------------------------------------------------------------------------
%% @doc Set the pid of the session's proxy process, including ENet, WebSocket
%% and internal handlers.
%% @end
%%----------------------------------------------------------------------------
-spec proxy(pid(), proxy_pid()) -> {ok, proxy_pid()}.
proxy(PID, ProxyPID) ->
gen_statem:call(PID, {set_proxy, ProxyPID}).
%%----------------------------------------------------------------------------
%% @doc Get the pid of the session's proxy process.
%% @end
%%----------------------------------------------------------------------------
-spec proxy(pid()) -> proxy_pid().
proxy(PID) ->
gen_statem:call(PID, get_proxy).
%%----------------------------------------------------------------------------
%% @doc Set the format for serializing data. If communication happens all
%% within Erlang node(s), then there is no need to set a serializer.
%% @end
%%----------------------------------------------------------------------------
-spec serializer(serializer(), pid()) -> {ok, serializer()}.
serializer(Serializer, PID) ->
gen_statem:call(PID, {set_serializer, Serializer}).
%%----------------------------------------------------------------------------
%% @doc Get the format for serializing data.
%% @end
%%----------------------------------------------------------------------------
-spec serializer(pid()) -> serializer() | undefined.
serializer(PID) ->
gen_statem:call(PID, get_serializer).
%%----------------------------------------------------------------------------
%% @doc Set the session latency
%% @end
%%----------------------------------------------------------------------------
-spec latency(pos_integer(), pid()) -> {ok, pos_integer()}.
latency(Latency, PID) ->
gen_statem:call(PID, {set_latency, Latency}).
%%----------------------------------------------------------------------------
%% @doc Get the session latency
%% @end
%%----------------------------------------------------------------------------
-spec latency(pid()) -> non_neg_integer().
latency(PID) ->
gen_statem:call(PID, get_latency).
%%----------------------------------------------------------------------------
%% @doc Set the game data
%% @end
%%----------------------------------------------------------------------------
-spec game_data(any(), pid()) -> {ok, any()}.
game_data(Data, PID) ->
gen_statem:call(PID, {set_game_data, Data}).
%%----------------------------------------------------------------------------
%% @doc Get the game data
%% @end
%%----------------------------------------------------------------------------
-spec game_data(pid()) -> any().
game_data(PID) ->
gen_statem:call(PID, get_game_info).
%%----------------------------------------------------------------------------
%% @doc Set the termination callback
%% @end
%%----------------------------------------------------------------------------
-spec disconnect_callback(mfargs(), pid()) -> {ok, mfargs()}.
disconnect_callback(Callback, PID) ->
gen_statem:call(PID, {set_disconnect_callback, Callback}).
%%----------------------------------------------------------------------------
%% @doc Get the termination callback
%% @end
%%----------------------------------------------------------------------------
-spec disconnect_callback(pid()) -> undefined | mfargs().
disconnect_callback(PID) ->
gen_statem:call(PID, get_disconnect_callback).
%%----------------------------------------------------------------------------
%% @doc Sets the session token
%% @end
%%----------------------------------------------------------------------------
-spec token(token(), pid()) -> {ok, token()}.
token(Token, PID) ->
gen_statem:call(PID, {set_token, Token}).
%%----------------------------------------------------------------------------
%% @doc Get the session token.
%% @end
%%----------------------------------------------------------------------------
-spec token(pid()) -> token().
token(PID) ->
gen_statem:call(PID, get_token).
%%----------------------------------------------------------------------------
%% @doc Sets the zone pid
%% @end
%%----------------------------------------------------------------------------
-spec zone(zone_pid(), pid()) -> {ok, pid()}.
zone(ZonePID, PID) ->
gen_statem:call(PID, {set_zone, ZonePID}).
%%----------------------------------------------------------------------------
%% @doc Get the zone pid
%% @end
%%----------------------------------------------------------------------------
-spec zone(pid()) -> zone_pid().
zone(PID) ->
gen_statem:call(PID, get_zone).
%%=========================================================================
%% Callback handlers
%%=========================================================================
init([Config]) ->
Session = #session{
id = proplists:get_value(id, Config, erlang:unique_integer([positive])),
proxy = proplists:get_value(proxy, Config, undefined),
serializer = proplists:get_value(serializer, Config, undefined),
latency = proplists:get_value(latency, Config, 0),
game_data = proplists:get_value(game_data, Config, undefined),
disconnect_callback = proplists:get_value(
disconnect_callback, Config, undefined
),
disconnect_timeout = proplists:get_value(
disconnect_timeout, Config, ?DEFAULT_DISCONNECT_TIMEOUT
),
shutdown_delay = proplists:get_value(
shutdown_delay, Config, ?DEFAULT_SHUTDOWN_DELAY
),
token = proplists:get_value(token, Config, undefined),
zone = undefined
},
% Register with gproc
true = gproc:reg({n, l, Session#session.id}, ignored),
% Set a timer to maybe terminate a stale session if it hasn't gone active
% within the default timeout period
case Session#session.proxy of
undefined ->
Timeout = Session#session.disconnect_timeout,
erlang:send_after(Timeout, self(), maybe_terminate),
{ok, disconnected, Session};
_ ->
{ok, connected, Session}
end.
callback_mode() -> state_functions.
disconnected(info, maybe_terminate, _Session) ->
logger:debug("Caught terminate in disconnected state. Stopping ~p.", [self()]),
{stop, normal};
disconnected({call, From}, {set_proxy, ProxyPID}, Session) ->
logger:debug("~p connecting to proxy ~p", [self(), ProxyPID]),
Reply = {reply, From, {ok, ProxyPID}},
{next_state, connected, Session#session{proxy = ProxyPID}, [Reply]};
disconnected({call, From}, {set_zone, ZonePID}, Session) ->
logger:debug("Cannot connect to zone ~p: disconnected", [ZonePID]),
Reply = {reply, From, {error, disconnected}},
{keep_state, Session, [Reply]};
disconnected(EventType, EventContent, Session) ->
handle_event(EventType, EventContent, Session).
connected({call, From}, {set_zone, ZonePID}, Session) ->
Reply = {reply, From, {ok, ZonePID}},
{next_state, active, Session#session{zone = ZonePID}, [Reply]};
connected(EventType, EventContent, Session) ->
handle_event(EventType, EventContent, Session).
active({call, From}, {set_zone, ZonePID}, Session) ->
Reply = {reply, From, {ok, undefined}},
{next_state, connected, Session#session{zone = ZonePID}, [Reply]};
active(EventType, EventContent, Session) ->
handle_event(EventType, EventContent, Session).
% Fall-throughs
handle_event({call, From}, {set_id, SessionID}, Session) ->
Reply = {reply, From, {ok, SessionID}},
{keep_state, Session#session{id = SessionID}, [Reply]};
handle_event({call, From}, get_id, Session) ->
Reply = {reply, From, Session#session.id},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_proxy, undefined}, Session) ->
Reply = {reply, From, {ok, undefined}},
Timeout = Session#session.disconnect_timeout,
erlang:send_after(Timeout, self(), maybe_terminate),
{next_state, disconnected, Session#session{proxy = undefined}, [Reply]};
handle_event({call, From}, {set_proxy, ProxyPID}, Session) ->
Reply = {reply, From, {ok, ProxyPID}},
{keep_state, Session#session{proxy = ProxyPID}, [Reply]};
handle_event({call, From}, get_proxy, Session) ->
Reply = {reply, From, Session#session.proxy},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_serializer, Serializer}, Session) ->
Reply = {reply, From, {ok, Serializer}},
{keep_state, Session#session{serializer = Serializer}, [Reply]};
handle_event({call, From}, get_serializer, Session) ->
Reply = {reply, From, Session#session.serializer},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_latency, Latency}, Session) ->
Reply = {reply, From, {ok, Latency}},
{keep_state, Session#session{latency = Latency}, [Reply]};
handle_event({call, From}, get_latency, Session) ->
Reply = {reply, From, Session#session.latency},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_game_data, GameData}, Session) ->
Reply = {reply, From, {ok, GameData}},
{keep_state, Session#session{game_data = GameData}, [Reply]};
handle_event({call, From}, get_game_data, Session) ->
Reply = {reply, From, Session#session.game_data},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_disconnect_callback, MFArgs}, Session) ->
Reply = {reply, From, {ok, MFArgs}},
{keep_state, Session#session{disconnect_callback = MFArgs}, [Reply]};
handle_event({call, From}, get_disconnect_callback, Session) ->
Reply = {reply, From, Session#session.disconnect_callback},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_token, Token}, Session) ->
Reply = {reply, From, {ok, Token}},
{keep_state, Session#session{token = Token}, [Reply]};
handle_event({call, From}, get_token, Session) ->
Reply = {reply, From, Session#session.token},
{keep_state, Session, [Reply]};
handle_event({call, From}, {set_zone, Zone}, Session) ->
logger:debug("Cannot connect to zone ~p: not connected", [Zone]),
Reply = {reply, From, {error, not_connected}},
{keep_state, Session, [Reply]};
handle_event({call, From}, get_zone, Session) ->
Reply = {reply, From, Session#session.zone},
{keep_state, Session, [Reply]};
handle_event(info, maybe_terminate, Session) ->
{keep_state, Session};
handle_event(_, _, Session) ->
{keep_state, Session}.
terminate(_Reason, _State, _Data) ->
ok.