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

feat: assignments #27

Closed
wants to merge 3 commits into from
Closed
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
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -35,3 +35,7 @@ helldivers_2-*.tar
npm-debug.log
/assets/node_modules/

# nix stuff
.direnv
.nix-hex
.nix-mix
58 changes: 58 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
description = "Helldiver 2 API";

inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
(system:
let pkgs = nixpkgs.legacyPackages.${system}; in
{
devShell = import ./shell.nix { inherit pkgs; };
}
);
}
35 changes: 35 additions & 0 deletions lib/helldivers_2/models/assignments.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Helldivers2.Models.Assignments do
alias Helldivers2.Models.Assignments.Assignment

@type t :: list(Assignment.t())

@spec download(String.t()) :: {:error, any()} | {:ok, list(Assignment.t())}
def download(war_id) do
base = do_download!(war_id)
{:ok, parse(base)}
end

@spec parse(list(map())) :: list(Assignment.t())
def parse(list) when is_list(list) do
Enum.map(list, &Assignment.parse(&1))
end

@spec do_download!(String.t()) :: map() | no_return()
defp do_download!(war_id) do
response =
[
url: "https://api.live.prod.thehelldiversgame.com/api/v2/Assignment/War/#{war_id}",
retry: :transient
]
|> Req.new()
|> Req.request!()

case response do
%Req.Response{status: 200} ->
response.body

%Req.Response{status: status} ->
raise "API returned an error #{status}"
end
end
end
26 changes: 26 additions & 0 deletions lib/helldivers_2/models/assignments/assignment.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Helldivers2.Models.Assignments.Assignment do
alias Helldivers2.Models.Assignments.Assignment.Setting

@type t :: %__MODULE__{
id32: non_neg_integer(),
progress: list(non_neg_integer()),
expiresIn: non_neg_integer(),
setting: Setting.t()
}

defstruct [
:id32,
:progress,
:expiresIn,
:setting
]

def parse(map) when is_map(map) do
%__MODULE__{
id32: Map.get(map, "id32"),
progress: Map.get(map, "progress"),
expiresIn: Map.get(map, "expiresIn"),
setting: Map.get(map, "setting"),
}
end
end
21 changes: 21 additions & 0 deletions lib/helldivers_2/models/assignments/assignment/reward.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Helldivers2.Models.Assignments.Assignment.Reward do
@type t :: %__MODULE__{
amount: non_neg_integer(),
id32: non_neg_integer(),
type: non_neg_integer(),
}

defstruct [
:amount,
:id32,
:type,
]

def parse(map) when is_map(map) do
%__MODULE__{
amount: Map.get(map, "amount"),
id32: Map.get(map, "id32"),
type: Map.get(map, "type"),
}
end
end
36 changes: 36 additions & 0 deletions lib/helldivers_2/models/assignments/assignment/setting.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Helldivers2.Models.Assignments.Assignment.Setting do
alias Helldivers2.Models.Assignments.Assignment.Reward
alias Helldivers2.Models.Assignments.Assignment.Task

@type t :: %__MODULE__{
type: non_neg_integer(),
overrideTitle: String.t(),
overrideBrief: String.t(),
taskDescription: String.t(),
tasks: list(Task.t()),
reward: Reward.t(),
flags: non_neg_integer(),
}

defstruct [
:type,
:overrideTitle,
:overrideBrief,
:taskDescription,
:tasks,
:reward,
:flags,
]

def parse(map) when is_map(map) do
%__MODULE__{
type: Map.get(map, "type"),
overrideTitle: Map.get(map, "overrideTitle"),
overrideBrief: Map.get(map, "overrideBrief"),
taskDescription: Map.get(map, "taskDescription"),
tasks: Map.get(map, "tasks"),
reward: Map.get(map, "reward"),
flags: Map.get(map, "flags"),
}
end
end
21 changes: 21 additions & 0 deletions lib/helldivers_2/models/assignments/assignment/task.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Helldivers2.Models.Assignments.Assignment.Task do
@type t :: %__MODULE__{
type: non_neg_integer(),
values: list(non_neg_integer()),
valuesTypes: list(non_neg_integer()),
}

defstruct [
:type,
:values,
:valuesTypes,
]

def parse(map) when is_map(map) do
%__MODULE__{
type: Map.get(map, "type"),
values: Map.get(map, "values"),
valuesTypes: Map.get(map, "valuesTypes"),
}
end
end
44 changes: 39 additions & 5 deletions lib/helldivers_2/war_season.ex
Original file line number Diff line number Diff line change
@@ -8,12 +8,14 @@ defmodule Helldivers2.WarSeason do
use GenServer
require Logger

alias Helldivers2.Models.NewsFeed.Message
alias Helldivers2.Models.Assignments
alias Helldivers2.Models.Assignments.Assignment
alias Helldivers2.Models.NewsFeed
alias Helldivers2.Models.WarStatus.PlanetStatus
alias Helldivers2.Models.NewsFeed.Message
alias Helldivers2.Models.WarInfo
alias Helldivers2.Models.WarInfo.Planet
alias Helldivers2.Models.WarStatus
alias Helldivers2.Models.WarInfo
alias Helldivers2.Models.WarStatus.PlanetStatus

@options [
war_id: [
@@ -47,11 +49,30 @@ defmodule Helldivers2.WarSeason do
end
end

@spec store(String.t(), WarInfo.t() | WarStatus.t() | NewsFeed.t()) :: :ok | :error
@spec store(String.t(), WarInfo.t() | WarStatus.t()) :: :ok | :error
def store(war_id, data) do
GenServer.call({:via, Registry, {__MODULE__.Registry, war_id}}, {:store, data})
end

@spec store(String.t(), NewsFeed.t() | Assignments.t(), :news_feed | :assignments) :: :ok | :error
def store(war_id, data, what) do
GenServer.call({:via, Registry, {__MODULE__.Registry, war_id}}, {:store, data, what})
end

@doc """
Lookup the `Assignments` associated with the given `war_id`.
"""
@spec get_assignments(String.t()) :: {:ok, Assignments.t()} | {:error, term()}
def get_assignments(war_id) do
case :ets.lookup(table_name(war_id), Assignments) do
[] ->
{:error, :not_found}

[{Assignments, assignments}] ->
{:ok, assignments}
end
end

@doc """
Lookup the `WarInfo` associated with the given `war_id`.
"""
@@ -180,7 +201,8 @@ defmodule Helldivers2.WarSeason do
end

@impl GenServer
def handle_call({:store, news_feed}, _from, %{table: table} = state) when is_list(news_feed) do
def handle_call({:store, news_feed, :news_feed}, _from, %{table: table} = state)
when is_list(news_feed) do
:ets.insert(table, {NewsFeed, news_feed})

for message <- news_feed do
@@ -190,6 +212,18 @@ defmodule Helldivers2.WarSeason do
{:reply, :ok, state}
end

@impl GenServer
def handle_call({:store, assignments, :assignments}, _from, %{table: table} = state)
when is_list(assignments) do
:ets.insert(table, {Assignments, assignments})

for assignment <- assignments do
:ets.insert(table, {{Assignments, assignment.id32}, assignment})
end

{:reply, :ok, state}
end

# Get the table name for a given war id
def table_name(war_id) when is_number(war_id), do: table_name(to_string(war_id))

16 changes: 11 additions & 5 deletions lib/helldivers_2/war_sync.ex
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ defmodule Helldivers2.WarSync do
The resulting information is then sent to the `Helldivers2.WarSeason` process
responsible for the season being synced.
"""
alias Helldivers2.Models.Assignments
alias Helldivers2.Models.NewsFeed
alias Helldivers2.Models.WarStatus
alias Helldivers2.Models.WarInfo
@@ -29,10 +30,13 @@ defmodule Helldivers2.WarSync do
]

def child_spec(options) do
Supervisor.child_spec(%{
id: Keyword.get(options, :war_id),
start: {__MODULE__, :start_link, [options]}
}, [])
Supervisor.child_spec(
%{
id: Keyword.get(options, :war_id),
start: {__MODULE__, :start_link, [options]}
},
[]
)
end

@doc "Supported options:\n#{NimbleOptions.docs(@options)}"
@@ -70,7 +74,9 @@ defmodule Helldivers2.WarSync do
{:ok, war_status} <- WarStatus.download(war_id),
:ok <- WarSeason.store(war_id, war_status),
{:ok, news_feed} <- NewsFeed.download(war_id),
:ok <- WarSeason.store(war_id, news_feed) do
:ok <- WarSeason.store(war_id, news_feed, :news_feed),
{:ok, assignment} <- Assignments.download(war_id),
:ok <- WarSeason.store(war_id, assignment, :assignments) do
Logger.info("Finished synchronizing API #{war_id}")
end

23 changes: 23 additions & 0 deletions lib/helldivers_2_web/controllers/api/war_season_controller.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Helldivers2Web.Api.WarSeasonController do
alias Helldivers2Web.Schemas.AssignmentsMessageSchema
use Helldivers2Web, :controller
use OpenApiSpex.ControllerSpecs

@@ -26,6 +27,28 @@ defmodule Helldivers2Web.Api.WarSeasonController do
})
end

operation :show_assignments,
summary: "Get the assignments",
externalDocs: %OpenApiSpex.ExternalDocumentation{
description: "",
url: "https://api.live.prod.thehelldiversgame.com/api/Assignment/War/801"
},
parameters: [
war_id: [in: :path, description: "The war ID", type: :integer, example: 801]
],
responses: [
ok: AssignmentsMessageSchema.response(),
not_found: NotFoundSchema.response(),
too_many_requests: TooManyRequestsSchema.response(),
unprocessable_entity: JsonErrorResponse.response()
]

def show_assignments(conn, %{war_id: war_id}) do
with {:ok, assignments} <- WarSeason.get_assignments(war_id) do
render(conn, :show, assignments: assignments)
end
end

operation :show_info,
summary: "Get information on a war season",
externalDocs: %OpenApiSpex.ExternalDocumentation{
11 changes: 11 additions & 0 deletions lib/helldivers_2_web/controllers/api/war_season_json.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Helldivers2Web.Api.WarSeasonJSON do
alias Helldivers2.Models.Assignments
alias Helldivers2.Models.NewsFeed
alias Helldivers2Web.Api.GlobalEventsJSON
alias Helldivers2Web.Api.JointOperationsJSON
@@ -11,6 +12,7 @@ defmodule Helldivers2Web.Api.WarSeasonJSON do
def show(%{war_info: war_info}), do: show(war_info)
def show(%{war_status: war_status}), do: show(war_status)
def show(%{news_feed: news_feed}), do: Enum.map(news_feed, &show/1)
def show(%{assignments: assignments}), do: Enum.map(assignments, &show/1)

def show(%WarInfo{} = war_info) do
%{
@@ -57,4 +59,13 @@ defmodule Helldivers2Web.Api.WarSeasonJSON do
"message" => message.messages
}
end

def show(%Assignments.Assignment{} = assignment) do
%{
"id32" => assignment.id32,
"progress" => assignment.progress,
"expiresIn" => assignment.expiresIn,
"setting" => assignment.setting,
}
end
end
1 change: 1 addition & 0 deletions lib/helldivers_2_web/router.ex
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ defmodule Helldivers2Web.Router do
pipe_through :api

get "/", WarSeasonController, :index
get "/:war_id/assignments", WarSeasonController, :show_assignments
get "/:war_id/info", WarSeasonController, :show_info
get "/:war_id/status", WarSeasonController, :show_status

105 changes: 105 additions & 0 deletions lib/helldivers_2_web/schemas/assignments_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule Helldivers2Web.Schemas.AssignmentsMessageSchema do
require OpenApiSpex

alias OpenApiSpex.Schema

@languages Application.compile_env(:helldivers_2, :languages)

@doc "Generates a schema for a single assignment response"
def response(),
do:
{"Assignment response", "application/json", __MODULE__,
Helldivers2Web.ApiSpec.default_options()}

def responses(),
do:
{"Assignments response", "application/json", %Schema{type: :array, items: __MODULE__},
Helldivers2Web.ApiSpec.default_options()}

OpenApiSpex.schema(%{
description: "Represents an assignment in a list of assignments",
type: :object,
properties: %{
id32: %Schema{
type: :integer,
description: "The identifier of this campaign"
},
progress: %Schema{
type: :string,
format: :"date-time",
description: "Progress of the assignment. Suspected that it's a tuple of length equal to the checkboxes in the UI. 0 is uncompleted and 1 is completed?"
},
expiresIn: %Schema{
type: :integer,
description: "When the assignment expires. Probably in seconds."
},
setting: %Schema{
type: :object,
properties: %{
type: %Schema{
type: :integer,
description: "TODO unknown"
},
overrideTitle: %Schema{
type: :string,
description: "The title of the assignment. \"MAJOR ORDER\" is one option.",
},
overrideBrief: %Schema{
type: :string,
description: "Thematic text for what is happening.",
},
taskDescription: %Schema{
type: :string,
description: "The specific task to perform.",
},
tasks: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
type: %Schema{
type: :integer,
description: "Probably keyed to some task string.",
},
values: %Schema{
type: :array,
items: %Schema{
type: :integer,
}
},
valuesTypes: %Schema{
type: :array,
items: %Schema{
type: :integer,
}
},
},
},
},
reward: %Schema{
type: :object,
properties: %{
amount: %Schema{
type: :integer,
description: "",
},
id32: %Schema{
type: :integer,
description: "",
},
type: %Schema{
type: :integer,
description: "",
},
},
},
flags: %Schema{
type: :integer,
description: "TODO what is this.",
}
}
},
}
})
end

42 changes: 42 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{ pkgs ? import <nixpkgs> {} }:

with pkgs;
let
inputs = [
curl
elixir_1_14
flyctl
gcc
glibcLocales
gnumake
libiconv
libxml2
openssl
postgresql
readline
yarn
zlib
] ++ lib.optional stdenv.isLinux inotify-tools
++ lib.optional stdenv.isDarwin terminal-notifier
++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [
CoreFoundation
CoreServices
]);

# define shell startup command
hooks = ''
# this allows mix to work on the local directory
mkdir -p .nix-mix
mkdir -p .nix-hex
export MIX_HOME=$PWD/.nix-mix
export HEX_HOME=$PWD/.nix-hex
export PATH=$MIX_HOME/bin:$PATH
export PATH=$HEX_HOME/bin:$PATH
export LANG=en_US.UTF-8
export ERL_AFLAGS="-kernel shell_history enabled"
'';

in mkShell {
buildInputs = inputs;
shellHook = hooks;
}