Files

666 lines
22 KiB
Elixir

defmodule WandererApp.Map.Operations.Signatures do
@moduledoc """
CRUD for map signatures.
"""
require Logger
alias WandererApp.Map.Operations
alias WandererApp.Map.Operations.Connections
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
alias WandererApp.Map.Server
alias WandererApp.Utils.EVEUtil
@spec validate_character_eve_id(map() | nil, String.t()) ::
{:ok, String.t()} | {:error, :invalid_character} | {:error, :unexpected_error}
defp validate_character_eve_id(params, fallback_char_id) when is_map(params) do
case Map.get(params, "character_eve_id") do
nil ->
{:ok, fallback_char_id}
provided_char_eve_id when is_binary(provided_char_eve_id) ->
case Character.by_eve_id(provided_char_eve_id) do
{:ok, character} ->
{:ok, character.id}
{:error, %Ash.Error.Query.NotFound{}} ->
{:error, :invalid_character}
{:error, %Ash.Error.Invalid{}} ->
# Invalid format (e.g., non-numeric string for an integer field)
{:error, :invalid_character}
{:error, reason} ->
Logger.error(
"[validate_character_eve_id] Unexpected error looking up character: #{inspect(reason)}"
)
{:error, :unexpected_error}
end
_ ->
{:error, :invalid_character}
end
end
defp validate_character_eve_id(_params, fallback_char_id) do
{:ok, fallback_char_id}
end
@spec list_signatures(String.t()) :: [map()]
def list_signatures(map_id) do
systems = Operations.list_systems(map_id)
if systems != [] do
systems
|> Enum.flat_map(fn sys ->
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
# Add solar_system_id to each signature and remove system_id
Enum.map(sigs, fn sig ->
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, sys.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
end)
else
err ->
Logger.error("[list_signatures] error: #{inspect(err)}")
[]
end
end)
else
[]
end
end
@spec create_signature(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
def create_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
%{"solar_system_id" => solar_system_id} = params
)
when is_integer(solar_system_id) do
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
{:ok, system} <- ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
attrs =
params
|> Map.put("system_id", system.id)
|> Map.delete("solar_system_id")
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: solar_system_id,
character_id: validated_char_uuid,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
# Handle linked_system_id if provided - auto-add system and create/update connection
linked_system_id = Map.get(params, "linked_system_id")
wormhole_type = Map.get(params, "type")
if is_integer(linked_system_id) and linked_system_id != solar_system_id do
handle_linked_system(
map_id,
solar_system_id,
linked_system_id,
wormhole_type,
user_id,
char_id
)
end
# Try to fetch the created signature to return with proper fields
with {:ok, sigs} <-
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
sig when not is_nil(sig) <- List.first(sigs) do
result =
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
_ ->
# Fallback: return attrs with solar_system_id added
attrs_result =
attrs
|> Map.put(:solar_system_id, solar_system_id)
|> Map.drop(["system_id"])
{:ok, attrs_result}
end
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
else
{:error, :invalid_character} ->
Logger.error("[create_signature] Invalid character_eve_id provided")
{:error, :invalid_character}
{:error, :unexpected_error} ->
Logger.error("[create_signature] Unexpected error during character validation")
{:error, :unexpected_error}
{:error, :invalid_solar_system} ->
Logger.error(
"[create_signature] Invalid solar_system_id: #{solar_system_id} (not a valid EVE system)"
)
{:error, :invalid_solar_system}
_ ->
Logger.error(
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
)
{:error, :system_not_found}
end
end
def create_signature(
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
_conn,
%{"solar_system_id" => _invalid} = _params
),
do: {:error, :missing_params}
def create_signature(_conn, _params), do: {:error, :missing_params}
# Check cache (not DB) to ensure system is actually visible on the map.
@spec ensure_system_on_map(String.t(), integer(), String.t(), String.t()) ::
{:ok, map()} | {:error, atom()}
defp ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
nil -> add_system_to_map(map_id, solar_system_id, user_id, char_id)
system -> {:ok, system}
end
end
@spec add_system_to_map(String.t(), integer(), String.t(), String.t()) ::
{:ok, map()} | {:error, atom()}
defp add_system_to_map(map_id, solar_system_id, user_id, char_id) do
with {:ok, static_info} when not is_nil(static_info) <-
WandererApp.CachedInfo.get_system_static_info(solar_system_id),
:ok <-
Server.add_system(
map_id,
%{solar_system_id: solar_system_id, coordinates: nil},
user_id,
char_id
),
system when not is_nil(system) <- fetch_system_after_add(map_id, solar_system_id) do
Logger.info("[create_signature] Auto-added system #{solar_system_id} to map #{map_id}")
{:ok, system}
else
{:ok, nil} ->
{:error, :invalid_solar_system}
{:error, _} ->
{:error, :invalid_solar_system}
nil ->
Logger.error("[add_system_to_map] Failed to fetch system after add")
{:error, :system_add_failed}
error ->
Logger.error("[add_system_to_map] Failed to add system: #{inspect(error)}")
{:error, :system_add_failed}
end
end
defp fetch_system_after_add(map_id, solar_system_id) do
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
nil ->
case MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: solar_system_id
}) do
{:ok, system} -> system
_ -> nil
end
system ->
system
end
end
# Handles the linked_system_id logic: auto-adds the linked system and creates/updates connection
@spec handle_linked_system(
String.t(),
integer(),
integer(),
String.t() | nil,
String.t(),
String.t()
) :: :ok | {:error, atom()}
defp handle_linked_system(
map_id,
source_system_id,
linked_system_id,
wormhole_type,
user_id,
char_id
) do
# Ensure the linked system is on the map
case ensure_system_on_map(map_id, linked_system_id, user_id, char_id) do
{:ok, _linked_system} ->
# Check if connection exists between the systems
case Connections.get_connection_by_systems(map_id, source_system_id, linked_system_id) do
{:ok, nil} ->
# No connection exists, create one
create_connection_with_wormhole_type(
map_id,
source_system_id,
linked_system_id,
wormhole_type,
char_id
)
{:ok, _existing_conn} ->
# Connection exists, update wormhole type if provided
update_connection_wormhole_type(
map_id,
source_system_id,
linked_system_id,
wormhole_type
)
{:error, reason} ->
Logger.warning(
"[handle_linked_system] Failed to check connection: #{inspect(reason)}"
)
{:error, :connection_check_failed}
end
{:error, :invalid_solar_system} ->
Logger.warning(
"[handle_linked_system] Invalid linked_system_id: #{linked_system_id} (not a valid EVE system)"
)
{:error, :invalid_linked_system}
{:error, reason} ->
Logger.warning("[handle_linked_system] Failed to add linked system: #{inspect(reason)}")
{:error, :linked_system_add_failed}
end
end
# Creates a connection between two systems with the specified wormhole type
@spec create_connection_with_wormhole_type(
String.t(),
integer(),
integer(),
String.t() | nil,
String.t()
) :: :ok | {:error, atom()}
defp create_connection_with_wormhole_type(
map_id,
source_system_id,
target_system_id,
wormhole_type,
char_id
) do
conn_attrs = %{
"solar_system_source" => source_system_id,
"solar_system_target" => target_system_id,
"type" => 0,
"wormhole_type" => wormhole_type
}
case Connections.create(conn_attrs, map_id, char_id) do
{:ok, :created} ->
Logger.info(
"[create_signature] Auto-created connection #{source_system_id} <-> #{target_system_id} (type: #{wormhole_type || "unknown"})"
)
:ok
{:skip, :exists} ->
# Connection already exists (race condition), update it instead
update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type)
error ->
Logger.warning(
"[create_connection_with_wormhole_type] Failed to create connection: #{inspect(error)}"
)
{:error, :connection_create_failed}
end
end
# Updates the wormhole type and ship size for an existing connection
@spec update_connection_wormhole_type(String.t(), integer(), integer(), String.t() | nil) ::
:ok | {:error, atom()}
defp update_connection_wormhole_type(_map_id, _source, _target, nil), do: :ok
defp update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type) do
# Get ship size from wormhole type
ship_size_type = EVEUtil.get_wh_size(wormhole_type)
if not is_nil(ship_size_type) do
case Server.update_connection_ship_size_type(map_id, %{
solar_system_source_id: source_system_id,
solar_system_target_id: target_system_id,
ship_size_type: ship_size_type
}) do
:ok ->
Logger.info(
"[create_signature] Updated connection #{source_system_id} <-> #{target_system_id} ship_size_type to #{ship_size_type} (wormhole: #{wormhole_type})"
)
:ok
error ->
Logger.warning(
"[update_connection_wormhole_type] Failed to update ship size: #{inspect(error)}"
)
{:error, :ship_size_update_failed}
end
else
:ok
end
end
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
sig_id,
params
) do
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
{:ok, sig} <- MapSystemSignature.by_id(sig_id),
{:ok, system} <- MapSystem.by_id(sig.system_id) do
base = %{
"eve_id" => sig.eve_id,
"name" => sig.name,
"kind" => sig.kind,
"group" => sig.group,
"type" => sig.type,
"custom_info" => sig.custom_info,
"description" => sig.description,
"linked_system_id" => sig.linked_system_id
}
# Merge user params (which may include character_eve_id) with base
attrs = Map.merge(base, params)
:ok =
Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [attrs],
removed_signatures: [],
solar_system_id: system.solar_system_id,
character_id: validated_char_uuid,
user_id: user_id,
delete_connection_with_sigs: false
})
# Fetch the updated signature to return with proper fields
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
result =
updated_sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
_ -> {:ok, attrs}
end
else
{:error, :invalid_character} ->
Logger.error("[update_signature] Invalid character_eve_id provided")
{:error, :invalid_character}
{:error, :unexpected_error} ->
Logger.error("[update_signature] Unexpected error during character validation")
{:error, :unexpected_error}
err ->
Logger.error("[update_signature] Signature or system not found: #{inspect(err)}")
{:error, :not_found}
end
end
def update_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
@spec delete_signature(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
def delete_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
sig_id
) do
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
{:ok, system} <- MapSystem.by_id(sig.system_id) do
removed = [
%{
"eve_id" => sig.eve_id,
"name" => sig.name,
"kind" => sig.kind,
"group" => sig.group
}
]
:ok =
Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [],
removed_signatures: removed,
solar_system_id: system.solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
})
:ok
else
err ->
Logger.error("[delete_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
end
def delete_signature(_conn, _sig_id), do: {:error, :missing_params}
@doc """
Links a signature to a target system, creating the association between
the signature and the wormhole connection to that system.
This also:
- Updates the signature's group to "Wormhole"
- Sets the target system's linked_sig_eve_id
- Copies temporary_name from signature to target system
- Updates connection time_status and ship_size_type from signature data
"""
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def link_signature(
%{assigns: %{map_id: map_id}} = _conn,
sig_id,
%{"solar_system_target" => solar_system_target}
)
when is_integer(solar_system_target) do
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
true <- source_system.map_id == map_id,
target_system when not is_nil(target_system) <-
WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_target}) do
# Update signature group to Wormhole and set linked_system_id
{:ok, updated_signature} =
signature
|> MapSystemSignature.update_group!(%{group: "Wormhole"})
|> MapSystemSignature.update_linked_system(%{linked_system_id: solar_system_target})
# Update target system if it has no linked signature or is already linked to the same signature
if is_nil(target_system.linked_sig_eve_id) or
target_system.linked_sig_eve_id == signature.eve_id do
# Set the target system's linked_sig_eve_id
Server.update_system_linked_sig_eve_id(map_id, %{
solar_system_id: solar_system_target,
linked_sig_eve_id: signature.eve_id
})
# Copy temporary_name if present
if not is_nil(signature.temporary_name) do
Server.update_system_temporary_name(map_id, %{
solar_system_id: solar_system_target,
temporary_name: signature.temporary_name
})
end
# Update connection time_status from signature custom_info
signature_time_status =
if not is_nil(signature.custom_info) do
case Jason.decode(signature.custom_info) do
{:ok, map} -> Map.get(map, "time_status")
{:error, _} -> nil
end
else
nil
end
# Update connection ship_size_type from signature wormhole type
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
# Back-link detection: if current signature yields no ship_size_type (e.g., K162),
# look for a forward signature in the target system that links back to our source
{signature_time_status, signature_ship_size_type} =
if is_nil(signature_ship_size_type) do
case Server.SignaturesImpl.find_forward_signature(
target_system.id,
source_system.solar_system_id
) do
nil ->
{signature_time_status, signature_ship_size_type}
forward_sig ->
Logger.info(
"[link_signature] Back-link detected: " <>
"using forward sig type=#{forward_sig.type} from target system"
)
forward_ship_size = EVEUtil.get_wh_size(forward_sig.type)
forward_time_status =
if is_nil(signature_time_status) and not is_nil(forward_sig.custom_info) do
case Jason.decode(forward_sig.custom_info) do
{:ok, map} -> Map.get(map, "time_status")
{:error, _} -> nil
end
else
signature_time_status
end
{forward_time_status, forward_ship_size}
end
else
{signature_time_status, signature_ship_size_type}
end
if not is_nil(signature_time_status) do
Server.update_connection_time_status(map_id, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: solar_system_target,
time_status: signature_time_status
})
end
if not is_nil(signature_ship_size_type) do
Server.update_connection_ship_size_type(map_id, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: solar_system_target,
ship_size_type: signature_ship_size_type
})
end
end
# Broadcast update
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
# Return the updated signature
result =
updated_signature
|> Map.from_struct()
|> Map.put(:solar_system_id, source_system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
false ->
{:error, :not_found}
nil ->
{:error, :target_system_not_found}
{:error, %Ash.Error.Query.NotFound{}} ->
{:error, :not_found}
err ->
Logger.error("[link_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
end
def link_signature(_conn, _sig_id, %{"solar_system_target" => _}),
do: {:error, :invalid_solar_system_target}
def link_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
@doc """
Unlinks a signature from its target system.
"""
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
def unlink_signature(%{assigns: %{map_id: map_id}} = _conn, sig_id) do
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
:ok <- if(source_system.map_id == map_id, do: :ok, else: {:error, :not_found}),
:ok <- if(not is_nil(signature.linked_system_id), do: :ok, else: {:error, :not_linked}) do
# Clear the target system's linked_sig_eve_id
Server.update_system_linked_sig_eve_id(map_id, %{
solar_system_id: signature.linked_system_id,
linked_sig_eve_id: nil
})
# Clear the signature's linked_system_id using the wrapper for logging
{:ok, updated_signature} =
Server.SignaturesImpl.update_signature_linked_system(signature, %{
linked_system_id: nil
})
# Broadcast update
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
# Return the updated signature
result =
updated_signature
|> Map.from_struct()
|> Map.put(:solar_system_id, source_system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
{:error, :not_found} ->
{:error, :not_found}
{:error, :not_linked} ->
{:error, :not_linked}
{:error, %Ash.Error.Query.NotFound{}} ->
{:error, :not_found}
err ->
Logger.error("[unlink_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
end
def unlink_signature(_conn, _sig_id), do: {:error, :missing_params}
end