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.
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.
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 <strong>Stop</strong> button when you are done watching.
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.
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.
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.
MQ.call("image.list", %{})
And the same for the scripts.
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.
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.
tournament_types = [
{"single elimination", "single elimination"},
{"double elimination", "double elimination"}
]
type_input = Kino.Input.select("Tournament Type", tournament_types)
type = Kino.Input.read(type_input)
SquadStrike.create_tournament(storage, type: type)
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.
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.
SquadStrike.resume(storage)