Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/teiserver/matchmaking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion lib/teiserver/party/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 34 additions & 15 deletions lib/teiserver/player/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 :: %{
Comment thread
burnhamrobertp marked this conversation as resolved.
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
Expand Down Expand Up @@ -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}})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
47 changes: 41 additions & 6 deletions lib/teiserver/player/tachyon_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions lib/teiserver/player/types/mm_pairing_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/teiserver/player/types/mm_searching_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
102 changes: 102 additions & 0 deletions test/teiserver_web/tachyon/matchmaking_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}]

Expand Down
12 changes: 12 additions & 0 deletions test/teiserver_web/tachyon/user_test.exs
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading