Files
wanderer/lib/wanderer_app/route_builder_client.ex
2026-02-08 19:25:50 +03:00

269 lines
8.0 KiB
Elixir

defmodule WandererApp.RouteBuilderClient do
@moduledoc """
HTTP client for the local route builder service.
"""
require Logger
@timeout_opts [pool_timeout: 5_000, receive_timeout: :timer.seconds(30)]
@loot_dir Path.join(["repo", "data", "route_by_systems"])
@available_routes_by ["blueLoot", "redLoot", "thera", "turnur", "so_cleaning", "trade_hubs"]
def available_routes_by(), do: @available_routes_by
def find_closest(
%{
origin: origin,
flag: flag,
connections: connections,
avoid: avoid,
count: count,
type: type,
security_type: security_type
} = payload
) do
url = "#{WandererApp.Env.custom_route_base_url()}/route/findClosest"
routes_settings = Map.get(payload, :routes_settings, %{})
destinations = destinations_for(type, security_type, routes_settings)
payload = %{
origin: origin,
flag: flag,
connections: connections || [],
avoid: avoid || [],
destinations: destinations,
count: count || 1
}
case Req.post(url, Keyword.merge([json: payload], @timeout_opts)) do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
{:ok, body}
{:ok, %{status: status, body: body}} ->
Logger.warning("[RouteBuilderClient] Unexpected status: #{status}")
{:error, {:unexpected_status, status, body}}
{:error, reason} ->
Logger.error("[RouteBuilderClient] Request failed: #{inspect(reason)}")
{:error, reason}
end
end
defp destinations_for(type, security_type, routes_settings) do
case normalize_type(type) do
:thera ->
thera_destinations(routes_settings, security_type)
:turnur ->
turnur_destinations(routes_settings, security_type)
_ ->
case load_loot_data(type) do
{:ok, %{"system_ids_by_band" => by_band}} ->
high = Map.get(by_band, "high", [])
low = Map.get(by_band, "low", [])
pick_by_band(high, low, security_type)
{:ok, %{"system_ids" => system_ids}} when is_list(system_ids) ->
filter_by_security(system_ids, security_type)
{:error, reason} ->
Logger.error("[RouteBuilderClient] Failed to load loot data: #{inspect(reason)}")
[]
_ ->
[]
end
end
end
defp thera_destinations(routes_settings, security_type) do
{:ok, thera_chains} = WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
system_ids =
thera_chains
|> Enum.map(fn %{first: first, second: second} ->
pick_thera_destination(first, second)
end)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
filter_by_security(system_ids, security_type)
end
defp turnur_destinations(routes_settings, security_type) do
{:ok, turnur_chains} = WandererApp.Server.TurnurDataFetcher.get_chain_pairs(routes_settings)
system_ids =
turnur_chains
|> Enum.map(fn %{first: first, second: second} ->
pick_turnur_destination(first, second)
end)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
filter_by_security(system_ids, security_type)
end
defp filter_by_security(system_ids, security_type) do
case normalize_security_type(security_type) do
"high" ->
Enum.filter(system_ids, fn system_id ->
case system_security(system_id) do
{:ok, security} -> security >= 0.5
_ -> false
end
end)
"low" ->
Enum.filter(system_ids, fn system_id ->
case system_security(system_id) do
{:ok, security} -> security > 0.0 and security < 0.5
_ -> false
end
end)
_ ->
system_ids
end
end
defp system_security(system_id) do
case WandererApp.CachedInfo.get_system_static_info(system_id) do
{:ok, %{security: security}} -> parse_security(security)
_ -> {:error, :missing_security}
end
end
defp pick_thera_destination(first, second) do
first_is_thera = is_thera_system?(first)
second_is_thera = is_thera_system?(second)
cond do
first_is_thera and not second_is_thera -> second
second_is_thera and not first_is_thera -> first
true -> second
end
end
defp is_thera_system?(system_id) do
case WandererApp.CachedInfo.get_system_static_info(system_id) do
{:ok, %{system_class: 12}} -> true
_ -> false
end
end
defp pick_turnur_destination(first, second) do
first_is_turnur = is_turnur_system?(first)
second_is_turnur = is_turnur_system?(second)
cond do
first_is_turnur and not second_is_turnur -> second
second_is_turnur and not first_is_turnur -> first
true -> second
end
end
defp is_turnur_system?(system_id) do
case WandererApp.CachedInfo.get_system_static_info(system_id) do
{:ok, %{solar_system_name: name}} when is_binary(name) ->
String.downcase(name) == "turnur"
_ ->
false
end
end
defp parse_security(security) when is_float(security), do: {:ok, security}
defp parse_security(security) when is_integer(security), do: {:ok, security * 1.0}
defp parse_security(security) when is_binary(security) do
case Float.parse(security) do
{value, _} -> {:ok, value}
_ -> {:error, :invalid_security}
end
end
defp parse_security(_), do: {:error, :invalid_security}
defp normalize_security_type("high"), do: "high"
defp normalize_security_type(:high), do: "high"
defp normalize_security_type("hight"), do: "high"
defp normalize_security_type(:hight), do: "high"
defp normalize_security_type("low"), do: "low"
defp normalize_security_type(:low), do: "low"
defp normalize_security_type(_), do: "both"
def stations_for(type) do
case normalize_type(type) do
:thera ->
%{}
:turnur ->
%{}
_ ->
case load_loot_data(type) do
{:ok, %{"system_stations" => system_stations}} when is_map(system_stations) ->
system_stations
{:ok, _} ->
%{}
{:error, reason} ->
Logger.error("[RouteBuilderClient] Failed to load loot stations: #{inspect(reason)}")
%{}
end
end
end
defp pick_by_band(high, _low, "high"), do: high
defp pick_by_band(high, _low, :high), do: high
defp pick_by_band(high, _low, "hight"), do: high
defp pick_by_band(high, _low, :hight), do: high
defp pick_by_band(_high, low, "low"), do: low
defp pick_by_band(_high, low, :low), do: low
defp pick_by_band(high, low, _), do: high ++ low
defp load_loot_data("blueLoot"), do: load_loot_file("blueloot.json")
defp load_loot_data(:blueLoot), do: load_loot_file("blueloot.json")
defp load_loot_data("redLoot"), do: load_loot_file("redloot.json")
defp load_loot_data(:redLoot), do: load_loot_file("redloot.json")
defp load_loot_data("so_cleaning"), do: load_loot_file("ss_cleaning.json")
defp load_loot_data(:so_cleaning), do: load_loot_file("ss_cleaning.json")
defp load_loot_data("trade_hubs"), do: load_loot_file("trade_hubs.json")
defp load_loot_data(:trade_hubs), do: load_loot_file("trade_hubs.json")
defp load_loot_data(_), do: load_loot_file("blueloot.json")
defp normalize_type("thera"), do: :thera
defp normalize_type(:thera), do: :thera
defp normalize_type("turnur"), do: :turnur
defp normalize_type(:turnur), do: :turnur
defp normalize_type("so_cleaning"), do: :so_cleaning
defp normalize_type(:so_cleaning), do: :so_cleaning
defp normalize_type("trade_hubs"), do: :trade_hubs
defp normalize_type(:trade_hubs), do: :trade_hubs
defp normalize_type(type), do: type
defp load_loot_file(filename) do
key = {__MODULE__, :loot_data, filename}
case :persistent_term.get(key, :missing) do
:missing ->
path = Path.join([:code.priv_dir(:wanderer_app), @loot_dir, filename])
with {:ok, body} <- File.read(path),
{:ok, json} <- Jason.decode(body) do
:persistent_term.put(key, json)
{:ok, json}
else
error -> error
end
cached ->
{:ok, cached}
end
end
end