%%========================================================================= %% 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/0, stop/0, register/1, apps/0, app_names/0, prefix/1, rpc/2, rpcs/1, route/2, router/1 ]). % gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -include_lib("eunit/include/eunit.hrl"). -define(DEFAULT_ENET_PARAMS, #{qos => reliable, channel => 0}). %%========================================================================= %% API %%========================================================================= %%------------------------------------------------------------------------- %% @doc Start the gen_server %% @end %%------------------------------------------------------------------------- -spec start() -> gen_server:start_ret(). start() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%------------------------------------------------------------------------- %% @doc Stop the gen_server %% @end %%------------------------------------------------------------------------- -spec stop() -> ok. stop() -> gen_server:stop(?SERVER). %%%------------------------------------------------------------------------- %%% @doc Register a new game 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(map()) -> {reply, ok | {error, atom()}, map()}. register(App) -> logger:notice("Got request to register: ~p", [App]), gen_server:call(?MODULE, {register, App}). %%------------------------------------------------------------------------- %% @doc Get all apps registered with the server, including prefix and %% decoder module definition %% @end %%------------------------------------------------------------------------- -spec apps() -> [{non_neg_integer(), map()}]. apps() -> gen_server:call(?MODULE, apps). %%------------------------------------------------------------------------- %% @doc Get a list of Overworld applications registered with the server %% @end %%------------------------------------------------------------------------- -spec app_names() -> [atom()]. app_names() -> gen_server:call(?MODULE, app_names). %%------------------------------------------------------------------------- %% @doc Get the base-10 prefix for a particular application %% @end %%------------------------------------------------------------------------- -spec prefix(atom()) -> non_neg_integer(). prefix(Name) -> gen_server:call(?MODULE, {prefix, Name}). %%------------------------------------------------------------------------- %% @doc Get a list of all RPCs registered with the server (keys only). %% Type options are: all, client, server %% @end %%------------------------------------------------------------------------- -spec rpcs(all | client | server) -> list(). rpcs(Type) -> gen_server:call(?MODULE, {rpcs, Type}). %%------------------------------------------------------------------------- %% @doc Return the full map for a particular RPC %% Type options are: client, server %% @end %%------------------------------------------------------------------------- -spec rpc(atom(), client | server) -> map(). rpc(RPC, Type) -> gen_server:call(?MODULE, {rpc, RPC, Type}). %%------------------------------------------------------------------------- %% @doc Route a message to the appropriate Overworld application based on %% application prefix %% @end %%------------------------------------------------------------------------- -spec route(<<_:16, _:_*8>>, pid()) -> term(). route(<>, SessionPID) -> % Get the decoder M/F for a given Overworld application case ow_protocol:router(Prefix) of false -> logger:notice("No router for prefix: 0x~.16b", [Prefix]), logger:notice("The rest of the message: ~p", [Msg]), ok; {MsgModule, EncoderLib, Application} -> erlang:apply(MsgModule, decode, [Msg, SessionPID, EncoderLib, Application]) %#{app := Application, router := Router} -> % % Assume this app implements the ow_router behaviour % % Get the EncoderLib and Application % EncoderLib = list_to_existing_atom(atom_to_list(Application) ++ "_pb"), % erlang:apply(Router, decode, [Msg, SessionPID, EncoderLib, Application]) end. %%------------------------------------------------------------------------- %% @doc Return the module and decoder function for a given prefix %% @end %%------------------------------------------------------------------------- -spec router(integer()) -> false | {atom(), atom(), atom()}. router(Prefix) -> gen_server:call(?MODULE, {router, Prefix}). %%============================================================================ %% gen_server callbacks %%============================================================================ init([]) -> % Initialize the state St0 = #{ % client RPCs c_rpc => #{}, % server RPCs s_rpc => #{}, apps => [] }, % Automatically register all apps St1 = auto_register(St0), {ok, St1}. handle_call({register, AppConfig}, _From, St0) -> % Fold over the list of modules to register them logger:notice("State before registration: ~p", [St0]), St1 = register_rpcs(AppConfig, St0), logger:notice("State after registration: ~p", [St1]), % Get the currently registered apps #{apps := Apps0} = St1, Apps1 = reg_app(AppConfig, Apps0), {reply, ok, St1#{apps := Apps1}}; handle_call(apps, _From, #{apps := Apps} = St0) -> {reply, Apps, St0}; handle_call({prefix, PrefixName}, _From, #{apps := Apps} = St0) -> [Prefix] = [P || {P, #{app := App}} <- Apps, App == PrefixName], {reply, Prefix, St0}; handle_call(app_names, _From, #{apps := Apps} = St0) -> Names = [App || {_P, #{app := App}} <- Apps], {reply, Names, St0}; handle_call({rpcs, all}, _From, #{c_rpc := C, s_rpc := S} = St0) -> Reply = maps:keys(C) ++ maps:keys(S), {reply, Reply, St0}; handle_call({rpcs, client}, _From, #{c_rpc := C} = St0) -> Reply = maps:keys(C), {reply, Reply, St0}; handle_call({rpcs, server}, _From, #{s_rpc := S} = St0) -> Reply = maps:keys(S), {reply, Reply, St0}; handle_call({rpc, RPC, client}, _From, #{c_rpc := C} = St0) -> Reply = maps:get(RPC, C), {reply, Reply, St0}; handle_call({rpc, RPC, server}, _From, #{s_rpc := S} = St0) -> Reply = maps:get(RPC, S), {reply, Reply, St0}; handle_call({router, Prefix}, _From, #{apps := Apps} = St0) -> AppMap = case orddict:is_key(Prefix, Apps) of true -> orddict:fetch(Prefix, Apps); false -> false end, #{router := MsgModule, app := Application, encoder := EncoderLib} = AppMap, {reply, {MsgModule, EncoderLib, Application}, 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 reg_rpc(atom(), map()) -> map(). reg_rpc(Module, #{c_rpc := CRPC, s_rpc := SRPC} = St0) -> % Get module info for the module AllAttributes = erlang:apply(Module, module_info, [attributes]), F = fun(Attribute) -> case proplists:lookup(Attribute, AllAttributes) of none -> #{}; {Attribute, Calls} -> M = deep_propmap(Calls), M2 = inject_defaults(M), M3 = inject_module(Module, M2), inject_encoder(Module, M3) end end, ClientMap = F(rpc_client), ServerMap = F(rpc_server), CRPC1 = maps:merge(ClientMap, CRPC), SRPC1 = maps:merge(ServerMap, SRPC), St0#{c_rpc => CRPC1, s_rpc => SRPC1}. -spec reg_app(map(), list()) -> list(). reg_app(#{prefix := Prefix} = App, AppList) -> % Note that this will bump the next available slot up. orddict:store(Prefix, App, AppList); reg_app(App, AppList) -> % Determine the next available prefix Next = case orddict:fetch_keys(AppList) of [] -> 0; Keys -> Max = lists:max(Keys), Max + 1 end, % The prefix is assumed to be 8-bits, weird stuff may happen beyond 255 % apps orddict:store(Next, App, AppList). -spec reg_app_test() -> ok. reg_app_test() -> Apps0 = orddict:new(), Test1 = #{app => test1, router => test1_msg, modules => []}, Apps1 = reg_app(Test1, Apps0), ?assertEqual(true, orddict:is_key(0, Apps1)), % Try adding another Test2 = #{app => test2, router => test2_msg, modules => []}, Apps2 = reg_app(Test2, Apps1), ?assertEqual(true, orddict:is_key(1, Apps2)), ok. -spec reg_app_prefix_test() -> ok. reg_app_prefix_test() -> Apps0 = orddict:new(), Test1 = #{ app => test1, prefix => 10, router => test1_msg, modules => [] }, Apps1 = reg_app(Test1, Apps0), ?assertEqual(true, orddict:is_key(10, Apps1)), % Try adding another to see if it increments properly Test2 = #{app => test2, router => test2_msg, modules => []}, Apps2 = reg_app(Test2, Apps1), ?assertEqual(true, orddict:is_key(11, Apps2)), ok. -spec deep_propmap(list()) -> map(). deep_propmap(PropList) -> deep_propmap(PropList, #{}). deep_propmap([], Map) -> Map; deep_propmap([H | T], Map) -> % Take the first item in the list and convert it to a map Map0 = case H of H when is_atom(H) -> #{H => #{}}; {K, {K1, V1}} -> #{K => deep_propmap([{K1, V1}])}; {K, V} -> #{K => V} end, Map1 = maps:merge(Map0, Map), deep_propmap(T, Map1). -spec inject_module(atom(), map()) -> map(). inject_module(Module, PropMap) -> F = fun(_Key, Val) -> Val#{module => Module} end, maps:map(F, PropMap). -spec inject_module_test() -> ok. inject_module_test() -> Map = setup_propmap_tests(), InjMap = inject_module(ow_test, Map), ExpectedFoo = #{module => ow_test}, ?assertEqual(ExpectedFoo, maps:get(foo, InjMap)), ExpectedBar = #{module => ow_test, qos => reliable}, ?assertEqual(ExpectedBar, maps:get(bar, InjMap)), ok. % TODO: Not clear this is the best place for it, but it's the most functional % place at the moment. -spec inject_defaults(map()) -> map(). inject_defaults(PropMap) -> F = fun(_Key, Val) -> maps:merge(?DEFAULT_ENET_PARAMS, Val) end, maps:map(F, PropMap). -spec setup_propmap_tests() -> map(). setup_propmap_tests() -> PropList = [ foo, {bar, {qos, reliable}}, {baz, { encoder, #{app => test, lib => test_pb, interface => test_msg} }}, {bop, { encoder, #{lib => special_pb} }} ], deep_propmap(PropList). -spec deep_propmap_test() -> ok. deep_propmap_test() -> Map = setup_propmap_tests(), ?assertEqual(#{}, maps:get(foo, Map)), ?assertEqual(#{qos => reliable}, maps:get(bar, Map)), ok. inject_encoder(Module, PropMap) -> Attributes = erlang:apply(Module, module_info, [attributes]), % Try to guess the encoder module based on convention ModuleString = atom_to_list(Module), [Prefix | _Rest] = string:split(ModuleString, "_", trailing), App = list_to_atom(Prefix), % Make the best guess for lib and interface modules EncoderLib = list_to_atom(Prefix ++ "_pb"), MaybeEncoderMod = list_to_atom(Prefix ++ "_msg"), EncoderInterface = case erlang:module_loaded(MaybeEncoderMod) of true -> MaybeEncoderMod; false -> % default ow_msg end, DefaultMap = #{ app => App, lib => EncoderLib, interface => EncoderInterface }, E = case proplists:get_value(rpc_encoder, Attributes) of undefined -> DefaultMap; [Encoder] -> Encoder end, F = fun (_Key, #{encoder := Existing} = Val) -> % Existing encoder found, merge with defaults to fill in any gaps Merged = maps:merge(DefaultMap, Existing), Val#{encoder => Merged}; (_Key, Val) -> Val#{encoder => E} end, maps:map(F, PropMap). -spec inject_encoder_test() -> ok. inject_encoder_test() -> Map = setup_propmap_tests(), % ow_app doesn't have anything defined so it ought to generate defaults EncMap = inject_encoder('ow_app', Map), ExpectedFoo = #{ encoder => #{ app => ow, lib => overworld_pb, interface => ow_msg } }, ?assertEqual(ExpectedFoo, maps:get(foo, EncMap)), ExpectedBaz = #{ encoder => #{ app => test, lib => test_pb, interface => test_msg } }, ?assertEqual(ExpectedBaz, maps:get(baz, EncMap)), ExpectedBop = #{ encoder => #{ app => ow, lib => special_pb, interface => ow_msg } }, ?assertEqual(ExpectedBop, maps:get(bop, EncMap)), ok. -spec register_rpcs(map(), map()) -> map(). register_rpcs(#{modules := Modules}, St0) when is_list(Modules) -> lists:foldl(fun(M, S) -> reg_rpc(M, S) end, St0, Modules); register_rpcs(#{app := App}, St0) -> % Lookup the modules for the application {ok, Modules} = application:get_key(App, modules), % Register any RPCs they might have lists:foldl(fun(M, S) -> reg_rpc(M, S) end, St0, Modules). -spec auto_register(map()) -> map(). auto_register(St0) -> % Get all loaded application modules AllApps = application:loaded_applications(), % For each app, attempt to register modules. % If registration is successful, register the app F = fun({App, _Descr, _Vers}, State0) -> {ok, Modules} = application:get_key(App, modules), % For each module, attempt to register it with Overworld State1 = lists:foldl( fun(M, S) -> reg_rpc(M, S) end, State0, Modules ), if State1 =/= State0 -> % This app must have added some new modules, so register AppConfig = get_overworld_config(App), #{apps := RegApps0} = State1, RegApps1 = reg_app(AppConfig, RegApps0), State1#{apps => RegApps1}; true -> State1 end end, lists:foldl(F, St0, AllApps). -spec get_overworld_config(atom()) -> map(). get_overworld_config(App) -> MaybeRouter = list_to_atom(atom_to_list(App) ++ "_msg"), Router = case erlang:module_loaded(MaybeRouter) of true -> MaybeRouter; false -> ow_msg end, Encoder = list_to_existing_atom(atom_to_list(App) ++ "_pb"), case application:get_env(App, overworld) of undefined -> % No config, deliver default config #{ app => App, router => Router, encoder => Encoder, modules => auto }; {ok, Config} -> Default = #{ app => App, router => Router, encoder => Encoder, modules => auto }, maps:merge(Default, Config) end.