Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matchmaking starts autohost #529

Merged
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
34 changes: 34 additions & 0 deletions lib/teiserver/autohost.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,34 @@ defmodule Teiserver.Autohost do
alias Teiserver.AutohostQueries
alias Teiserver.Repo

@type id :: Teiserver.Autohost.Autohost.id()
@type reg_value :: Registry.reg_value()

@type start_script :: %{
battleId: String.t(),
engineVersion: String.t(),
gameName: String.t(),
mapName: String.t(),
startPosType: :fixed | :random | :ingame | :beforegame,
allyTeams: [ally_team(), ...]
}

@type ally_team :: %{
teams: [team(), ...]
}

@type team :: %{
players: [player()]
}

@type player :: %{
userId: String.t(),
name: String.t(),
password: String.t()
}

@type start_response :: Teiserver.Autohost.TachyonHandler.start_response()

def create_autohost(attrs \\ %{}) do
%Autohost{}
|> Autohost.changeset(attrs)
Expand Down Expand Up @@ -36,4 +62,12 @@ defmodule Teiserver.Autohost do
def lookup_autohost(autohost_id) do
Teiserver.Autohost.Registry.lookup(autohost_id)
end

@spec list() :: [reg_value()]
defdelegate list(), to: Registry

@spec start_matchmaking(Autohost.id(), start_script()) ::
{:ok, start_response()} | {:error, term()}
defdelegate start_matchmaking(autohost_id, start_script),
to: Teiserver.Autohost.TachyonHandler
end
8 changes: 8 additions & 0 deletions lib/teiserver/autohost/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ defmodule Teiserver.Autohost.Registry do
Horde.Registry.update_value(__MODULE__, via_tuple(autohost_id), callback)
end

@doc """
Returns all the currently registered autohosts
"""
@spec list() :: [reg_value()]
def list() do
Horde.Registry.select(__MODULE__, [{{:_, :_, :"$1"}, [], [:"$1"]}])
end

def child_spec(_) do
Supervisor.child_spec(Horde.Registry,
id: __MODULE__,
Expand Down
105 changes: 103 additions & 2 deletions lib/teiserver/autohost/tachyon_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ defmodule Teiserver.Autohost.TachyonHandler do
@behaviour Handler

@type connected_state :: %{max_battles: non_neg_integer(), current_battles: non_neg_integer()}
@type state :: %{autohost: Autohost.t(), state: :handshaking | {:connected, connected_state()}}
@type state :: %{
autohost: Autohost.t(),
state: :handshaking | {:connected, connected_state()},
pending_responses: Handler.pending_responses()
}

@type start_response :: %{
ips: [String.t()],
port: integer()
}

@impl Handler
def connect(conn) do
autohost = conn.assigns[:token].autohost
{:ok, %{autohost: autohost, state: :handshaking}}
{:ok, %{autohost: autohost, state: :handshaking, pending_responses: %{}}}
end

@impl Handler
Expand All @@ -26,6 +35,33 @@ defmodule Teiserver.Autohost.TachyonHandler do
end

@impl Handler
def handle_info({:start_matchmaking, start_script, sender}, state) do
start_matchmaking_message = Schema.request("autohost/start", start_script)
message_id = start_matchmaking_message.messageId
# arbitrary timeout for the autohost to reply
tref = :erlang.send_after(10_000, self(), {:timeout, message_id})

new_state =
Map.update(state, :pending_responses, %{}, fn pendings ->
Map.put(pendings, message_id, {tref, sender})
end)

{:push, {:text, [start_matchmaking_message |> Jason.encode!()]}, new_state}
end

def handle_info({:timeout, msg_id}, state) do
Logger.warning("Timeout detected for autohost #{state.autohost.id}, terminating")

resp =
Schema.event("system/disconnected", %{
reason: :timeout,
details: "timeout for message #{msg_id}"
})
|> Jason.encode!()

{:stop, :normal, 1000, [{:text, resp}], state}
end

def handle_info(_msg, state) do
{:ok, state}
end
Expand Down Expand Up @@ -105,11 +141,76 @@ defmodule Teiserver.Autohost.TachyonHandler do
{:stop, :normal, 1000, [{:text, resp}], state}
end

# Generic handler for when a response arrives too late
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am planning to refactor the transport bits, so that this logic about awaiting for the response and timeout management will be centralised and abstracted out of the Autohost and Player modules.

def handle_command(command_id, "response", message_id, _, state)
when not is_map_key(state.pending_responses, message_id) do
# we do expect autohost to be responsive and well behaved, so log timeouts
Logger.warning(
"Autohost(#{state.autohost.id}) response timeout for command #{command_id}, message #{message_id}"
)

{:ok, state}
end

def handle_command("autohost/start", "response", message_id, message, state)
when is_map_key(state.pending_responses, message_id) do
{{tref, reply_to}, pendings} = Map.pop(state.pending_responses, message_id)
:erlang.cancel_timer(tref)
new_state = %{state | pending_responses: pendings}
notify_autohost_started(reply_to, message)
{:ok, new_state}
end

def handle_command(command_id, _message_type, message_id, _message, state) do
resp =
Schema.error_response(command_id, message_id, :command_unimplemented)
|> Jason.encode!()

{:reply, :ok, {:text, resp}, state}
end

# TODO: there should be some kind of retry here
@spec start_matchmaking(Autohost.id(), Teiserver.Autohost.start_script()) ::
{:ok, start_response()} | {:error, term()}
def start_matchmaking(autohost_id, start_script) do
case Registry.lookup(autohost_id) do
{pid, _} ->
send(pid, {:start_matchmaking, start_script, self()})

# This receive is a bit iffy and may cause problem with controlled shutdown
# Ideally, the same way player work, there would be a GenServer decoupled
# from the actual websocket connection, but for now, this poor's man
# GenServer.call will have to do
receive do
{:start_matchmaking_response, resp} -> resp
after
10_000 -> {:error, :timeout}
end

_ ->
{:error, :no_host_available}
end
end

defp notify_autohost_started(reply_to, %{"status" => "failed", "reason" => reason} = msg) do
msg =
case msg["details"] do
nil -> reason
details -> "#{reason} - #{details}"
end

send(reply_to, {:start_matchmaking_response, {:error, msg}})
end

defp notify_autohost_started(reply_to, %{"status" => "success", "data" => data}) do
send(
reply_to,
{:start_matchmaking_response,
{:ok,
%{
ips: data["ips"],
port: data["port"]
}}}
)
end
end
3 changes: 2 additions & 1 deletion lib/teiserver/matchmaking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Teiserver.Matchmaking do
@type join_result :: Matchmaking.QueueServer.join_result()
@type leave_result :: Matchmaking.QueueServer.leave_result()
@type lost_reason :: Matchmaking.PairingRoom.lost_reason()
@type ready_data :: Matchmaking.PairingRoom.ready_data()

@spec lookup_queue(Matchmaking.QueueServer.id()) :: pid() | nil
def lookup_queue(queue_id) do
Expand Down Expand Up @@ -57,6 +58,6 @@ defmodule Teiserver.Matchmaking do
@doc """
to tell the room that the given player is ready for the match
"""
@spec ready(pid(), T.userid()) :: :ok | {:error, term()}
@spec ready(pid(), ready_data()) :: :ok | {:error, term()}
defdelegate ready(room_pid, user_id), to: Matchmaking.PairingRoom
end
Loading
Loading