Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
54 changes: 34 additions & 20 deletions lib/teiserver/player/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ defmodule Teiserver.Player.Session do

@type conn_state :: :connected | :reconnecting | :disconnected

@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 +148,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 @@ -313,7 +321,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 @@ -414,7 +422,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 @@ -669,25 +677,17 @@ defmodule Teiserver.Player.Session do

Logger.info("session reused")

{current_party, invited_to} = get_party_states(state.party)
self_state = build_self_state(state)

party_state =
%PT.PartyState{
version: if(current_party == nil, do: nil, else: current_party.version),
current_party: if(current_party == nil, do: nil, else: current_party.id),
invited_to: Enum.map(invited_to, fn st -> {st.version, st.id} end)
version: if(self_state.party == nil, do: nil, else: self_state.party.version),
current_party: if(self_state.party == nil, do: nil, else: self_state.party.id),
invited_to: Enum.map(self_state.invited_to_parties, fn st -> {st.version, st.id} end)
Comment thread
burnhamrobertp marked this conversation as resolved.
Outdated
}

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
}

{:reply, {:ok, original_conn_pid, self_state}, new_state}
end

Expand Down Expand Up @@ -1350,7 +1350,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.add(timeout_ms, :millisecond)
Comment thread
burnhamrobertp marked this conversation as resolved.
Outdated
}}

new_state =
Expand Down Expand Up @@ -1563,7 +1564,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 @@ -1909,6 +1910,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 @@ -1932,10 +1946,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 @@ -220,8 +220,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 @@ -1058,12 +1058,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 @@ -1087,7 +1086,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 @@ -1228,6 +1236,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
9 changes: 5 additions & 4 deletions lib/teiserver/player/types/mm_pairing_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ defmodule Teiserver.Player.Types.MmPairingState do

alias Teiserver.Matchmaking

@enforce_keys [:paired_queue, :room, :frozen_queues, :readied?]
defstruct [:paired_queue, :room, :frozen_queues, :readied?, battle_password: ""]
@enforce_keys [:paired_queue, :room, :frozen_queues, :readied?, :timeout_at]
defstruct [:paired_queue, :room, :frozen_queues, :readied?, :timeout_at, battle_password: ""]
# TODO: remove the battle password from there. It should only be a battle
# concern, and let the player know about it when joining.

@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()}],
frozen_queues: [Matchmaking.queue_ref()],
readied?: boolean(),
timeout_at: DateTime.t(),
battle_password: String.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