diff --git a/lib/teiserver/matchmaking.ex b/lib/teiserver/matchmaking.ex index 76257e5f6..64415faeb 100644 --- a/lib/teiserver/matchmaking.ex +++ b/lib/teiserver/matchmaking.ex @@ -11,6 +11,7 @@ defmodule Teiserver.Matchmaking do @type queue :: Matchmaking.QueueServer.queue() @type queue_id :: Matchmaking.QueueServer.id() + @type queue_ref :: {queue_id(), version :: String.t()} @type member :: Member.t() @type join_error :: Matchmaking.QueueServer.join_error() @type join_result :: Matchmaking.QueueServer.join_result() diff --git a/lib/teiserver/party/server.ex b/lib/teiserver/party/server.ex index 3fae2a7eb..e08f3a984 100644 --- a/lib/teiserver/party/server.ex +++ b/lib/teiserver/party/server.ex @@ -136,7 +136,7 @@ defmodule Teiserver.Party.Server do interactions between parties and matchmaking by removing any potential of a party member already being in matchmaking outside the party. """ - @spec join_queues(PT.Data.id(), [{Matchmaking.queue_id(), version :: String.t()}]) :: + @spec join_queues(PT.Data.id(), [Matchmaking.queue_ref()]) :: :ok | {:error, reason :: term()} def join_queues(party_id, queues) do via_tuple(party_id) diff --git a/lib/teiserver/player/session.ex b/lib/teiserver/player/session.ex index 1d99f824a..e86b4e0e9 100644 --- a/lib/teiserver/player/session.ex +++ b/lib/teiserver/player/session.ex @@ -38,6 +38,17 @@ defmodule Teiserver.Player.Session do @type conn_state :: :connected | :reconnecting | :disconnected + @typedoc """ + the parts of the session state that go into a `user/self` event + """ + @type self_state :: %{ + party: PartyTypes.Overview.t() | nil, + invited_to_parties: [PartyTypes.Overview.t()], + current_lobby: TachyonLobby.id() | nil, + current_battle: PT.BattleState.t() | nil, + matchmaking: PT.Data.matchmaking_state() + } + @connection_timeout :timer.seconds(30) def start_link({user_id, arg}) do @@ -140,7 +151,7 @@ defmodule Teiserver.Player.Session do @doc false def trigger_connection_timeout(pid), do: send(pid, :connection_timeout) - @spec join_queues(User.id(), [{Matchmaking.queue_id(), version :: String.t()}]) :: + @spec join_queues(User.id(), [Matchmaking.queue_ref()]) :: :ok | Matchmaking.join_error() def join_queues(user_id, queue_ids) do user_id |> via_tuple() |> GenServer.call({:matchmaking, {:join_queues, queue_ids}}) @@ -321,7 +332,7 @@ defmodule Teiserver.Player.Session do connections. """ @spec replace_connection(pid(), pid()) :: - {:ok, old_conn_pid :: pid() | nil, session_state :: %{party: PT.PartyState.t()}} | :died + {:ok, old_conn_pid :: pid() | nil, self_state()} | :died def replace_connection(sess_pid, new_conn_pid) do GenServer.call(sess_pid, {:replace, new_conn_pid}) catch @@ -422,7 +433,7 @@ defmodule Teiserver.Player.Session do @spec party_notify_join_queues( User.id(), - [{Matchmaking.queue_id(), version :: String.t()}], + [Matchmaking.queue_ref()], PartyTypes.Overview.t() ) :: :ok def party_notify_join_queues(user_id, queues, %PartyTypes.Overview{} = party_state) do @@ -688,13 +699,7 @@ defmodule Teiserver.Player.Session do new_state = %{state | conn_pid: new_conn_pid, monitors: monitors, party: party_state} - # used to create the first user/self event - self_state = %{ - party: current_party, - invited_to_parties: invited_to, - current_lobby: get_in(state.lobby.id), - current_battle: state.battle - } + self_state = build_self_state(state) {:reply, {:ok, original_conn_pid, self_state}, new_state} end @@ -1351,7 +1356,8 @@ defmodule Teiserver.Player.Session do paired_queue: paired_queue, room: room_pid, frozen_queues: other_queues, - readied?: false + readied?: false, + timeout_at: DateTime.utc_now() |> DateTime.shift(microsecond: {timeout_ms * 1000, 6}) }} new_state = @@ -1545,7 +1551,7 @@ defmodule Teiserver.Player.Session do end def handle_cast({:user, {:role_updated, roles}}, %PT.Data{} = state) do - send_to_player!({:user, {:role_updated, roles}}, state) + send_to_player!({:user, {:role_updated, roles, build_self_state(state)}}, state) new_state = %{state | user: %{state.user | roles: roles}} {:noreply, new_state} end @@ -1901,6 +1907,19 @@ defmodule Teiserver.Player.Session do SessionRegistry.via_tuple(user_id) end + @spec build_self_state(PT.Data.t()) :: self_state() + defp build_self_state(%PT.Data{} = state) do + {current_party, invited_to} = get_party_states(state.party) + + %{ + party: current_party, + invited_to_parties: invited_to, + current_lobby: get_in(state.lobby.id), + current_battle: state.battle, + matchmaking: state.matchmaking + } + end + # assume all checks have been done, and make the current player join # the specified queues, modifying the state accordingly and returning it defp join_matchmaking(queues, %PT.Data{} = state) do @@ -1924,10 +1943,10 @@ defmodule Teiserver.Player.Session do User.id(), Party.id() | nil, MC.t(), - [{Matchmaking.queue_id(), version :: String.t()}], - [{Matchmaking.queue_id(), version :: String.t()}] + [Matchmaking.queue_ref()], + [Matchmaking.queue_ref()] ) :: - {:ok, MC.t(), [Matchmaking.queue_id()]} | Matchmaking.join_error() + {:ok, MC.t(), [Matchmaking.queue_ref()]} | Matchmaking.join_error() defp join_all_queues(_user_id, _party_id, monitors, [], joined), do: {:ok, monitors, joined} defp join_all_queues(user_id, party_id, monitors, [to_join | rest], joined) do diff --git a/lib/teiserver/player/tachyon_handler.ex b/lib/teiserver/player/tachyon_handler.ex index 383fb05d9..707c1c1b2 100644 --- a/lib/teiserver/player/tachyon_handler.ex +++ b/lib/teiserver/player/tachyon_handler.ex @@ -212,8 +212,8 @@ defmodule Teiserver.Player.TachyonHandler do {:event, "user/updated", event, state} end - def handle_info({:user, {:role_updated, roles}}, state) do - event = build_user_self_event(%{state.user | roles: roles}, state) + def handle_info({:user, {:role_updated, roles, sess_state}}, state) do + event = build_user_self_event(%{state.user | roles: roles}, sess_state) {:event, "user/self", event, state} end @@ -1052,12 +1052,11 @@ defmodule Teiserver.Player.TachyonHandler do outgoingFriendRequest: outgoing, incomingFriendRequest: incoming, ignoreIds: [], - # TODO: see https://github.com/beyond-all-reason/teiserver/issues/1450 - matchmaking: %{state: :no_matchmaking}, + matchmaking: matchmaking_state_to_tachyon(sess_state.matchmaking), # TODO. For now it's just there so clients don't break on missing # required property clanInvites: [], - currentLobby: sess_state[:current_lobby], + currentLobby: sess_state.current_lobby, roles: roles_to_tachyon(user.roles) } } @@ -1081,7 +1080,16 @@ defmodule Teiserver.Player.TachyonHandler do case SessionSupervisor.start_session(user) do {:ok, session_pid} -> {:ok, _pid} = Registry.register_and_kill_existing(user.id) - {:ok, session_pid, %{party: nil, invited_to_parties: [], current_battle: nil}} + + self_state = %{ + party: nil, + invited_to_parties: [], + current_lobby: nil, + current_battle: nil, + matchmaking: :no_matchmaking + } + + {:ok, session_pid, self_state} {:error, {:already_started, pid}} -> case Session.replace_connection(pid, self()) do @@ -1222,6 +1230,33 @@ defmodule Teiserver.Player.TachyonHandler do } end + defp matchmaking_state_to_tachyon(:no_matchmaking), do: %{state: :no_matchmaking} + + defp matchmaking_state_to_tachyon({:searching, %PT.MmSearchingState{} = searching}) do + %{ + state: :queuing, + queues: Enum.map(searching.joined_queues, &queue_ref_to_tachyon/1) + } + end + + defp matchmaking_state_to_tachyon({:pairing, %PT.MmPairingState{} = pairing}) do + queue = + pairing.paired_queue + |> queue_ref_to_tachyon() + |> Map.merge(%{ + timeoutAt: DateTime.to_unix(pairing.timeout_at, :microsecond), + hasAlreadyReadied: pairing.readied? + }) + + %{ + state: :found, + queue: queue, + otherQueues: Enum.map(pairing.frozen_queues, &queue_ref_to_tachyon/1) + } + end + + defp queue_ref_to_tachyon({queue_id, version}), do: %{id: queue_id, version: version} + def battle_state_to_tachyon(nil), do: nil def battle_state_to_tachyon(%PT.BattleState{} = battle) do diff --git a/lib/teiserver/player/types/mm_pairing_state.ex b/lib/teiserver/player/types/mm_pairing_state.ex index f102079c9..cfdff9a49 100644 --- a/lib/teiserver/player/types/mm_pairing_state.ex +++ b/lib/teiserver/player/types/mm_pairing_state.ex @@ -6,14 +6,15 @@ defmodule Teiserver.Player.Types.MmPairingState do alias Teiserver.Matchmaking - @enforce_keys [:paired_queue, :room, :frozen_queues, :readied?] - defstruct [:paired_queue, :room, :frozen_queues, :readied?] + @enforce_keys [:paired_queue, :room, :frozen_queues, :readied?, :timeout_at] + defstruct [:paired_queue, :room, :frozen_queues, :readied?, :timeout_at] @type t :: %__MODULE__{ - paired_queue: {Matchmaking.queue_id(), version :: String.t()}, + paired_queue: Matchmaking.queue_ref(), room: pid(), # a list of the other queues to rejoin in case the pairing fails - frozen_queues: [{Matchmaking.queue_id(), version :: String.t()}], - readied?: boolean() + frozen_queues: [Matchmaking.queue_ref()], + readied?: boolean(), + timeout_at: DateTime.t() } end diff --git a/lib/teiserver/player/types/mm_searching_state.ex b/lib/teiserver/player/types/mm_searching_state.ex index 024feda66..12d7d4151 100644 --- a/lib/teiserver/player/types/mm_searching_state.ex +++ b/lib/teiserver/player/types/mm_searching_state.ex @@ -9,6 +9,6 @@ defmodule Teiserver.Player.Types.MmSearchingState do defstruct [:joined_queues] @type t :: %__MODULE__{ - joined_queues: nonempty_list(Matchmaking.queue_id()) + joined_queues: nonempty_list(Matchmaking.queue_ref()) } end diff --git a/test/teiserver_web/tachyon/matchmaking_test.exs b/test/teiserver_web/tachyon/matchmaking_test.exs index 54faaf667..8d644ef38 100644 --- a/test/teiserver_web/tachyon/matchmaking_test.exs +++ b/test/teiserver_web/tachyon/matchmaking_test.exs @@ -716,6 +716,108 @@ defmodule Teiserver.Tachyon.MatchmakingTest do end end + describe "state after reconnection" do + setup [:setup_queue, :setup_app] + + defp reconnect_self_matchmaking(token) do + client = Tachyon.connect(token, swallow_first_event: false) + + %{"commandId" => "user/self", "data" => %{"user" => user}} = + Tachyon.recv_message!(client) + + user["matchmaking"] + end + + test "no matchmaking", %{app: app} do + {:ok, %{client: client, token: token}} = setup_user(app) + Tachyon.abrupt_disconnect!(client) + + assert %{"state" => "no_matchmaking"} = reconnect_self_matchmaking(token) + end + + test "queuing", %{app: app, queue_id: queue_id, queue_version: version} do + {:ok, %{client: client, token: token}} = setup_user(app) + + assert %{"status" => "success"} = + Tachyon.join_queues!(client, [%{id: queue_id, version: version}]) + + Tachyon.abrupt_disconnect!(client) + + assert %{"state" => "queuing", "queues" => [%{"id" => ^queue_id, "version" => ^version}]} = + reconnect_self_matchmaking(token) + end + + test "found", %{app: app, queue_id: queue_id, queue_pid: queue_pid, queue_version: version} do + [%{client: client, token: token} | _others] = + join_and_pair(app, %{id: queue_id, version: version}, queue_pid, 2) + + Tachyon.abrupt_disconnect!(client) + + assert %{ + "state" => "found", + "queue" => %{ + "id" => ^queue_id, + "version" => ^version, + "hasAlreadyReadied" => false, + "timeoutAt" => timeout_at + }, + "otherQueues" => [] + } = reconnect_self_matchmaking(token) + + assert timeout_at > DateTime.utc_now() |> DateTime.to_unix(:microsecond) + end + + test "found reports the queues left behind", %{ + app: app, + queue_id: queue_id, + queue_pid: queue_pid, + queue_version: version + } do + {:ok, other} = setup_queue(1) + other_id = other[:queue_id] + other_version = other[:queue_version] + + {:ok, %{client: client, token: token}} = setup_user(app) + {:ok, %{client: other_client}} = setup_user(app) + + assert %{"status" => "success"} = + Tachyon.join_queues!(client, [ + %{id: queue_id, version: version}, + %{id: other_id, version: other_version} + ]) + + assert %{"status" => "success"} = + Tachyon.join_queues!(other_client, [%{id: queue_id, version: version}]) + + send(queue_pid, :tick) + assert {:ok, %{"commandId" => "matchmaking/found"}} = Tachyon.recv_message(client) + + Tachyon.abrupt_disconnect!(client) + + assert %{ + "state" => "found", + "queue" => %{"id" => ^queue_id}, + "otherQueues" => [%{"id" => ^other_id, "version" => ^other_version}] + } = reconnect_self_matchmaking(token) + end + + test "found after readying up", %{ + app: app, + queue_id: queue_id, + queue_pid: queue_pid, + queue_version: version + } do + [%{client: client, token: token} | _others] = + join_and_pair(app, %{id: queue_id, version: version}, queue_pid, 2) + + assert %{"status" => "success"} = Tachyon.matchmaking_ready!(client) + Tachyon.abrupt_disconnect!(client) + + assert %{"state" => "found", "queue" => %{"hasAlreadyReadied" => true}} = + reconnect_self_matchmaking(token) + end + end + describe "join with autohost" do setup [{Tachyon, :setup_client}, :setup_app, :setup_queue, {Tachyon, :setup_autohost}] diff --git a/test/teiserver_web/tachyon/user_test.exs b/test/teiserver_web/tachyon/user_test.exs index 8f3af3d0e..533dd2724 100644 --- a/test/teiserver_web/tachyon/user_test.exs +++ b/test/teiserver_web/tachyon/user_test.exs @@ -1,6 +1,7 @@ defmodule TeiserverWeb.Tachyon.UserTest do alias Teiserver.Account alias Teiserver.Helpers.GeneralTestLib + alias Teiserver.Player alias Teiserver.Support.Tachyon use TeiserverWeb.ConnCase, async: false @@ -55,6 +56,17 @@ defmodule TeiserverWeb.Tachyon.UserTest do assert userdata["username"] == user.name assert userdata["displayName"] == user.name assert userdata["status"] == "menu" + assert userdata["matchmaking"] == %{"state" => "no_matchmaking"} + end + + test "sent when roles are updated", %{user: user, client: client} do + :ok = Player.update_user_roles(user.id, ["Contributor"]) + + assert %{"commandId" => "user/self", "data" => %{"user" => userdata}} = + Tachyon.recv_message!(client) + + assert userdata["roles"] == ["contributor"] + assert userdata["matchmaking"] == %{"state" => "no_matchmaking"} end test "filters out unmappable roles in tachyon messages" do