Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us
Notesclub

Squad Strike Runner

notebooks/rabbit_driver.livemd

Squad Strike Runner

Application.put_env(:challonge, :challonge_api_key, System.get_env("LB_CHALLONGE_API_KEY"))

Application.put_env(:amiibo_serialization, :key_retail, System.get_env("LB_KEY_RETAIL"))

Mix.install([
  {:squad_strike, path: Path.join([__DIR__, "rabbit_driver", "squad_strike"])},
  {:jason, "~> 1.4"},
  {:amqp, "~> 3.3"},
  {:kino, "~> 0.10.0"}
])

Setup RabbitMQ

Before Running This Livbook!

You first, need to open the Secrets tab in the sidebar to the left. In there you need to create three secrets, named AMQP_URL, CHALLONGE_API_KEY, and KEY_RETAIL. These will be explained below.

AMQP_URL

You will then put in the URL to connect to your RabbitMQ broker. It will look like this:

amqp://username:password@raspberrypi.local:5672

If you enter it wrong, then you may see an error like this when running the setup cell.

01:12:17.379 [info] Application squad_strike exited: SquadStrike.Application.start(:normal, []) returned an error: shutdown: failed to start child: SquadStrike.MQ
    ** (EXIT) an exception was raised:
        ** (MatchError) no match of right hand side value: {:error, :econnrefused}
            (squad_strike 0.1.0) lib/squad_strike/mq.ex:22: anonymous fn/2 in SquadStrike.MQ.start_link/1
            (elixir 1.14.2) lib/agent/server.ex:8: Agent.Server.init/1
            (stdlib 4.0.1) gen_server.erl:848: :gen_server.init_it/2
            (stdlib 4.0.1) gen_server.erl:811: :gen_server.init_it/6
            (stdlib 4.0.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

The default domain for a Raspberry Pi is raspberrypi.local. However, if you changed it, then you will need to update the AMQP URL as well.

CHALLONGE_API_KEY

From the Challonge website, you can create a Challonge API key. Save it as a secret named CHALLONGE_API_KEY.

KEY_RETAIL

This is the secret key used to decrypt amiibo. Dig around on the Internet long enough and you will find it. Use the single-file version. It must be base64-encoded and set to KEY_RETAIL in the secrets.

💡 This Livebook calls a helper project so you don't need to see all the code that's working in the background. If you want to see it, you will find it at notebooks/rabbit_driver/squad_strike.

The main modules used from the helper project are SquadStrike and SquadStrike.MQ. We’ll alias the latter to just MQ.

You’ll often see MQ.cast and MQ.call. MQ.cast sends a message but doesn’t expect a reply. Similarly, MQ.call will send a message and wait for a response.

alias SquadStrike.MQ, as: MQ
alias SquadStrike.Storage, as: Storage

Next, specify where the tournament info can be found. In the following cell, type in a directory path that contains the entries spreadsheet downloaded from submissionapp.com. There should also be a directory named bins that has all the bin files. All files must be named exactly as downloaded.

Ideally the directory you pick should have only these files. Other files will be written as the automation runs.

dir_input = Kino.Input.text("Squad Strike Directory")
dir = dir_input |> Kino.Input.read() |> Path.expand()

storage = Storage.new(dir)

Logs

The app will send out logs, which you can watch. Once run, this will print logs below this cell. Stopping this cell will stop the logs from appearing.

You must run this cell before starting other cells. If a long-running cell is running, then you won’t be able to run this cell until after it finishes.

💡 This section runs in parallel with other code. That way you can watch the logs while running something else. You can skip over this section if you don't need it.
url = MQ.url()
exchange = MQ.exchange()

# Creating a new connection fixes some problems with stopping the cell.
{:ok, conn} = AMQP.Connection.open(url)
{:ok, channel} = AMQP.Channel.open(conn)
{:ok, %{queue: queue}} = AMQP.Queue.declare(channel, "", exclusive: true, auto_delete: true)

pid =
  spawn(fn ->
    AMQP.Queue.bind(channel, queue, exchange, routing_key: "log.#")
    {:ok, tag} = AMQP.Basic.consume(channel, queue, nil, no_ack: true)

    handler = fn recurse ->
      receive do
        {:basic_cancel, _meta} ->
          :ok

        {:basic_deliver, payload, _meta} ->
          %{"level" => level, "msg" => msg, "timestamp" => time} = Jason.decode!(payload)

          IO.puts("#{time} [#{level}] #{inspect(msg)}")

          recurse.(recurse)

        _ ->
          recurse.(recurse)
      after
        # When pings stop, automatically unsubscribe.
        2000 ->
          AMQP.Queue.unsubscribe(channel, tag)
          :ok
      end
    end

    handler.(handler)
  end)

heart_beat = fn recurse ->
  # Send the log watcher a ping to keep it alive.
  send(pid, :ping)

  # Wait a second so we aren't spamming the process.
  Process.sleep(1000)

  recurse.(recurse)
end

# Run the heartbeat function until the cell is explicitly stopped.
# Stopping the hearbeat messages will cause the queue to be cleaned up.
heart_beat.(heart_beat)

Screenshot

This section has some cells that are useful for debugging your capture card. Or you can use this to get screenshots that you can then modify for use as targets in your automation.

💡 This section runs in parallel with other code. That way you can watch the continuous screen capture while running something else. You can skip over this section if you don't need it.
First, is some basic code to handle requesting and handling the image. ```elixir defmodule Screenshot do def capture(fun, opts \\ []) do {:ok, json, _meta} = MQ.call("image.screenshot", %{timeout_ms: 1000}, opts) bytes = Base.decode64!(json["screenshot"]["bytes"]) fun.(bytes) rescue _error -> nil end def capture_forever(delay, fun) do # Create a queue so we don't make a new one with each request. {:ok, conn} = AMQP.Connection.open(MQ.url()) {:ok, channel} = AMQP.Channel.open(conn) {:ok, %{queue: queue}} = AMQP.Queue.declare(channel, "", auto_delete: true) capture(fun, reply_to: queue) Process.sleep(delay) capture_forever(delay, fun) end end ``` The next cell will capture a single image and display it. It's most useful for when grabbing an image to use as an automation target. If the full image doesn't appear completely, then run the cell again. You will notice the screenshot appears squished. This is because the capture card is using a resolution of 640x480 which is a 4x3 ratio. The Switch normally runs a 16x9 ratio. This is expected and not a problem. ```elixir image = Screenshot.capture(fn bytes -> bytes end) ``` This next cell will continuously capture images every few seconds. You can change the capture rate. It's a number in milliseconds. Or you can use the `:timer.seconds` function. Be sure to click the Stop button when you are done watching. ```elixir frame = Kino.Frame.new() |> Kino.render() Screenshot.capture_forever(:timer.seconds(5), fn bytes -> Kino.Frame.render(frame, bytes) end) ``` ## Squad Strike Setup To run the automation, we first send some target images that we want to look for. They are usually small, unambiguous parts fo the screen. ![](img/ready_to_fight.png) The scripts will reference these images. Typically checking if the image is visible, waiting until it becomes visible, or waiting until it disappears. These images are dependent on the screen resolution of the capture card. If you don't use **640x480** as your resolution, then you must update all the images accordingly. The scripts can also be sent ahead of time, or they can be run on demand. The following cell will send both the images and the scripts so they can be used by name. ```elixir MQ.setup() ``` You can then check that the images were uploaded. Other images may also be present. You will notice the Squad Strike images start with `ss_` to avoid name conflicts with other files. ```elixir MQ.call("image.list", %{}) ``` And the same for the scripts. ```elixir MQ.call("script.list", %{}) ``` ## Challonge Setup This section will set up the tournament in Challonge.
⛔️ Only run this section once! Otherwise, a new tournament will be created, and you might not be able to continue the old tournament.
```elixir with {:ok, _tsv} <- Storage.entries_spreadsheet(storage), {:ok, _bin_dir} <- Storage.bins_dir(storage) do :ok else {:error, reason} -> IO.puts(inspect(reason)) end storage ``` If everything looks good, you can proceed to create the tournament in Challonge. ```elixir tournament_types = [ {"single elimination", "single elimination"}, {"double elimination", "double elimination"} ] type_input = Kino.Input.select("Tournament Type", tournament_types) ``` ```elixir type = Kino.Input.read(type_input) SquadStrike.create_tournament(storage, type: type) ``` ```elixir SquadStrike.add_participants(storage) ```
💡 Before running the next cell, open Challonge and make sure your bracket looks right. You will probably want to randomize the entries.
```elixir SquadStrike.start_tournament(storage) ``` ## Squad Strike Automation Now the real fun starts. You are ready to start the tournament. Stopping this cell will stop the automation. Though if a script is in progress, it will run to completion. The state is saved so you can resume an existing tournament. ```elixir SquadStrike.resume(storage) ```