lib/a2s/client.ex

defmodule A2S.Client do
  @moduledoc """
  An easy to use client that performs the handshaking and busy-work necessary to make execute A2S queries.

  This client handles requests concurrently and should be suitable for most general uses.

  Note: must supply a :name
  """

  use Supervisor

  ## API

  def start_link(opts) do
    name = a2s_name!(opts)
    port = Keyword.get(opts, :port, 20850)
    idle_timeout = Keyword.get(opts, :idle_timeout, 120_000)
    recv_timeout = Keyword.get(opts, :recv_timeout, 3000)

    config = %{
      port: port,
      idle_timeout: idle_timeout,
      recv_timeout: recv_timeout
    }

    Supervisor.start_link(__MODULE__, config, name: supervisor_name(name))
  end

  def child_spec(opts) do
    %{
      id: a2s_name!(opts),
      start: {__MODULE__, :start_link, [opts]}
    }
  end

  @doc """
  Query a game server running at `address` for the data specified by `query`.
  """
  @spec query(:info | :players | :rules, {:inet.ip_address, :inet.port_number}, timeout) ::
  {:info, A2S.Info.t}
  | {:players | A2S.Players.t}
  | {:rules | A2S.Rules.t}
  | {:error, any}
  def query(query, address, timeout \\ 5000) do
    :gen_statem.call(find_or_start(address), query, timeout)
  end

  ## CALLBACKS

  @impl true
  def init(config) do
    :persistent_term.put({A2S.Statem, :recv_timeout}, config.recv_timeout)
    :persistent_term.put({A2S.Statem, :idle_timeout}, config.idle_timeout)

    children = [
      {Registry, [keys: :unique, name: :a2s_registry]},
      {A2S.DynamicSupervisor, []},
      {A2S.UDP, [port: config.port]}
    ]

    Supervisor.init(children, strategy: :one_for_all)
  end

  ## FUNCTIONS

  defp a2s_name!(opts) do
    Keyword.get(opts, :name) || raise(ArgumentError, "must supply a name")
  end

  defp supervisor_name(name), do: :"#{name}.Supervisor"

  @spec find_or_start({:inet.ip_address, :inet.port_number}) :: pid
  defp find_or_start(address) do
    case Registry.lookup(:a2s_registry, address) do
      [{pid, _value}] -> pid
      [] ->
        case A2S.DynamicSupervisor.start_child(address) do
          {:ok, pid} -> pid
          {:ok, pid, _info} -> pid
          {:error, {:already_started, pid}} -> pid
        end
    end
  end
end