Current section
Files
Jump to
Current section
Files
src/ow_zone.erl
-module(ow_zone).
-behaviour(gen_server).
% gen_server
-export([start/3, start/4]).
-export([start_link/3, start_link/4]).
-export([start_monitor/3, start_monitor/4]).
-export([call/2, call/3]).
-export([cast/2]).
-export([reply/2]).
-export([stop/1, stop/3]).
-export([
join/3,
part/3,
disconnect/2,
reconnect/2,
rpc/4,
broadcast/2,
send/3
]).
% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
%%=======================================================================
%% Internal State, default configurations
%%=======================================================================
-define(TAG_I(Msg), {'$ow_zone_internal', Msg}).
-record(state, {
cb_mod :: module(),
cb_data :: term(),
zone_data :: zone_data()
}).
% DEFAULT_LERP_MS/DEFAULT_TICK_MS = 4 packets buffered by default
-define(DEFAULT_LERP_MS, 80).
-define(DEFAULT_TICK_MS, 20).
-define(INITIAL_ZONE_DATA, #{
clients => [],
frame => 0,
tick_ms => ?DEFAULT_TICK_MS,
lerp_period => ?DEFAULT_LERP_MS
}).
%%=======================================================================
%% Types
%%=======================================================================
-type server_name() :: gen_server:server_name().
-type server_ref() :: gen_server:server_ref().
-type start_opt() :: gen_server:start_opt().
-type start_ret() ::
{ok, pid()}
| ignore
| {error, term()}.
-type start_mon_ret() ::
{ok, {pid(), reference()}}
| ignore
| {error, term()}.
-type state() :: #state{}.
-type from() :: gen_server:from().
-type zone_msg() :: {atom(), map()}.
-type session_pid() :: pid().
-type ow_zone_call_resp() ::
{noreply, state()}
| {reply, zone_msg(), state()}
| {broadcast, zone_msg(), state()}
| {{send, [session_pid()]}, zone_msg(), state()}.
-type ow_zone_cast_resp() ::
{noreply, state()}
| {broadcast, zone_msg(), state()}
| {{send, [session_pid()]}, zone_msg(), state()}.
-type zone_data() ::
#{
clients => [session_pid()],
frame => non_neg_integer(),
tick_ms => pos_integer(),
lerp_period => pos_integer()
}.
%%=======================================================================
%% ow_zone callbacks
%%=======================================================================
-callback init(Args) -> Result when
Args :: term(),
Result ::
{ok, InitialData}
| {ok, InitialData, ConfigMap}
| ignore
| {stop, Reason},
ConfigMap :: zone_data(),
InitialData :: term(),
Reason :: term().
-callback handle_join(Msg, From, ZoneData, State) -> Result when
From :: session_pid(),
Msg :: term(),
ZoneData :: zone_data(),
State :: term(),
Result :: ow_zone_call_resp().
-callback handle_part(Msg, From, ZoneData, State) -> Result when
From :: session_pid(),
Msg :: term(),
ZoneData :: zone_data(),
State :: term(),
Result :: ow_zone_call_resp().
-callback handle_reconnect(From, ZoneData, State) -> Result when
From :: session_pid(),
ZoneData :: zone_data(),
State :: term(),
Result :: ow_zone_call_resp().
-callback handle_disconnect(From, ZoneData, State) -> Result when
From :: session_pid(),
ZoneData :: zone_data(),
State :: term(),
Result :: ow_zone_cast_resp().
-callback handle_tick(ZoneData, State) -> Result when
ZoneData :: zone_data(),
State :: term(),
Result :: ow_zone_cast_resp().
-callback handle_info(Msg, State) -> Result when
Msg :: term(),
State :: term(),
Result :: ow_zone_cast_resp().
-optional_callbacks([
handle_join/4,
handle_part/4,
handle_disconnect/3,
handle_reconnect/3,
handle_info/2
]).
-hank([{unused_callbacks, [all]}]).
%%=======================================================================
%% gen_server API functions
%%=======================================================================
-spec start(Module, Args, Opts) -> Result when
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_ret().
start(Module, Args, Opts) ->
gen_server:start(?MODULE, {Module, Args}, Opts).
-spec start(ServerName, Module, Args, Opts) -> Result when
ServerName :: server_name(),
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_ret().
start(ServerName, Module, Args, Opts) ->
Resp = gen_server:start(ServerName, ?MODULE, {Module, Args}, Opts),
logger:notice("Zone server started: ~p", [Resp]),
Resp.
-spec start_link(Module, Args, Opts) -> Result when
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_ret().
start_link(Module, Args, Opts) ->
gen_server:start_link(?MODULE, {Module, Args}, Opts).
-spec start_link(ServerName, Module, Args, Opts) -> Result when
ServerName :: server_name(),
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_ret().
start_link(ServerName, Module, Args, Opts) ->
Resp = gen_server:start_link(ServerName, ?MODULE, {Module, Args}, Opts),
logger:notice("Zone server started: ~p", [Resp]),
Resp.
-spec start_monitor(Module, Args, Opts) -> Result when
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_mon_ret().
start_monitor(Module, Args, Opts) ->
gen_server:start_monitor(?MODULE, {Module, Args}, Opts).
-spec start_monitor(ServerName, Module, Args, Opts) -> Result when
ServerName :: server_name(),
Module :: module(),
Args :: term(),
Opts :: [start_opt()],
Result :: start_mon_ret().
start_monitor(ServerName, Module, Args, Opts) ->
gen_server:start_monitor(ServerName, ?MODULE, {Module, Args}, Opts).
-spec call(ServerRef, Message) -> Reply when
ServerRef :: server_ref(),
Message :: term(),
Reply :: term().
call(ServerRef, Msg) ->
gen_server:call(ServerRef, Msg).
-spec call(ServerRef, Message, Timeout) -> Reply when
ServerRef :: server_ref(),
Message :: term(),
Timeout :: timeout(),
Reply :: term().
call(ServerRef, Msg, Timeout) ->
gen_server:call(ServerRef, Msg, Timeout).
-spec cast(ServerRef, Message) -> ok when
ServerRef :: server_ref(),
Message :: term().
cast(ServerRef, Msg) ->
gen_server:cast(ServerRef, Msg).
-spec reply(From, Message) -> ok when
From :: from(),
Message :: term().
reply(From, Reply) ->
gen_server:reply(From, Reply).
-spec stop(ServerRef) -> ok when
ServerRef :: server_ref().
stop(ServerRef) ->
gen_server:stop(ServerRef).
-spec stop(ServerRef, Reason, Timeout) -> ok when
ServerRef :: server_ref(),
Reason :: term(),
Timeout :: timeout().
stop(ServerRef, Reason, Timeout) ->
gen_server:stop(ServerRef, Reason, Timeout).
%%=======================================================================
%% Public API for ow_zone
%%=======================================================================
-spec join(server_ref(), term(), session_pid()) -> ok.
join(ServerRef, Msg, SessionPID) ->
gen_server:call(ServerRef, ?TAG_I({join, Msg, SessionPID})).
-spec part(server_ref(), term(), session_pid()) -> ok.
part(ServerRef, Msg, SessionPID) ->
gen_server:call(ServerRef, ?TAG_I({part, Msg, SessionPID})).
-spec disconnect(server_ref(), session_pid()) -> ok.
disconnect(ServerRef, SessionPID) ->
gen_server:cast(ServerRef, ?TAG_I({disconnect, SessionPID})).
-spec reconnect(server_ref(), session_pid()) -> ok.
reconnect(ServerRef, SessionPID) ->
gen_server:call(ServerRef, ?TAG_I({reconnect, SessionPID})).
-spec rpc(server_ref(), atom(), term(), session_pid()) -> ok.
rpc(ServerRef, Type, Msg, SessionPID) ->
gen_server:call(ServerRef, ?TAG_I({Type, Msg, SessionPID})).
-spec broadcast(server_ref(), term()) -> ok.
broadcast(ServerRef, Msg) ->
gen_server:cast(ServerRef, ?TAG_I({broadcast, Msg})).
-spec send(server_ref(), [session_pid()], term()) -> ok.
send(ServerRef, SessionPIDs, Msg) ->
gen_server:cast(ServerRef, ?TAG_I({send, SessionPIDs, Msg})).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% gen_server callback functions for internal state
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% @doc Initialize the internal state of the zone, with timer
init({CbMod, CbArgs}) ->
init(CbMod, CbMod:init(CbArgs)).
init(CbMod, {ok, CbData}) ->
init(CbMod, {ok, CbData, #{}});
init(CbMod, {ok, CbData, ZoneData}) ->
Config = maps:merge(?INITIAL_ZONE_DATA, ZoneData),
St0 = initialize_state(CbMod, CbData, Config),
{ok, St0};
init(_CbMod, ignore) ->
ignore;
init(_CbMod, Stop) ->
Stop.
%%%%%%%%%%%%%%%%%%%%%%%%%%
% 1. Add the player to the player list (THIS IMPLIES VALIDATION IS DONE ELSEWHERE)
% 2. Check if callback is exported
% 3. Run the callback
% 4. Check the output which will be of form {noreply, CbData1} OR {replytype(), msg(), CbData1}
% 4a. If there is no reply, we're done
% 4b. If there is a reply, we need to handle it
handle_call(?TAG_I({join, Msg, Who}), _From, St0) ->
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZD
} = St0,
% Update the sessions ZonePid
ZonePid = self(),
% Could crash with noproc if the player has timed out meanwhile
{ok, ZonePid} = ow_session:zone(ZonePid, Who),
Callback =
case erlang:function_exported(CbMod, disconnect, 1) of
true ->
{CbMod, disconnect, [Who]};
false ->
{CbMod, part, [#{}, Who]}
end,
{ok, Callback} = ow_session:disconnect_callback(Callback, Who),
% Add the session PID to the clients list
#{clients := Clients} = ZD,
ZD1 = ZD#{clients := [Who | Clients]},
St1 = St0#state{zone_data = ZD1},
% Run the callback handler, if exported
maybe
true ?= erlang:function_exported(CbMod, handle_join, 4),
{ReplyType, ReplyMsg, CbData1} ?= CbMod:handle_join(Msg, Who, ZD, CbData0),
CallReply = handle_notify(ReplyType, ReplyMsg, St1),
{reply, CallReply, St1#state{cb_data = CbData1}}
else
false ->
% Handler not exported, noop.
{reply, ok, St1};
{noreply, CbData2} ->
% State internal update, but no reply
{reply, ok, St1#state{cb_data = CbData2}}
% TODO: Placing this here in case it is needed later
%{error, CbData2} ->
% % Server rejects this client join for whatever reason.
% % Roll back state update, and then update CbData
% {reply, ok, St0#state{cb_data = CbData2}}
end;
handle_call(?TAG_I({part, Msg, Who}), _From, St0) ->
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZD
} = St0,
% Remove the client from the clients list
#{clients := Clients} = ZD,
ZD1 = ZD#{clients := lists:delete(Who, Clients)},
St1 = St0#state{zone_data = ZD1},
% Remove the zone from the session
{ok, _} = ow_session:zone(undefined, Who),
% Run the callback handler, if exported
maybe
true ?= is_client(Who, St0),
true ?= erlang:function_exported(CbMod, handle_part, 4),
{ReplyType, ReplyMsg, CbData1} ?= CbMod:handle_part(Msg, Who, ZD, CbData0),
CallMsg = handle_notify(ReplyType, ReplyMsg, St1),
{reply, CallMsg, St1#state{cb_data = CbData1}}
else
false ->
% No update
{reply, ok, St1};
{noreply, CbData2} ->
% State internal update, but no reply
{reply, ok, St1#state{cb_data = CbData2}}
end;
handle_call(?TAG_I({reconnect, Who}), _From, St0) ->
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZD
} = St0,
CbMod = St0#state.cb_mod,
CbData0 = St0#state.cb_data,
% Run the callback handler, if exported
maybe
true ?= is_client(Who, St0),
true ?= erlang:function_exported(CbMod, handle_reconnect, 3),
{ReplyType, ReplyMsg, CbData1} ?= CbMod:handle_reconnect(Who, ZD, CbData0),
CallReply = handle_notify(ReplyType, ReplyMsg, St0),
{reply, CallReply, St0#state{cb_data = CbData1}}
else
false ->
% Handler not exported, noop.
{reply, ok, St0};
{noreply, CbData2} ->
% State internal update, but no reply
{reply, ok, St0#state{cb_data = CbData2}}
end;
handle_call(?TAG_I({Type, Msg, Who}), _From, St0) ->
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZD
} = St0,
Handler = list_to_existing_atom("handle_" ++ atom_to_list(Type)),
maybe
true ?= is_client(Who, St0),
true ?= erlang:function_exported(CbMod, Handler, 4),
{ReplyType, ReplyMsg, CbData1} ?= CbMod:Handler(Msg, Who, ZD, CbData0),
CallMsg = handle_notify(ReplyType, ReplyMsg, St0),
% Replies other than noreply will probably not be sent anywhere useful
{reply, CallMsg, St0#state{cb_data = CbData1}}
else
false ->
% Handler not exported, noop.
{reply, ok, St0};
{noreply, CbData2} ->
% State internal update, but no reply
{reply, ok, St0#state{cb_data = CbData2}}
end;
handle_call(Call, _From, St0) ->
%TODO : Allow fall-through ?
logger:debug(
"Zone was called with a message it does not understand: ~p", [Call]
),
{reply, ok, St0}.
handle_cast(?TAG_I({disconnect, Who}), St0) ->
logger:notice("Received disconnect from session ~p. My state: ~p", [Who, St0]),
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZD
} = St0,
maybe
true ?= erlang:function_exported(CbMod, handle_disconnect, 3),
{ReplyType, ReplyMsg, CbData1} ?= CbMod:handle_disconnect(Who, ZD, CbData0),
ok = handle_notify(ReplyType, ReplyMsg, St0),
{noreply, St0#state{cb_data = CbData1}}
else
false ->
{noreply, St0};
{noreply, CbData2} ->
{noreply, St0#state{cb_data = CbData2}}
end;
handle_cast(?TAG_I({broadcast, Msg}), St0) ->
ok = handle_notify(broadcast, Msg, St0),
{noreply, St0};
handle_cast(?TAG_I({send, IDs, Msg}), St0) ->
ok = handle_notify({send, IDs}, Msg, St0),
{noreply, St0};
handle_cast(Cast, St0) ->
%TODO : Allow fall-through ?
logger:debug(
"Zone was casted a message it does not understand: ~p", [Cast]
),
{noreply, St0}.
handle_info(?TAG_I(tick), St0) ->
#state{
cb_mod = CbMod,
cb_data = CbData0,
zone_data = ZoneData
} = St0,
#{frame := Frame} = ZoneData,
% Increment the frame counter
ZoneData1 = ZoneData#{frame := Frame + 1},
St1 = St0#state{zone_data = ZoneData1},
% Run the callback handler
Result = CbMod:handle_tick(ZoneData1, CbData0),
case Result of
{noreply, CbData1} ->
{noreply, St1#state{cb_data = CbData1}};
{ReplyType, Msg, CbData1} ->
ok = handle_notify(ReplyType, Msg, St1),
{noreply, St1#state{cb_data = CbData1}}
end;
handle_info(Msg, #state{cb_mod = CbMod} = St0) ->
#state{cb_mod = CbMod, cb_data = CbData0} = St0,
maybe
true ?= erlang:function_exported(CbMod, handle_info, 2),
{noreply, CbData1} ?= CbMod:handle_info(Msg, CbData0),
{noreply, St0#state{cb_data = CbData1}}
else
false ->
{noreply, St0};
{reply, _Msg, CbDataN} ->
logger:warning("Dropping 'reply' message sent to info handler in zone ~p", [self()]),
{noreply, St0#state{cb_data = CbDataN}};
{MsgType, Msg1, CbDataN} ->
St1 = St0#state{cb_data = CbDataN},
handle_notify(MsgType, Msg1, St1),
{noreply, St1}
end.
terminate(_Reason, _St0) -> ok.
code_change(_OldVsn, St0, _Extra) -> {ok, St0}.
%%=======================================================================
%% Internal functions
%%=======================================================================
handle_notify({send, IDs}, {MsgType, Msg}, _St0) ->
% Filter down to only the clients specified
ok = ow_session_util:notify_clients(MsgType, Msg, IDs),
ok;
handle_notify(broadcast, {MsgType, Msg}, St0) ->
#{clients := Clients} = St0#state.zone_data,
ok = ow_session_util:notify_clients(MsgType, Msg, Clients),
ok;
handle_notify(reply, {MsgType, Msg}, _St0) ->
{MsgType, Msg}.
initialize_state(CbMod, CbData, ZoneData) ->
#{tick_ms := TickMs} = ZoneData,
% setup the timer
timer:send_interval(TickMs, self(), ?TAG_I(tick)),
#state{
cb_mod = CbMod,
cb_data = CbData,
zone_data = ZoneData
}.
is_client(Who, St0) ->
#{clients := Clients} = St0#state.zone_data,
lists:member(Who, Clients).