game_session.erl 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. -module(game_session).
  2. -behaviour(gen_server).
  3. -include_lib("server/include/social_actions.hrl").
  4. -include_lib("server/include/logging.hrl").
  5. -include_lib("server/include/games.hrl").
  6. -include_lib("server/include/classes.hrl").
  7. -include_lib("server/include/requests.hrl").
  8. -include_lib("server/include/settings.hrl").
  9. -include_lib("server/include/log.hrl").
  10. -export([start_link/1]).
  11. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
  12. -export([process_request/3]).
  13. -export([bot_session_attach/2]).
  14. -define(SERVER, ?MODULE).
  15. -record(state, {
  16. user = undefined,
  17. rpc, %% rpc process
  18. rpc_mon, %% rpc monitor referense
  19. games = [],
  20. rels_notif_channel = undefined,
  21. rels_players =[]
  22. }).
  23. -record(participation, {
  24. game_id :: 'GameId'(), %% generated by id_generator
  25. reg_num :: integer(),
  26. rel_module :: atom(),
  27. rel_pid :: pid(), %% relay, which handles communication gameman maps game_id onto pid
  28. tab_module :: atom(),
  29. tab_pid :: pid(),
  30. ref :: any(), %% monitor reference to relay
  31. role = viewer :: atom() %% [viewer, player, ghost]
  32. }).
  33. % TODO: perform start_link on web socket init
  34. start_link(RPC) when is_pid(RPC) ->
  35. gen_server:start_link(?MODULE, [RPC], []).
  36. bot_session_attach(Pid, UserInfo) ->
  37. gen_server:cast(Pid, {bot_session_attach, UserInfo}).
  38. % TODO: in case of game requests from web page handle them here
  39. process_request(Pid, Source, Msg) ->
  40. ?INFO("API from ~p payload ~p pid ~p",[Source,Msg,Pid]),
  41. gen_server:call(Pid, {client_request, Msg}).
  42. init([RPC]) ->
  43. MonRef = erlang:monitor(process, RPC),
  44. {ok, #state{rpc = RPC, rpc_mon = MonRef}}.
  45. handle_call({client_request, Request}, From, State) ->
  46. handle_client_request(Request, From, State);
  47. handle_call(Request, From, State) ->
  48. ?INFO("unrecognized call: ~p", [Request]),
  49. {stop, {unknown_call, From, Request}, State}.
  50. handle_cast({bot_session_attach, UserInfo}, State = #state{user = undefined}) ->
  51. % ?INFO("bot session attach", []),
  52. {noreply, State#state{user = UserInfo}};
  53. handle_cast(Msg, State) ->
  54. ?INFO("session: unrecognized cast: ~p", [Msg]),
  55. {stop, {error, {unknown_cast, Msg}}, State}.
  56. handle_info({relay_event, SubscrId, RelayMsg}, State) ->
  57. handle_relay_message(RelayMsg, SubscrId, State);
  58. handle_info({relay_kick, SubscrId, Reason}, State) ->
  59. ?INFO("Recived a kick notification from the table: ~p", [Reason]),
  60. handle_relay_kick(Reason, SubscrId, State);
  61. handle_info({delivery, ["user_action", Action, Who, Whom], _} = Notification,
  62. #state{rels_players = RelsPlayers,
  63. user = User,
  64. rpc = RPC
  65. } = State) ->
  66. ?INFO("~w:handle_info/2 Delivery: ~p", [?MODULE, Notification]),
  67. UserId = User#'PlayerInfo'.id,
  68. case list_to_binary(Who) of
  69. UserId ->
  70. PlayerId = list_to_binary(Whom),
  71. case lists:member(PlayerId, RelsPlayers) of
  72. true ->
  73. Type = case Action of
  74. "subscribe" -> ?SOCIAL_ACTION_SUBSCRIBE;
  75. "unsubscribe" -> ?SOCIAL_ACTION_UNSUBSCRIBE;
  76. "block" -> ?SOCIAL_ACTION_BLOCK;
  77. "unblock" -> ?SOCIAL_ACTION_UNBLOCK
  78. end,
  79. Msg = #social_action_msg{initiator = UserId,
  80. recipient = PlayerId,
  81. type = Type
  82. },
  83. % TODO: put real db change notification from users:343 module here
  84. % nsx_msg:notify_db_subscription_change
  85. % should be additionaly subscribed in bg feed worker binded to USER_EXCHANGE
  86. ok = send_message_to_player(RPC, Msg);
  87. false ->
  88. do_nothing
  89. end;
  90. _ ->
  91. do_nothing
  92. end,
  93. {noreply, State};
  94. handle_info({'DOWN', MonitorRef, _Type, _Object, _Info} = Msg, State = #state{rpc_mon = MonitorRef}) ->
  95. ?INFO("connection closed, shutting down session:~p", [Msg]),
  96. {stop, normal, State};
  97. handle_info({'DOWN', OtherRef, process, _Object, Info} = _Msg,
  98. #state{games = Games, rpc = RPC} = State) ->
  99. case lists:keyfind(OtherRef, #participation.ref, Games) of
  100. #participation{} ->
  101. ?INFO("The table is down: ~p", [Info]),
  102. ?INFO("Closing the client and sutting down the session.", []),
  103. send_message_to_player(RPC, #disconnect{reason_id = <<"tableDown">>,
  104. reason = <<"The table you are playing on is unexpectedly down.">>}),
  105. {stop, table_down, State};
  106. _ ->
  107. {noreply, State}
  108. end;
  109. handle_info(Info, State) ->
  110. ?INFO("session: unrecognized info: ~p", [Info]),
  111. {noreply, State}.
  112. terminate(Reason, #state{rels_notif_channel = RelsChannel}) ->
  113. ?INFO("terminating session. reason: ~p", [Reason]),
  114. if RelsChannel =/= undefined -> nsm_mq_channel:close(RelsChannel);
  115. true -> do_nothing end,
  116. ok.
  117. code_change(_OldVsn, State, _Extra) ->
  118. {ok, State}.
  119. %%===================================================================
  120. handle_client_request(#session_attach{token = Token}, _From,
  121. #state{user = undefined} = State) ->
  122. ?INFO("checking session token: ~p", [Token]),
  123. case auth_server:get_user_info(Token) of
  124. false ->
  125. ?INFO("failed session attach: ~p", [Token]),
  126. {stop, normal, {error, invalid_token}, State};
  127. UserInfo ->
  128. ?INFO("successfull session attach. Your user info: ~p", [UserInfo]),
  129. {reply, UserInfo, State#state{user = UserInfo}}
  130. end;
  131. handle_client_request(#session_attach_debug{token = Token, id = Id}, _From,
  132. #state{user = undefined} = State) ->
  133. ?INFO("checking debug session token: ~p", [{Token,Id}]),
  134. case {?IS_TEST, auth_server:get_user_info(Token, Id)} of
  135. {_Test, false} ->
  136. ?INFO("... ~p", [{_Test,false}]),
  137. {stop, normal, {error, invalid_token}, State};
  138. {false, true} ->
  139. ?INFO("... ~p", [{false,true}]),
  140. {stop, normal, {error, invalid_token}, State};
  141. {true, UserInfo} ->
  142. ?INFO("... ~p", [{true,UserInfo}]),
  143. {reply, UserInfo, State#state{user = UserInfo}};
  144. {false, UserInfo} ->
  145. ?INFO("... ~p", [{true,UserInfo}]),
  146. {reply, UserInfo, State#state{user = UserInfo}}
  147. end;
  148. handle_client_request(_, _From, #state{user = undefined} = State) ->
  149. ?INFO("unknown session call", []),
  150. {reply, {error, do_session_attach_first}, State};
  151. handle_client_request(#get_game_info{}, _From, State) ->
  152. ?INFO("session get game info", []),
  153. {reply, {error, not_implemented}, State};
  154. handle_client_request(#logout{}, _From, State) ->
  155. ?INFO("client requests #logout{}", []),
  156. {stop, normal, ok, State};
  157. handle_client_request(#get_player_stats{player_id = PlayerId, game_type = Game}, _From,
  158. State) when is_binary(Game) ->
  159. ?INFO("get player stats", []),
  160. GameModule = api_utils:gametype_to_gamemodule(api_utils:gametype_to_atom(Game)),
  161. Res = GameModule:get_player_stats(PlayerId),
  162. {reply, Res, State};
  163. handle_client_request(#chat{chat_id = GameId, message = Msg0}, _From,
  164. #state{user = User, games = Games} = State) ->
  165. ?INFO("chat", []),
  166. Msg = #chat_msg{chat = GameId, content = Msg0,
  167. author_id = User#'PlayerInfo'.id,
  168. author_nick = User#'PlayerInfo'.login
  169. },
  170. Participation = get_relay(GameId, Games),
  171. Res = case Participation of
  172. false ->
  173. {error, chat_not_registered};
  174. #participation{rel_pid = Srv, rel_module = RMod} ->
  175. RMod:publish(Srv, Msg)
  176. end,
  177. {reply, Res, State};
  178. handle_client_request(#social_action_msg{type=Type, initiator=P1, recipient=P2}, _From,
  179. #state{user = User} = State) when User =/= undefined ->
  180. UserIdBin = User#'PlayerInfo'.id,
  181. ?INFO("social action msg from ~p to ~p (casted by ~p)", [P1, P2, UserIdBin]),
  182. UserId = binary_to_list(UserIdBin),
  183. case Type of
  184. ?SOCIAL_ACTION_SUBSCRIBE ->
  185. Subject = binary_to_list(P2),
  186. nsm_users:subscribe_user(UserId, Subject),
  187. {reply, ok, State};
  188. ?SOCIAL_ACTION_UNSUBSCRIBE ->
  189. Subject = binary_to_list(P2),
  190. nsm_users:remove_subscribe(UserId, Subject),
  191. {reply, ok, State};
  192. ?SOCIAL_ACTION_BLOCK ->
  193. Subject = binary_to_list(P2),
  194. nsx_msg:notify(["subscription", "user", UserId, "block_user"], {Subject}),
  195. {reply, ok, State};
  196. ?SOCIAL_ACTION_UNBLOCK ->
  197. Subject = binary_to_list(P2),
  198. nsx_msg:notify(["subscription", "user", UserId, "unblock_user"], {Subject}),
  199. {reply, ok, State};
  200. ?SOCIAL_ACTION_LOVE ->
  201. {reply, ok, State};
  202. ?SOCIAL_ACTION_HAMMER ->
  203. {reply, ok, State};
  204. UnknownAction ->
  205. ?ERROR("Unknown social action msg from ~p to ~p: ~w", [P1,P2, UnknownAction]),
  206. {reply, {error, unknown_action}, State}
  207. end;
  208. handle_client_request(#social_action{} = Msg, _From,
  209. #state{user = User, games = Games} = State) ->
  210. ?INFO("social action", []),
  211. GameId = Msg#social_action.game,
  212. Res = #social_action_msg{type = Msg#social_action.type,
  213. game = GameId,
  214. recipient = Msg#social_action.recipient,
  215. initiator = User#'PlayerInfo'.id},
  216. Participation = get_relay(GameId, Games),
  217. Ans = case Participation of
  218. false ->
  219. {error, chat_not_registered};
  220. #participation{rel_pid = Srv, rel_module=RMod} ->
  221. RMod:publish(Srv, Res)
  222. end,
  223. {reply, Ans, State};
  224. handle_client_request(#subscribe_player_rels{players = Players}, _From,
  225. #state{user = User, rels_notif_channel = RelsChannel,
  226. rels_players = RelsPlayers, rpc = RPC} = State) ->
  227. ?INFO("subscribe player relations notifications: ~p", [Players]),
  228. UserId = User#'PlayerInfo'.id,
  229. UserIdStr = binary_to_list(UserId),
  230. %% Create subscription if we need
  231. NewRelsChannel =
  232. if RelsChannel == undefined ->
  233. {ok, Channel} = nsx_msg:subscribe_for_user_actions(UserIdStr, self()),
  234. Channel;
  235. true ->
  236. RelsChannel
  237. end,
  238. %% Add players relations to which we need to common list
  239. F = fun(PlayerId, Acc) ->
  240. case lists:member(PlayerId, Acc) of
  241. true -> Acc;
  242. false -> [PlayerId | Acc]
  243. end
  244. end,
  245. NewRelsPlayers = lists:foldl(F, RelsPlayers, Players),
  246. %% Notify the client about current state of subscription relations.
  247. %% (Blocking relations state should be "false" at the start)
  248. F2 =
  249. fun(PlayerId) ->
  250. PlayerIdStr = binary_to_list(PlayerId),
  251. Type = case nsm_users:is_user_subscr(UserIdStr, PlayerIdStr) of
  252. true -> ?SOCIAL_ACTION_SUBSCRIBE;
  253. false -> ?SOCIAL_ACTION_UNSUBSCRIBE
  254. end,
  255. Msg = #social_action_msg{initiator = UserId,
  256. recipient = PlayerId,
  257. type = Type},
  258. ok = send_message_to_player(RPC, Msg)
  259. end,
  260. lists:foreach(F2, Players),
  261. NewState = State#state{rels_players = NewRelsPlayers,
  262. rels_notif_channel = NewRelsChannel},
  263. {reply, ok, NewState};
  264. handle_client_request(#unsubscribe_player_rels{players = Players}, _From,
  265. #state{rels_notif_channel = RelsChannel,
  266. rels_players = RelsPlayers
  267. } = State) ->
  268. ?INFO("unsubscribe player relations notifications", []),
  269. %% Remove players from common list
  270. NewRelsPlayers = RelsPlayers -- Players,
  271. %% Remove subscription if we don't need it now
  272. NewRelsChannel =
  273. if NewRelsPlayers == [] -> nsm_mq_channel:close(RelsChannel),
  274. undefined;
  275. true ->
  276. RelsChannel
  277. end,
  278. NewState = State#state{rels_players = NewRelsPlayers,
  279. rels_notif_channel = NewRelsChannel},
  280. {reply, ok, NewState};
  281. handle_client_request(#join_game{game = GameId}, _From,
  282. #state{user = User, rpc = RPC, games = Games} = State) ->
  283. UserId = User#'PlayerInfo'.id,
  284. ?INFO("join game ~p user ~p from ~p", [GameId, UserId,_From]),
  285. case get_relay(GameId, Games) of
  286. #participation{} ->
  287. {reply, {error, already_joined}, State};
  288. false ->
  289. ?INFO("Requesting main relay info...",[]),
  290. case game_manager:get_relay_mod_pid(GameId) of
  291. {FLMod, FLPid} ->
  292. ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]),
  293. case FLMod:reg(FLPid, User) of
  294. {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} ->
  295. ?INFO("join to game relay: ~p",[{RMod, RPid}]),
  296. {ok, _SubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum),
  297. Ref = erlang:monitor(process, RPid),
  298. Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum,
  299. rel_module = RMod, rel_pid = RPid,
  300. tab_module = TMod, tab_pid = TPid, role = player},
  301. Res = #'TableInfo'{}, % FIXME: The client should accept 'ok' responce
  302. {reply, Res, State#state{games = [Part | State#state.games]}};
  303. {error, finished} ->
  304. ?INFO("The game is finished: ~p.",[GameId]),
  305. ok = send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>,
  306. reason = null}),
  307. {reply, {error, finished}, State};
  308. {error, out} ->
  309. ?INFO("Out of the game: ~p.",[GameId]),
  310. ok = send_message_to_player(RPC, #disconnect{reason_id = <<"disconnected">>,
  311. reason = null}),
  312. {reply, {error, out}, State};
  313. {error, not_allowed} ->
  314. ?ERROR("Not allowed to connect: ~p.",[GameId]),
  315. ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>,
  316. reason = null}),
  317. {reply, {error, not_allowed}, State}
  318. end;
  319. undefined ->
  320. ?ERROR("Game not found: ~p.",[GameId]),
  321. ok = send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>,
  322. reason = null}),
  323. {reply, {error, not_exists}, State}
  324. end
  325. end;
  326. handle_client_request(#game_action{game = GameId} = Msg, _From, State) ->
  327. % ?INFO("game action ~p", [{GameId,Msg,_From}]),
  328. Participation = get_relay(GameId, State#state.games),
  329. case Participation of
  330. false ->
  331. {reply, {error, game_not_found}, State};
  332. #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} ->
  333. UId = (State#state.user)#'PlayerInfo'.id,
  334. ?INFO("PLAYER ~p MOVES ~p in GAME ~p",[UId,Msg,GameId]),
  335. {reply, TMod:submit(TPid, RegNum, Msg), State}
  336. end;
  337. handle_client_request(#pause_game{game = GameId, action = Action}, _From, State) ->
  338. % ?INFO("pause game", []),
  339. % UId = (State#state.user)#'PlayerInfo'.id,
  340. Participation = get_relay(GameId, State#state.games),
  341. % ?INFO("ID: ~p, pause game: ~p, my games: ~p", [UId, GameId, State#state.games]),
  342. case Participation of
  343. false ->
  344. ?INFO("A", []),
  345. {reply, {error, game_not_found}, State};
  346. #participation{reg_num = RegNum, tab_pid = TPid, tab_module = TMod} ->
  347. Signal = case Action of
  348. <<"pause">> -> pause_game;
  349. <<"resume">> -> resume_game
  350. end,
  351. Res = TMod:signal(TPid, RegNum, {Signal, self()}),
  352. ?INFO("B. Res: ~p", [Res]),
  353. {reply, Res, State}
  354. end;
  355. handle_client_request(Request, _From, State) ->
  356. ?INFO("unrecognized client request: ~p", [Request]),
  357. {stop, {unknown_client_request, Request}, State}.
  358. %%===================================================================
  359. handle_relay_message(Msg, _SubscrId, #state{rpc = RPC} = State) ->
  360. try send_message_to_player(RPC, Msg) of
  361. ok ->
  362. {noreply, State};
  363. tcp_closed ->
  364. {stop, normal, State};
  365. E -> error_logger:info_msg("ERROR SEND MESSAGE TO PLAYER: ~p",[E]),
  366. {stop, normal, State}
  367. catch
  368. exit:{normal, {gen_server,call, [RPC, {send_message, _}]}} ->
  369. {stop, normal, State};
  370. exit:{noproc, {gen_server,call, [RPC, {send_message, _}]}} ->
  371. {stop, normal, State};
  372. E:R ->
  373. {stop, normal, State}
  374. end.
  375. %%===================================================================
  376. %% The notification from the current table to rejoin to the game
  377. %% because the user for example was moved to another table.
  378. handle_relay_kick({rejoin, GameId}, _SubscrId,
  379. #state{user = User, games = Games, rpc = RPC} = State) ->
  380. ?INFO("Requesting main relay info...",[]),
  381. UserId = User#'PlayerInfo'.id,
  382. case game_manager:get_relay_mod_pid(GameId) of
  383. {FLMod, FLPid} ->
  384. ?INFO("Found the game: ~p. Trying to register...",[{FLMod, FLPid}]),
  385. case FLMod:reg(FLPid, User) of
  386. {ok, {RegNum, {RMod, RPid}, {TMod, TPid}}} ->
  387. ?INFO("join to game relay: ~p",[{RMod, RPid}]),
  388. {ok, _NewSubscrId} = RMod:subscribe(RPid, self(), UserId, RegNum),
  389. Ref = erlang:monitor(process, RPid),
  390. Part = #participation{ref = Ref, game_id = GameId, reg_num = RegNum,
  391. rel_module = RMod, rel_pid = RPid,
  392. tab_module = TMod, tab_pid = TPid, role = player},
  393. NewGames = lists:keyreplace(GameId, #participation.game_id, Games, Part),
  394. {noreply, State#state{games = NewGames}};
  395. {error, finished} ->
  396. ?INFO("The game is finished: ~p.",[GameId]),
  397. send_message_to_player(RPC, #disconnect{reason_id = <<"gameFinished">>,
  398. reason = null}),
  399. {stop, normal, State};
  400. {error, out} ->
  401. ?INFO("Out of the game: ~p.",[GameId]),
  402. send_message_to_player(RPC, #disconnect{reason_id = <<"kicked">>,
  403. reason = null}),
  404. {stop, normal, State};
  405. {error, not_allowed} ->
  406. ?ERROR("Not allowed to connect: ~p.",[GameId]),
  407. send_message_to_player(RPC, #disconnect{reason_id = <<"notAllowed">>,
  408. reason = null}),
  409. {stop, {error, not_allowed_to_join}, State}
  410. end;
  411. undefined ->
  412. ?ERROR("Game not found: ~p.",[GameId]),
  413. send_message_to_player(RPC, #disconnect{reason_id = <<"notExists">>,
  414. reason = null}),
  415. {stop, {error, game_not_found}, State}
  416. end;
  417. handle_relay_kick(Reason, _SubscrId, #state{rpc = RPC} = State) ->
  418. {ReasonId, ReasonText} =
  419. case Reason of
  420. table_closed -> {<<"tableClosed">>, null};
  421. table_down -> {null, <<"The table was closed unexpectedly.">>};
  422. game_over -> {null, <<"The game is over.">>};
  423. _ -> {<<"kicked">>, null}
  424. end,
  425. send_message_to_player(RPC, #disconnect{reason_id = ReasonId, reason = ReasonText}),
  426. {stop, normal, State}.
  427. %%===================================================================
  428. get_relay(GameId, GameList) ->
  429. lists:keyfind(GameId, #participation.game_id, GameList).
  430. % TODO: flush message to web socket process
  431. send_message_to_player(Pid, Message) ->
  432. ?INFO("MESSAGE to ~p ~p",[Pid,Message]),
  433. Pid ! {send_message,Message}, ok.