%%========================================================================= %% Overworld Protocol %% %% This module handles: %% - Registering/deregistering new opcodes for message types %% - Routing messages to appropriate modules to deserialization %% %%========================================================================= -module(ow_protocol). -behaviour(gen_server). -define(SERVER, ?MODULE). % public API -export([ start/1, stop/0, registered_apps/0, registered_ops/0, register/1, register_app/1, op_info/1, decode/2, response/1, response/2 ]). % gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -include("db/ow_database.hrl"). -include_lib("kernel/include/logger.hrl"). -include_lib("eunit/include/eunit.hrl"). -opaque opcode() :: <<_:16>>. -export_type([opcode/0]). %%========================================================================= %% API %%========================================================================= %%------------------------------------------------------------------------- %% @doc Start the gen_server %% @end %%------------------------------------------------------------------------- -spec start([atom(), ...]) -> {ok, pid()} | ignore | {error, term()}. start(Modules) -> gen_server:start_link({local, ?SERVER}, ?MODULE, [Modules], []). %%------------------------------------------------------------------------- %% @doc Stop the gen_server %% @end %%------------------------------------------------------------------------- -spec stop() -> ok. stop() -> gen_server:stop(?SERVER). %%------------------------------------------------------------------------- %% @doc Register a new Erlang application with Overworld. This is required %% for Overworld to generate a downloadable zips of protobuf, etc %% files. Returns {ok, NewState} on success and {{error, Reason}, %% State}} if the registration fails. %% @end %%------------------------------------------------------------------------- -spec register_app(atom()) -> {reply, ok | {error, atom()}, map()}. register_app(Application) -> logger:info("Registering application: ~p~n", [Application]), gen_server:call(?MODULE, {register_app, Application}). %%------------------------------------------------------------------------- %% @doc Register a new opcode and associated callback function for Module. %% Returns {ok, NewState} on success and {{error, Reason}, State}} if %% the registration fails. %% @end %%------------------------------------------------------------------------- -spec register(atom()) -> {reply, ok | {error, atom()}, map()}. register(Module) -> logger:info("Registering callbacks for : ~p~n", [Module]), gen_server:call(?MODULE, {register_op, Module}). %%------------------------------------------------------------------------- %% @doc Decode messages from clients and route them to an appropriate %% function %% @end %%------------------------------------------------------------------------- -spec decode(binary(), ow_session:session()) -> ok | {ok, ow_session:session()} | {binary(), ow_session:session()}. decode(Message, Session) -> gen_server:call(?MODULE, {decode, Message, Session}). %%------------------------------------------------------------------------- %% @doc Encode a general response %% @end %%------------------------------------------------------------------------- -spec response(ok | error) -> binary(). response(ok) -> overworld_pb:encode_msg(#{status => 'OK'}, gen_response); response(error) -> overworld_pb:encode_msg(#{status => 'ERROR'}, gen_response). %%------------------------------------------------------------------------- %% @doc Encode a general response with reasoning %% @end %%------------------------------------------------------------------------- -spec response(ok | error, string()) -> binary(). response(ok, Msg) -> overworld_pb:encode_msg( #{ status => 'OK', msg => Msg }, gen_response ); response(error, Msg) -> overworld_pb:encode_msg( #{ status => 'ERROR', msg => Msg }, gen_response ). %%------------------------------------------------------------------------- %% @doc Get a list of opcodes registered with the server %% @end %%------------------------------------------------------------------------- -spec registered_ops() -> [pos_integer(), ...]. registered_ops() -> gen_server:call(?MODULE, registered_ops). %%------------------------------------------------------------------------- %% @doc Get a list of Overworld games registered with the server %% @end %%------------------------------------------------------------------------- -spec registered_apps() -> [atom(), ...]. registered_apps() -> gen_server:call(?MODULE, registered_apps). %%------------------------------------------------------------------------- %% @doc Return the MFA for a particular opcode %% @end %%------------------------------------------------------------------------- -spec op_info(pos_integer()) -> ow_rpc:rpc(). op_info(OpCode) -> gen_server:call(?MODULE, {op_info, OpCode}). %%============================================================================ %% gen_server callbacks %%============================================================================ init([Modules]) -> % Register all functions defined in ?INITIAL_MODULES InitialOps = lists:foldl( fun(Item, Map) -> reg(Item, Map) end, #{}, Modules ), InitialApps = [overworld], InitialState = #{ops => InitialOps, apps => InitialApps}, {ok, InitialState}. handle_call({register_op, Module}, _From, St0) -> % Get list of opcodes to register for Module Ops0 = maps:get(ops, St0), Ops1 = reg(Module, Ops0), {reply, ok, St0#{ops => Ops1}}; handle_call({register_app, Application}, _From, St0) -> % Get list of opcodes to register for Module Apps0 = maps:get(apps, St0), Apps1 = reg_app(Application, Apps0), {reply, ok, St0#{apps => Apps1}}; handle_call({decode, Message, Session}, _From, St0) -> Reply = route(Message, Session, St0), {reply, Reply, St0}; handle_call(registered_ops, _From, St0) -> Ops = maps:get(ops, St0), Reply = maps:keys(Ops), {reply, Reply, St0}; handle_call(registered_apps, _From, St0) -> Apps = maps:get(apps, St0), {reply, Apps, St0}; handle_call({op_info, OpCode}, _From, St0) -> Ops = maps:get(ops, St0), Reply = maps:get(OpCode, Ops, undefined), {reply, Reply, St0}. handle_cast(_Request, St0) -> {noreply, St0}. handle_info(_Request, St0) -> {noreply, St0}. terminate(_Reason, _St0) -> ok. code_change(_OldVsn, St0, _Extra) -> {ok, St0}. %%============================================================================ %% Internal functions %%============================================================================ -spec route(<<_:16, _:_*8>>, ow_session:session(), map()) -> ow_session:net_msg(). route(<>, Session, St0) -> Routes = maps:get(ops, St0), case maps:get(OpCode, Routes, unknown) of unknown -> logger:notice("Got an unknown OpCode: ~p", [OpCode]); Map -> case ow_rpc:c2s_handler(Map) of {Module, Fun, 2} -> % Apply the callback with the rest of the message plus % current session information (authentication status, etc). %logger:debug("Calling ~p:~p(...)", [Module, Fun]), erlang:apply(Module, Fun, [Message, Session]); {Module, Fun, 1} -> % The callback is called "sessionless" - e.g. heartbeats, % version request, etc. erlang:apply(Module, Fun, [Message]), % return the session {ok, Session}; undefined -> % There is no 'C' in this RPC. logger:notice("No RPC registered for ~p", [OpCode]), {ok, Session} end end. reg(Module, Ops) -> RPCs = erlang:apply(Module, rpc_info, []), lists:foldl(fun(Item, Map) -> reg_op(Item, Map) end, Ops, RPCs). reg_app(App, AppList) -> case lists:member(App, AppList) of true -> AppList; false -> [App | AppList] end. reg_op(Map, St0) -> % Get the OpCode from the inner map OpCode = maps:get(opcode, Map), case maps:get(OpCode, St0, undefined) of undefined -> maps:put(OpCode, Map, St0); _ -> St0 end.