Files
wanderer/lib/wanderer_app/map/operations/connections.ex
2025-06-13 21:14:07 -04:00

321 lines
12 KiB
Elixir

defmodule WandererApp.Map.Operations.Connections do
@moduledoc """
Operations for managing map connections, including creation, updates, and deletions.
Handles special cases like C1 wormhole sizing rules and unique constraint handling.
"""
require Logger
alias WandererApp.Map.Server
alias Ash.Error.Invalid
alias WandererApp.MapConnectionRepo
alias WandererApp.CachedInfo
# Connection type constants
@connection_type_wormhole 0
@connection_type_stargate 1
# Ship size constants
@small_ship_size 0
@medium_ship_size 1
@large_ship_size 2
@xlarge_ship_size 3
# System class constants
@c1_system_class 1
@doc """
Creates a connection between two systems, applying special rules for C1 wormholes.
Handles parsing of input parameters, validates system information, and manages
unique constraint violations gracefully.
"""
def create(attrs, map_id, char_id) do
do_create(attrs, map_id, char_id)
end
defp do_create(attrs, map_id, char_id) do
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),
{:ok, src_info} <- CachedInfo.get_system_static_info(source),
{:ok, tgt_info} <- CachedInfo.get_system_static_info(target) do
build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info)
else
{:error, reason} -> handle_precondition_error(reason, attrs)
{:ok, []} -> {:error, :inconsistent_state}
other -> {:error, :unexpected_precondition_error, other}
end
end
defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do
Logger.debug("[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}")
# Guard against nil info
if is_nil(src_info) or is_nil(tgt_info) do
{:error, :invalid_system_info}
else
info = %{
solar_system_source_id: src_info.solar_system_id,
solar_system_target_id: tgt_info.solar_system_id,
character_id: char_id,
type: parse_type(attrs["type"]),
ship_size_type: resolve_ship_size(attrs, src_info, tgt_info)
}
case Server.add_connection(map_id, info) do
:ok -> {:ok, :created}
{:ok, []} -> log_warn_and(:inconsistent_state, info)
{:error, %Invalid{errors: errs}} = err ->
if Enum.any?(errs, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
{:error, _} = err -> Logger.error("[add_connection] #{inspect(err)}"); {:error, :server_error}
other -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error}
end
end
end
defp resolve_ship_size(attrs, src_info, tgt_info) do
type = parse_type(attrs["type"])
if type == @connection_type_wormhole and
(src_info.system_class == @c1_system_class or
tgt_info.system_class == @c1_system_class) do
@medium_ship_size
else
parse_ship_size(attrs["ship_size_type"], @large_ship_size)
end
end
defp parse_ship_size(nil, default), do: default
defp parse_ship_size(val, _default) when is_integer(val), do: val
defp parse_ship_size(val, default) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> default
end
end
defp parse_ship_size(_, default), do: default
defp parse_type(nil), do: @connection_type_wormhole
defp parse_type(val) when is_integer(val), do: val
defp parse_type(val) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> @connection_type_wormhole
end
end
defp parse_type(_), do: @connection_type_wormhole
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
defp parse_int(val, _) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> {:ok, i}
:error -> {:error, :invalid_integer}
end
end
defp parse_int(_, field), do: {:error, {:invalid_field, field}}
defp handle_precondition_error(reason, attrs) do
Logger.warning("[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}")
{:error, :precondition_failed, reason}
end
defp log_warn_and(return, info) do
Logger.warning("[add_connection] inconsistent for #{inspect(info)}")
{:error, return}
end
defp is_unique_constraint_error?(%{code: :unique_constraint}), do: true
defp is_unique_constraint_error?(_), do: false
@spec list_connections(String.t()) :: [map()] | {:error, atom()}
def list_connections(map_id) do
with {:ok, conns} <- MapConnectionRepo.get_by_map(map_id) do
conns
else
{:error, err} ->
Logger.warning("[list_connections] Repo error: #{inspect(err)}")
{:error, :repo_error}
other ->
Logger.error("[list_connections] Unexpected repo result: #{inspect(other)}")
{:error, :unexpected_repo_result}
end
end
@spec list_connections(String.t(), integer()) :: [map()]
def list_connections(map_id, system_id) do
list_connections(map_id)
|> Enum.filter(fn c ->
c.solar_system_source == system_id or c.solar_system_target == system_id
end)
end
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
def get_connection(map_id, conn_id) do
case MapConnectionRepo.get_by_id(map_id, conn_id) do
{:ok, conn} -> {:ok, conn}
_ -> {:error, "Connection not found"}
end
end
@spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
result <- (
try do
_allowed_keys = [
:mass_status,
:ship_size_type,
:type
]
_update_map =
attrs
|> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end)
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|> Enum.into(%{})
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
res
rescue
error ->
Logger.error("[update_connection] Exception: #{inspect(error)}")
{:error, :exception}
end
),
:ok <- result,
{:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, conn_id) do
{:ok, updated_conn}
else
{:error, err} -> {:error, err}
_ -> {:error, :unexpected_error}
end
end
def update_connection(_conn, _conn_id, _attrs), do: {:error, :missing_params}
@spec delete_connection(Plug.Conn.t(), integer(), integer()) :: :ok | {:error, atom()}
def delete_connection(%{assigns: %{map_id: map_id}} = _conn, src, tgt) do
case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do
:ok -> :ok
{:error, :not_found} ->
Logger.warning("[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}")
{:error, :not_found}
{:error, _} = err ->
Logger.error("[delete_connection] Server error: #{inspect(err)}")
{:error, :server_error}
_ ->
Logger.error("[delete_connection] Unknown error")
{:error, :unknown}
end
end
def delete_connection(_conn, _src, _tgt), do: {:error, :missing_params}
@doc "Batch upsert for connections"
@spec upsert_batch(Plug.Conn.t(), [map()]) :: %{created: integer(), updated: integer(), skipped: integer()}
def upsert_batch(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conns) do
_assigns = %{map_id: map_id, char_id: char_id}
Enum.reduce(conns, %{created: 0, updated: 0, skipped: 0}, fn conn_attrs, acc ->
case upsert_single(conn, conn_attrs) do
{:ok, :created} -> %{acc | created: acc.created + 1}
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
_ -> %{acc | skipped: acc.skipped + 1}
end
end)
end
def upsert_batch(_conn, _conns), do: %{created: 0, updated: 0, skipped: 0}
@doc "Upsert a single connection"
@spec upsert_single(Plug.Conn.t(), map()) :: {:ok, :created | :updated} | {:error, atom()}
def upsert_single(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conn_data) do
source = conn_data["solar_system_source"] || conn_data[:solar_system_source]
target = conn_data["solar_system_target"] || conn_data[:solar_system_target]
with {:ok, %{} = existing_conn} <- get_connection_by_systems(map_id, source, target),
{:ok, _} <- update_connection(conn, existing_conn.id, conn_data) do
{:ok, :updated}
else
{:ok, nil} ->
case create_connection(map_id, conn_data, char_id) do
{:ok, _} -> {:ok, :created}
{:skip, :exists} -> {:ok, :updated}
err -> {:error, err}
end
{:error, _} = err ->
Logger.warning("[upsert_single] Connection lookup error: #{inspect(err)}")
{:error, :lookup_error}
err ->
Logger.error("[upsert_single] Update failed: #{inspect(err)}")
{:error, :unexpected_error}
end
end
def upsert_single(_conn, _conn_data), do: {:error, :missing_params}
@doc "Get a connection by source and target system IDs"
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
def get_connection_by_systems(map_id, source, target) do
with {:ok, conn} <- WandererApp.Map.find_connection(map_id, source, target) do
if conn, do: {:ok, conn}, else: WandererApp.Map.find_connection(map_id, target, source)
else
{:error, reason} -> {:error, reason}
end
end
# -- Helpers ---------------------------------------------------------------
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
Enum.reduce_while(attrs, :ok, fn {key, val}, _acc ->
result =
case key do
"mass_status" -> maybe_update_mass_status(map_id, conn, val)
"ship_size_type" -> maybe_update_ship_size_type(map_id, conn, val)
"type" -> maybe_update_type(map_id, conn, val)
_ -> :ok
end
if result == :ok do
{:cont, :ok}
else
{:halt, result}
end
end)
|> case do
:ok -> :ok
err -> err
end
end
defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok
defp maybe_update_mass_status(map_id, conn, value) do
Server.update_connection_mass_status(map_id, %{
solar_system_source_id: conn.solar_system_source,
solar_system_target_id: conn.solar_system_target,
mass_status: value
})
end
defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok
defp maybe_update_ship_size_type(map_id, conn, value) do
Server.update_connection_ship_size_type(map_id, %{
solar_system_source_id: conn.solar_system_source,
solar_system_target_id: conn.solar_system_target,
ship_size_type: value
})
end
defp maybe_update_type(_map_id, _conn, nil), do: :ok
defp maybe_update_type(map_id, conn, value) do
Server.update_connection_type(map_id, %{
solar_system_source_id: conn.solar_system_source,
solar_system_target_id: conn.solar_system_target,
type: value
})
end
@doc "Creates a connection between two systems"
@spec create_connection(String.t(), map(), String.t()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(map_id, attrs, char_id) do
do_create(attrs, map_id, char_id)
end
@doc "Creates a connection between two systems from a Plug.Conn"
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
do_create(attrs, map_id, char_id)
end
end