Skip to content

Commit

Permalink
Basic handling of tachyon message to join a queue
Browse files Browse the repository at this point in the history
  • Loading branch information
geekingfrog committed Sep 3, 2024
1 parent 345b55d commit 3c4c8cd
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 9 deletions.
51 changes: 48 additions & 3 deletions lib/teiserver/player/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ defmodule Teiserver.Player.Session do
require Logger

alias Teiserver.Data.Types, as: T
alias Teiserver.Player
alias Teiserver.{Player, Matchmaking}

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

@type matchmaking_state :: %{
joined_queues: [Matchmaking.queue_id()]
}

@type state :: %{
user_id: T.userid(),
mon_ref: reference(),
conn_pid: pid() | nil
conn_pid: pid() | nil,
matchmaking: matchmaking_state()
}

@spec conn_state(T.userid()) :: conn_state()
Expand All @@ -29,6 +34,11 @@ defmodule Teiserver.Player.Session do
:disconnected
end

@spec join_queues(T.userid(), [Matchmaking.queue_id()]) :: Matchmaking.join_result()
def join_queues(user_id, queue_ids) do
GenServer.call(via_tuple(user_id), {:join_queues, queue_ids})
end

def start_link({_conn_pid, user_id} = arg) do
GenServer.start_link(__MODULE__, arg, name: via_tuple(user_id))
end
Expand All @@ -50,7 +60,10 @@ defmodule Teiserver.Player.Session do
state = %{
user_id: user_id,
mon_ref: ref,
conn_pid: conn_pid
conn_pid: conn_pid,
matchmaking_state: %{
joined_queues: []
}
}

{:ok, state}
Expand All @@ -73,6 +86,38 @@ defmodule Teiserver.Player.Session do
{:reply, result, state}
end

def handle_call({:join_queues, queue_ids}, _from, state) do
if not Enum.empty?(state.matchmaking_state.joined_queues) do
{:reply, {:error, :already_queued}, state}
else
case join_all_queues(state.user_id, queue_ids, []) do
:ok ->
{:reply, :ok, put_in(state.matchmaking_state.joined_queues, queue_ids)}

{:error, err} ->
{:reply, {:error, err}, state}
end
end
end

@spec join_all_queues(T.userid(), [Matchmaking.queue_id()], [Matchmaking.queue_id()]) ::
Matchmaking.join_result()
defp join_all_queues(_user_id, [], _joined), do: :ok

defp join_all_queues(user_id, [to_join | rest], joined) do
case Matchmaking.join_queue(to_join, user_id) do
:ok ->
join_all_queues(user_id, rest, [to_join | joined])

# the `queue` message is all or nothing, so if joining a later queue need
# to leave the queues already joined
{:error, reason} ->
Enum.each(joined, fn qid -> Matchmaking.leave_queue(qid, user_id) end)

{:error, reason}
end
end

@impl true
def handle_info({:DOWN, ref, :process, _, _reason}, state) when ref == state.mon_ref do
{:noreply, %{state | conn_pid: nil}}
Expand Down
27 changes: 27 additions & 0 deletions lib/teiserver/player/tachyon_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ defmodule Teiserver.Player.TachyonHandler do
{:reply, :ok, {:text, resp}, state}
end

def handle_command("matchmaking/queue" = cmd_id, "request", message_id, message, state) do
queue_ids = message["data"]["queues"]

response =
case Player.Session.join_queues(state.user.id, queue_ids) do
:ok ->
Schema.response(cmd_id, message_id)

{:error, reason} ->
reason =
case reason do
:invalid_queue -> :invalid_queue_specified
x -> x
end

%{
type: :response,
status: :failed,
commandId: cmd_id,
messageId: message_id,
reason: reason
}
end

{:push, {:text, Jason.encode!(response)}, state}
end

def handle_command(command_id, _message_type, message_id, _message, state) do
resp =
Schema.error_response(command_id, message_id, :command_unimplemented)
Expand Down
29 changes: 29 additions & 0 deletions priv/tachyon/schema/matchmaking/queue/request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$id": "https://schema.beyondallreason.dev/tachyon/matchmaking/queue/request.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MatchmakingQueueRequest",
"tachyon": {
"source": "user",
"target": "server",
"scopes": ["tachyon.lobby"]
},
"type": "object",
"properties": {
"type": { "const": "request" },
"messageId": { "type": "string" },
"commandId": { "const": "matchmaking/queue" },
"data": {
"title": "MatchmakingQueueRequestData",
"type": "object",
"properties": {
"queues": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
},
"required": ["queues"]
}
},
"required": ["type", "messageId", "commandId", "data"]
}
18 changes: 18 additions & 0 deletions test/support/tachyon.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Teiserver.Support.Tachyon do
alias WebsocketSyncClient, as: WSC
alias Teiserver.OAuthFixtures

def setup_client(_context), do: setup_client()
def setup_client() do
user = Central.Helpers.GeneralTestLib.make_user(%{"data" => %{"roles" => ["Verified"]}})
%{client: client, token: token} = connect(user)
Expand Down Expand Up @@ -63,6 +64,16 @@ defmodule Teiserver.Support.Tachyon do
end
end

@doc """
Cleanly disconnect a client by sending a disconnect message before closing
the connection.
"""
def disconnect(client) do
req = request("system/disconnect")
:ok = WSC.send_message(client, {:text, req |> Jason.encode!()})
WSC.disconnect(client)
end

@doc """
high level function to get the list of matchmaking queues
"""
Expand All @@ -87,6 +98,13 @@ defmodule Teiserver.Support.Tachyon do
resp
end

def join_queues!(client, queue_ids) do
req = request("matchmaking/queue", %{queues: queue_ids})
:ok = WSC.send_message(client, {:text, req |> Jason.encode!()})
{:ok, resp} = recv_response(client)
resp
end

@doc """
Run the given function `f` until `pred` returns true on its result.
Waits `wait` ms between each tries. Raise an error if `pred` returns false
Expand Down
42 changes: 36 additions & 6 deletions test/teiserver_web/tachyon/matchmaking_test.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
defmodule Teiserver.Matchmaking.MatchmakingTest do
use TeiserverWeb.ConnCase
alias Teiserver.Support.Tachyon
alias WebsocketSyncClient, as: WSC

# redefinition because ExUnit < 1.15 doesn't support passing setup
# definition as {Tachyon, :setup_client}
defp setup_client(_context), do: Tachyon.setup_client()

describe "list" do
setup :setup_client
setup {Tachyon, :setup_client}

test "works", %{client: client} do
resp = Tachyon.list_queues!(client)
Expand All @@ -28,4 +23,39 @@ defmodule Teiserver.Matchmaking.MatchmakingTest do
assert MapSet.new(resp["data"]["playlists"]) == expected_playlists
end
end

defp setup_queue(_context) do
alias Teiserver.Matchmaking.QueueServer
id = UUID.uuid4()

{:ok, pid} =
QueueServer.init_state(%{id: id, name: id, team_size: 1, team_count: 2})
|> QueueServer.start_link()

{:ok, queue_id: id, queue_pid: pid}
end

describe "joining queues" do
setup [{Tachyon, :setup_client}, :setup_queue]

test "works", %{client: client, queue_id: queue_id} do
resp = Tachyon.join_queues!(client, [queue_id])
assert %{"status" => "success"} = resp
resp = Tachyon.join_queues!(client, [queue_id])
assert %{"status" => "failed", "reason" => "already_queued"} = resp
end

test "multiple", %{client: client, queue_id: queue_id} do
{:ok, queue_id: other_queue_id, queue_pid: _} = setup_queue(nil)
resp = Tachyon.join_queues!(client, [queue_id, other_queue_id])
assert %{"status" => "success"} = resp
end

test "all or nothing", %{client: client, queue_id: queue_id} do
resp = Tachyon.join_queues!(client, [queue_id, "lolnope that's not a queue"])
assert %{"status" => "failed", "reason" => "invalid_queue_specified"} = resp
resp = Tachyon.join_queues!(client, [queue_id])
assert %{"status" => "success"} = resp
end
end
end

0 comments on commit 3c4c8cd

Please sign in to comment.