-module(game_session). -behaviour(gen_server). -include_lib("server/include/social_actions.hrl"). -include_lib("server/include/logging.hrl"). -include_lib("server/include/games.hrl"). -include_lib("server/include/classes.hrl"). -include_lib("server/include/requests.hrl"). -include_lib("server/include/settings.hrl"). -include_lib("server/include/log.hrl"). -export([start_link/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([process_request/3]). -export([bot_session_attach/2]). -define(SERVER, ?MODULE). -record(state, { user = undefined, rpc, %% rpc process rpc_mon, %% rpc monitor referense games = [], rels_notif_channel = undefined, rels_players =[] }). -record(participation, { game_id :: 'GameId'(), %% generated by id_generator reg_num :: integer(), rel_module :: atom(), rel_pid :: pid(), %% relay, which handles communication gameman maps game_id onto pid tab_module :: atom(), tab_pid :: pid(), ref :: any(), %% monitor reference to relay role = viewer :: atom() %% [viewer, player, ghost] }). % TODO: perform start_link on web socket init start_link(RPC) when is_pid(RPC) -> gen_server:start_link(?MODULE, [RPC], []). bot_session_attach(Pid, UserInfo) -> gen_server:cast(Pid, {bot_session_attach, UserInfo}). % TODO: in case of game requests from web page handle them here process_request(Pid, Source, Msg) -> ?INFO("API from ~p payload ~p pid ~p",[Source,Msg,Pid]), gen_server:call(Pid, {client_request, Msg}). init([RPC]) -> MonRef = erlang:monitor(process, RPC), {ok, #state{rpc = RPC, rpc_mon = MonRef}}. handle_call({client_request, Request}, From, State) -> handle_client_request(Request, From, State); handle_call(Request, From, State) -> ?INFO("unrecognized call: ~p", [Request]), {stop, {unknown_call, From, Request}, State}. handle_cast({bot_session_attach, UserInfo}, State = #state{user = undefined}) -> % ?INFO("bot session attach", []), {noreply, State#state{user = UserInfo}}; handle_cast(Msg, State) -> ?INFO("session: unrecognized cast: ~p", [Msg]), {stop, {error, {unknown_cast, Msg}}, State}. handle_info({relay_event, SubscrId, RelayMsg}, State) -> handle_relay_message(RelayMsg, SubscrId, State); handle_info({relay_kick, SubscrId, Reason}, State) -> ?INFO("Recived a kick notification from the table: ~p", [Reason]), handle_relay_kick(Reason, SubscrId, State); handle_info({delivery, ["user_action", Action, Who, Whom], _} = Notification, #state{rels_players = RelsPlayers, user = User, rpc = RPC } = State) -> ?INFO("~w:handle_info/2 Delivery: ~p", [?MODULE, Notification]), UserId = User#'PlayerInfo'.id, case list_to_binary(Who) of UserId -> PlayerId = list_to_binary(Whom), case lists:member(PlayerId, RelsPlayers) of true -> Type = case Action of "subscribe" -> ?SOCIAL_ACTION_SUBSCRIBE; "unsubscribe" -> ?SOCIAL_ACTION_UNSUBSCRIBE; "block" -> ?SOCIAL_ACTION_BLOCK; "unblock" -> ?SOCIAL_ACTION_UNBLOCK end, Msg = #social_action_msg{initiator = UserId, recipient = PlayerId, type = Type }, % TODO: put real db change notification from users:343 module here % nsx_msg:notify_db_subscription_change % should be additionaly subscribed in bg feed worker binded to USER_EXCHANGE ok = send_message_to_player(RPC, Msg); false -> do_nothing end; _ -> do_nothing end, {noreply, State}; handle_info({'DOWN', MonitorRef, _Type, _Object, _Info} = Msg, State = #state{rpc_mon = MonitorRef}) -> ?INFO("connection closed, shutting down session:~p", [Msg]), {stop, normal, State}; handle_info({'DOWN', OtherRef, process, _Object, Info} = _Msg, #state{games = Games, rpc = RPC} = State) -> case lists:keyfind(OtherRef, #participation.ref, Games) of #participation{} -> ?INFO("The table is down: ~p", [Info]), ?INFO("Closing the client and sutting down the session.", []), send_message_to_player(RPC, #disconnect{reason_id = <<"tableDown">>, reason = <<"The table you are playing on is unexpectedly down.">>}), {stop, table_down, State}; _ -> {noreply, State} end; handle_info(Info, State) -> ?INFO("session: unrecognized info: ~p", [Info]), {noreply, State}. terminate(Reason, #state{rels_notif_channel = RelsChannel}) -> ?INFO("terminating session. reason: ~p", [Reason]), if RelsChannel =/= undefined -> nsm_mq_channel:close(RelsChannel); true -> do_nothing end, ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%=================================================================== handle_client_request(#session_attach{token = Token}, _From, #state{user = undefined} = State) -> ?INFO("checking session token: ~p", [Token]), case auth_server:get_user_info(Token) of false -> ?INFO("failed session attach: ~p", [Token]), {stop, normal, {error, invalid_token}, State}; UserInfo -> ?INFO("successfull session attach. Your user info: ~p", [UserInfo]), {reply, UserInfo, State#state{user = UserInfo}} end; handle_client_request(#session_attach_debug{token = Token, id = Id}, _From, #state{user = undefined} = State) -> ?INFO("checking debug session token: ~p", [{Token,Id}]), case {?IS_TEST, auth_server:get_user_info(Token, Id)} of {_Test, false} -> ?INFO("... ~p", [{_Test,false}]), {stop, normal, {error, invalid_token}, State}; {false, true} -> ?INFO("... ~p", [{false,true}]), {stop, normal, {error, invalid_token}, State}; {true, UserInfo} -> ?INFO("... ~p", [{true,UserInfo}]), {reply, UserInfo, State#state{user = UserInfo}}; {false, UserInfo} -> ?INFO("... ~p", [{true,UserInfo}]), {reply, UserInfo, State#state{user = UserInfo}} end; handle_client_request(_, _From, #state{user = undefined} = State) -> ?INFO("unknown session call", []), {reply, {error, do_session_attach_first}, State}; handle_client_request(#get_game_info{}, _From, State) -> ?INFO("session get game info", []), {reply, {error, not_implemented}, State}; handle_client_request(#logout{}, _From, State) -> ?INFO("client requests #logout{}", []), {stop, normal, ok, State}; handle_client_request(#get_player_stats{player_id = PlayerId, game_type = Game}, _From, State) when is_binary(Game) -> ?INFO("get player stats", []), GameModule = api_utils:gametype_to_gamemodule(api_utils:gametype_to_atom(Game)), Res = GameModule:get_player_stats(PlayerId), {reply, Res, State}; handle_client_request(#chat{chat_id = GameId, message = Msg0}, _From, #state{user = User, games = Games} = State) -> ?INFO("chat", []), Msg = #chat_msg{chat = GameId, content = Msg0, author_id = User#'PlayerInfo'.id, author_nick = User#'PlayerInfo'.login }, Participation = get_relay(GameId, Games), Res = case Participation of false -> {error, chat_not_registered}; #participation{rel_pid = Srv, rel_module = RMod} -> RMod:publish(Srv, Msg) end, {reply, Res, State}; handle_client_request(#social_action_msg{type=Type, initiator=P1, recipient=P2}, _From, #state{user = User} = State) when User =/= undefined -> UserIdBin = User#'PlayerInfo'.id, ?INFO("social action msg from ~p to ~p (casted by ~p)", [P1, P2, UserIdBin]), UserId = binary_to_list(UserIdBin), case Type of ?SOCIAL_ACTION_SUBSCRIBE -> Subject = binary_to_list(P2), nsm_users:subscribe_user(UserId, Subject), {reply, ok, State}; ?SOCIAL_ACTION_UNSUBSCRIBE -> Subject = binary_to_list(P2), nsm_users:remove_subscribe(UserId, Subject), {reply, ok, State}; ?SOCIAL_ACTION_BLOCK -> Subject = binary_to_list(P2), nsx_msg:notify(["subscription", "user", UserId, "block_user"], {Subject}), {reply, ok, State}; ?SOCIAL_ACTION_UNBLOCK -> Subject = binary_to_list(P2), nsx_msg:notify(["subscription", "user", UserId, "unblock_user"], {Subject}), {reply, ok, State}; ?SOCIAL_ACTION_LOVE -> {reply, ok, State}; ?SOCIAL_ACTION_HAMMER -> {reply, ok, State}; UnknownAction -> ?ERROR("Unknown social action msg from ~p to ~p: ~w", [P1,P2, UnknownAction]), {reply, {error, unknown_action}, State} end; handle_client_request(#social_action{} = Msg, _From, #state{user = User, games = Games} = State) -> ?INFO("social action", []), GameId = Msg#social_action.game, Res = #social_action_msg{type = Msg#social_action.type, game = GameId, recipient = Msg#social_action.recipient, initiator = User#'PlayerInfo'.id}, Participation = get_relay(GameId, Games), Ans = case Participation of false -> {error, chat_not_registered}; #participation{rel_pid = Srv, rel_module=RMod} -> RMod:publish(Srv, Res) end, {reply, Ans, State}; handle_client_request(#subscribe_player_rels{players = Players}, _From, #state{user = User, rels_notif_channel = RelsChannel, rels_players = RelsPlayers, rpc = RPC} = State) -> ?INFO("subscribe player relations notifications: ~p", [Players]), UserId = User#'PlayerInfo'.id, UserIdStr = binary_to_list(UserId), %% Create subscription if we need NewRelsChannel = if RelsChannel == undefined -> {ok, Channel} = nsx_msg:subscribe_for_user_actions(UserIdStr, self()), Channel; true -> RelsChannel end, %% Add players relations to which we need to common list F = fun(PlayerId, Acc) -> case lists:member(PlayerId, Acc) of true -> Acc; false -> [PlayerId | Acc] end end, NewRelsPlayers = lists:foldl(F, RelsPlayers, Players), %% Notify the client about current state of subscription relations. %% (Blocking relations state should be "false" at the start) F2 = fun(PlayerId) -> PlayerIdStr = binary_to_list(PlayerId), Type = case nsm_users:is_user_subscr(UserIdStr, PlayerIdStr) of true -> ?SOCIAL_ACTION_SUBSCRIBE; false -> ?SOCIAL_ACTION_UNSUBSCRIBE end, Msg = #social_action_msg{initiator = UserId, recipient = PlayerId, type = Type}, ok = send_message_to_player(RPC, Msg) end, lists:foreach(F2, Players), NewState = State#state{rels_players = NewRelsPlayers, rels_notif_channel = NewRelsChannel}, {reply, ok, NewState}; handle_client_request(#unsubscribe_player_rels{players = Players}, _From, #state{rels_notif_channel = RelsChannel, rels_players = RelsPlayers } = State) -> ?INFO("unsubscribe player relations notifications", []), %% Remove players from common list NewRelsPlayers = RelsPlayers -- Players, %% Remove subscription if we don't need it now NewRelsChannel = if NewRelsPlayers == [] -> nsm_mq_channel:close(RelsChannel), undefined; true -> RelsChannel end, NewState = State#state{rels_players = NewRelsPlayers, rels_notif_channel = NewRelsChannel}, {reply, ok, NewState}; handle_client_request(#join_game{game = GameId}, _From, #state{user = User, rpc = RPC, games = Games} = State) -> UserId = User#'PlayerInfo'.id, ?INFO("join game ~p user ~p from ~p", [GameId, UserId,_From]), case get_relay(GameId, Games) of #participation{} -> {reply, {error, already_joined}, State}; false -> ?INFO("Requesting main relay info...",[]), case game_manager:get_relay_mod_pid(GameId) of {FLMod, FLPid} -> ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]), case FLMod:reg(FLPid, User) of {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} -> ?INFO("join to game relay: ~p",[{RMod, RPid}]), {ok, _SubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum), Ref = erlang:monitor(process, RPid), Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum, rel_module = RMod, rel_pid = RPid, tab_module = TMod, tab_pid = TPid, role = player}, Res = #'TableInfo'{}, % FIXME: The client should accept 'ok' responce {reply, Res, State#state{games = [Part | State#state.games]}}; {error, finished} -> ?INFO("The game is finished: ~p.",[GameId]), ok = send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>, reason = null}), {reply, {error, finished}, State}; {error, out} -> ?INFO("Out of the game: ~p.",[GameId]), ok = send_message_to_player(RPC, #disconnect{reason_id = <<"disconnected">>, reason = null}), {reply, {error, out}, State}; {error, not_allowed} -> ?ERROR("Not allowed to connect: ~p.",[GameId]), ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>, reason = null}), {reply, {error, not_allowed}, State} end; undefined -> ?ERROR("Game not found: ~p.",[GameId]), ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>, reason = null}), {reply, {error, not_exists}, State} end end; handle_client_request(#game_action{game = GameId} = Msg, _From, State) -> % ?INFO("game action ~p", [{GameId,Msg,_From}]), Participation = get_relay(GameId, State#state.games), case Participation of false -> {reply, {error, game_not_found}, State}; #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} -> UId = (State#state.user)#'PlayerInfo'.id, ?INFO("PLAYER ~p MOVES ~p in GAME ~p",[UId,Msg,GameId]), {reply, TMod:submit(TPid, RegNum, Msg), State} end; handle_client_request(#pause_game{game = GameId, action = Action}, _From, State) -> % ?INFO("pause game", []), % UId = (State#state.user)#'PlayerInfo'.id, Participation = get_relay(GameId, State#state.games), % ?INFO("ID: ~p, pause game: ~p, my games: ~p", [UId, GameId, State#state.games]), case Participation of false -> ?INFO("A", []), {reply, {error, game_not_found}, State}; #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} -> Signal = case Action of <<"pause">> -> pause_game; <<"resume">> -> resume_game end, Res = TMod:signal(TPid, RegNum, {Signal, self()}), ?INFO("B. Res: ~p", [Res]), {reply, Res, State} end; handle_client_request(Request, _From, State) -> ?INFO("unrecognized client request: ~p", [Request]), {stop, {unknown_client_request, Request}, State}. %%=================================================================== handle_relay_message(Msg, _SubscrId, #state{rpc = RPC} = State) -> try send_message_to_player(RPC, Msg) of ok -> {noreply, State}; tcp_closed -> {stop, normal, State}; E -> error_logger:info_msg("ERROR SEND MESSAGE TO PLAYER: ~p",[E]), {stop, normal, State} catch exit:{normal, {gen_server,call, [RPC, {send_message, _}]}} -> {stop, normal, State}; exit:{noproc, {gen_server,call, [RPC, {send_message, _}]}} -> {stop, normal, State}; E:R -> {stop, normal, State} end. %%=================================================================== %% The notification from the current table to rejoin to the game %% because the user for example was moved to another table. handle_relay_kick({rejoin, GameId}, _SubscrId, #state{user = User, games = Games, rpc = RPC} = State) -> ?INFO("Requesting main relay info...",[]), UserId = User#'PlayerInfo'.id, case game_manager:get_relay_mod_pid(GameId) of {FLMod, FLPid} -> ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]), case FLMod:reg(FLPid, User) of {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} -> ?INFO("join to game relay: ~p",[{RMod, RPid}]), {ok, _NewSubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum), Ref = erlang:monitor(process, RPid), Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum, rel_module = RMod, rel_pid = RPid, tab_module = TMod, tab_pid = TPid, role = player}, NewGames = lists:keyreplace(GameId, #participation.game_id, Games, Part), {noreply, State#state{games = NewGames}}; {error, finished} -> ?INFO("The game is finished: ~p.",[GameId]), send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>, reason = null}), {stop, normal, State}; {error, out} -> ?INFO("Out of the game: ~p.",[GameId]), send_message_to_player(RPC, #disconnect{reason_id = <<"kicked">>, reason = null}), {stop, normal, State}; {error, not_allowed} -> ?ERROR("Not allowed to connect: ~p.",[GameId]), send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>, reason = null}), {stop, {error, not_allowed_to_join}, State} end; undefined -> ?ERROR("Game not found: ~p.",[GameId]), send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>, reason = null}), {stop, {error, game_not_found}, State} end; handle_relay_kick(Reason, _SubscrId, #state{rpc = RPC} = State) -> {ReasonId, ReasonText} = case Reason of table_closed -> {<<"tableClosed">>, null}; table_down -> {null, <<"The table was closed unexpectedly.">>}; game_over -> {null, <<"The game is over.">>}; _ -> {<<"kicked">>, null} end, send_message_to_player(RPC, #disconnect{reason_id = ReasonId, reason = ReasonText}), {stop, normal, State}. %%=================================================================== get_relay(GameId, GameList) -> lists:keyfind(GameId, #participation.game_id, GameList). % TODO: flush message to web socket process send_message_to_player(Pid, Message) -> ?INFO("MESSAGE to ~p ~p",[Pid,Message]), Pid ! {send_message,Message}, ok.