mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-05-03 07:50:37 +00:00
403 lines
12 KiB
Elixir
403 lines
12 KiB
Elixir
defmodule WandererApp.Map.Server.SignaturesImpl do
|
|
@moduledoc false
|
|
|
|
require Logger
|
|
|
|
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
|
alias WandererApp.Character
|
|
alias WandererApp.User.ActivityTracker
|
|
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
|
|
alias WandererApp.Utils.EVEUtil
|
|
|
|
@doc """
|
|
Public entrypoint for updating signatures on a map system.
|
|
"""
|
|
def update_signatures(
|
|
map_id,
|
|
%{
|
|
solar_system_id: system_solar_id,
|
|
character_id: char_id,
|
|
user_id: user_id,
|
|
delete_connection_with_sigs: delete_conn?,
|
|
added_signatures: added_params,
|
|
updated_signatures: updated_params,
|
|
removed_signatures: removed_params
|
|
}
|
|
)
|
|
when not is_nil(char_id) do
|
|
with {:ok, system} <-
|
|
MapSystem.read_by_map_and_solar_system(%{
|
|
map_id: map_id,
|
|
solar_system_id: system_solar_id
|
|
}) do
|
|
do_update_signatures(
|
|
map_id,
|
|
system,
|
|
char_id,
|
|
user_id,
|
|
delete_conn?,
|
|
added_params,
|
|
updated_params,
|
|
removed_params
|
|
)
|
|
else
|
|
error ->
|
|
Logger.warning("Skipping signature update: #{inspect(error)}")
|
|
end
|
|
end
|
|
|
|
def update_signatures(_map_id, _), do: :ok
|
|
|
|
defp do_update_signatures(
|
|
map_id,
|
|
system,
|
|
character_id,
|
|
user_id,
|
|
delete_conn?,
|
|
added_params,
|
|
updated_params,
|
|
removed_params
|
|
) do
|
|
# Get character EVE ID for signature parsing
|
|
character_eve_id =
|
|
case Character.get_character(character_id) do
|
|
{:ok, %{eve_id: eve_id}} ->
|
|
eve_id
|
|
|
|
_ ->
|
|
Logger.warning("Could not get character EVE ID for character_id: #{character_id}")
|
|
nil
|
|
end
|
|
|
|
# parse incoming DTOs
|
|
added_sigs = parse_signatures(added_params, character_eve_id, system.id)
|
|
updated_sigs = parse_signatures(updated_params, character_eve_id, system.id)
|
|
removed_sigs = parse_signatures(removed_params, character_eve_id, system.id)
|
|
|
|
# fetch both current & all (including deleted) signatures once
|
|
existing_current = MapSystemSignature.by_system_id!(system.id)
|
|
existing_all = MapSystemSignature.by_system_id_all!(system.id)
|
|
|
|
removed_ids = Enum.map(removed_sigs, & &1.eve_id)
|
|
updated_ids = Enum.map(updated_sigs, & &1.eve_id)
|
|
added_ids = Enum.map(added_sigs, & &1.eve_id)
|
|
|
|
# 1. Removals
|
|
existing_current
|
|
|> Enum.filter(&(&1.eve_id in removed_ids))
|
|
|> Enum.each(&remove_signature(map_id, &1, system, delete_conn?))
|
|
|
|
# 2. Updates
|
|
existing_current
|
|
|> Enum.filter(&(&1.eve_id in updated_ids))
|
|
|> Enum.each(fn existing ->
|
|
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
|
|
apply_update_signature(map_id, existing, update)
|
|
end)
|
|
|
|
# 3. Additions & restorations
|
|
added_eve_ids = Enum.map(added_sigs, & &1.eve_id)
|
|
|
|
existing_index =
|
|
existing_all
|
|
|> Enum.filter(&(&1.eve_id in added_eve_ids))
|
|
|> Map.new(&{&1.eve_id, &1})
|
|
|
|
added_sigs
|
|
|> Enum.each(fn sig ->
|
|
case existing_index[sig.eve_id] do
|
|
nil ->
|
|
MapSystemSignature.create!(sig)
|
|
|
|
existing ->
|
|
# If signature already exists, update it instead of ignoring
|
|
# This handles the case where frontend sends existing sigs as "added"
|
|
apply_update_signature(map_id, existing, sig)
|
|
end
|
|
end)
|
|
|
|
# 4. Activity tracking
|
|
if added_ids != [] do
|
|
track_activity(
|
|
:signatures_added,
|
|
map_id,
|
|
system.solar_system_id,
|
|
user_id,
|
|
character_id,
|
|
added_ids
|
|
)
|
|
end
|
|
|
|
if removed_ids != [] do
|
|
track_activity(
|
|
:signatures_removed,
|
|
map_id,
|
|
system.solar_system_id,
|
|
user_id,
|
|
character_id,
|
|
removed_ids
|
|
)
|
|
end
|
|
|
|
# 5. Broadcast to any live subscribers
|
|
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
|
|
|
|
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
|
# Send individual signature events
|
|
Enum.each(added_sigs, fn sig ->
|
|
WandererApp.ExternalEvents.broadcast(map_id, :signature_added, %{
|
|
solar_system_id: system.solar_system_id,
|
|
signature_id: sig.eve_id,
|
|
name: sig.name,
|
|
kind: sig.kind,
|
|
group: sig.group,
|
|
type: sig.type
|
|
})
|
|
end)
|
|
|
|
Enum.each(removed_ids, fn sig_eve_id ->
|
|
WandererApp.ExternalEvents.broadcast(map_id, :signature_removed, %{
|
|
solar_system_id: system.solar_system_id,
|
|
signature_id: sig_eve_id
|
|
})
|
|
end)
|
|
|
|
# Also send the summary event for backwards compatibility
|
|
WandererApp.ExternalEvents.broadcast(map_id, :signatures_updated, %{
|
|
solar_system_id: system.solar_system_id,
|
|
added_count: length(added_ids),
|
|
updated_count: length(updated_ids),
|
|
removed_count: length(removed_ids)
|
|
})
|
|
|
|
# Always return :ok - external event failures should not affect the main operation
|
|
:ok
|
|
end
|
|
|
|
defp remove_signature(map_id, sig, system, delete_conn?) do
|
|
# Check if this signature is the active one for the target system
|
|
# This prevents deleting connections when old/orphan signatures are removed
|
|
is_active = sig.linked_system_id && is_active_signature_for_target?(map_id, sig)
|
|
|
|
# Only delete connection if this signature is the active one
|
|
if delete_conn? && is_active do
|
|
ConnectionsImpl.delete_connection(map_id, %{
|
|
solar_system_source_id: system.solar_system_id,
|
|
solar_system_target_id: sig.linked_system_id
|
|
})
|
|
end
|
|
|
|
# Only clear linked_sig_eve_id if this signature is the active one
|
|
if is_active do
|
|
SystemsImpl.update_system_linked_sig_eve_id(map_id, %{
|
|
solar_system_id: sig.linked_system_id,
|
|
linked_sig_eve_id: nil
|
|
})
|
|
end
|
|
|
|
sig
|
|
|> MapSystemSignature.destroy!()
|
|
end
|
|
|
|
defp is_active_signature_for_target?(map_id, sig) do
|
|
case MapSystem.read_by_map_and_solar_system(%{
|
|
map_id: map_id,
|
|
solar_system_id: sig.linked_system_id
|
|
}) do
|
|
{:ok, target_system} -> target_system.linked_sig_eve_id == sig.eve_id
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
def apply_update_signature(
|
|
map_id,
|
|
%MapSystemSignature{} = existing,
|
|
update_params
|
|
)
|
|
when not is_nil(update_params) do
|
|
case MapSystemSignature.update(
|
|
existing,
|
|
update_params |> Map.put(:update_forced_at, DateTime.utc_now())
|
|
) do
|
|
{:ok, updated} ->
|
|
maybe_update_connection_time_status(map_id, existing, updated)
|
|
maybe_update_connection_mass_status(map_id, existing, updated)
|
|
maybe_sync_custom_mass_status_to_connection(map_id, existing, updated)
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to update signature #{existing.id}: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp maybe_update_connection_time_status(
|
|
map_id,
|
|
%{custom_info: old_custom_info} = _old_sig,
|
|
%{custom_info: new_custom_info, system_id: system_id, linked_system_id: linked_system_id} =
|
|
_updated_sig
|
|
)
|
|
when not is_nil(linked_system_id) do
|
|
old_time_status = get_time_status(old_custom_info)
|
|
new_time_status = get_time_status(new_custom_info)
|
|
|
|
if old_time_status != new_time_status do
|
|
{:ok, source_system} = MapSystem.by_id(system_id)
|
|
|
|
ConnectionsImpl.update_connection_time_status(map_id, %{
|
|
solar_system_source_id: source_system.solar_system_id,
|
|
solar_system_target_id: linked_system_id,
|
|
time_status: new_time_status
|
|
})
|
|
end
|
|
end
|
|
|
|
defp maybe_update_connection_time_status(_map_id, _old_sig, _updated_sig), do: :ok
|
|
|
|
defp maybe_update_connection_mass_status(
|
|
map_id,
|
|
%{type: old_type} = _old_sig,
|
|
%{type: new_type, system_id: system_id, linked_system_id: linked_system_id} =
|
|
_updated_sig
|
|
)
|
|
when not is_nil(linked_system_id) do
|
|
if old_type != new_type do
|
|
{:ok, source_system} = MapSystem.by_id(system_id)
|
|
signature_ship_size_type = EVEUtil.get_wh_size(new_type)
|
|
|
|
if not is_nil(signature_ship_size_type) do
|
|
ConnectionsImpl.update_connection_ship_size_type(map_id, %{
|
|
solar_system_source_id: source_system.solar_system_id,
|
|
solar_system_target_id: linked_system_id,
|
|
ship_size_type: signature_ship_size_type
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
|
|
|
defp maybe_sync_custom_mass_status_to_connection(
|
|
map_id,
|
|
%{custom_info: old_custom_info} = _old_sig,
|
|
%{custom_info: new_custom_info, system_id: system_id, linked_system_id: linked_system_id} =
|
|
_updated_sig
|
|
)
|
|
when not is_nil(linked_system_id) do
|
|
old_mass_status = get_mass_status(old_custom_info)
|
|
new_mass_status = get_mass_status(new_custom_info)
|
|
|
|
if old_mass_status != new_mass_status and not is_nil(new_mass_status) do
|
|
{:ok, source_system} = MapSystem.by_id(system_id)
|
|
|
|
ConnectionsImpl.update_connection_mass_status(map_id, %{
|
|
solar_system_source_id: source_system.solar_system_id,
|
|
solar_system_target_id: linked_system_id,
|
|
mass_status: new_mass_status
|
|
})
|
|
end
|
|
end
|
|
|
|
defp maybe_sync_custom_mass_status_to_connection(_map_id, _old_sig, _updated_sig), do: :ok
|
|
|
|
@doc """
|
|
Finds the "forward" signature in a target system that links back to the source system.
|
|
Used for back-link detection: when a K162 is linked from System B → System A,
|
|
finds the existing signature in System A that already links to System B (e.g., H296).
|
|
"""
|
|
def find_forward_signature(target_system_uuid, source_solar_system_id) do
|
|
target_system_uuid
|
|
|> MapSystemSignature.by_system_id!()
|
|
|> Enum.find(fn sig -> sig.linked_system_id == source_solar_system_id end)
|
|
rescue
|
|
e ->
|
|
Logger.warning("[find_forward_signature] Error: #{inspect(e)}")
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Wrapper for updating a signature's linked_system_id with logging.
|
|
Logs all unlink operations (when linked_system_id is set to nil) with context
|
|
to help diagnose unexpected unlinking issues.
|
|
"""
|
|
def update_signature_linked_system(signature, %{linked_system_id: nil} = params) do
|
|
# Log all unlink operations with context for debugging
|
|
Logger.warning(
|
|
"[Signature Unlink] eve_id=#{signature.eve_id} " <>
|
|
"system_id=#{signature.system_id} " <>
|
|
"old_linked_system_id=#{signature.linked_system_id} " <>
|
|
"stacktrace=#{format_stacktrace()}"
|
|
)
|
|
|
|
MapSystemSignature.update_linked_system(signature, params)
|
|
end
|
|
|
|
def update_signature_linked_system(signature, params) do
|
|
MapSystemSignature.update_linked_system(signature, params)
|
|
end
|
|
|
|
defp format_stacktrace do
|
|
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
|
|
|
stacktrace
|
|
|> Enum.take(10)
|
|
|> Enum.map_join(" <- ", fn {mod, fun, arity, _} ->
|
|
"#{inspect(mod)}.#{fun}/#{arity}"
|
|
end)
|
|
end
|
|
|
|
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
|
|
ActivityTracker.track_map_event(event, %{
|
|
map_id: map_id,
|
|
solar_system_id: solar_system_id,
|
|
user_id: user_id,
|
|
character_id: character_id,
|
|
signatures: signatures
|
|
})
|
|
end
|
|
|
|
@doc false
|
|
defp parse_signatures(signatures, character_eve_id, system_id) do
|
|
Enum.map(signatures, fn sig ->
|
|
base = %{
|
|
system_id: system_id,
|
|
eve_id: sig["eve_id"],
|
|
name: sig["name"],
|
|
temporary_name: sig["temporary_name"],
|
|
description: Map.get(sig, "description"),
|
|
kind: sig["kind"],
|
|
group: sig["group"],
|
|
type: Map.get(sig, "type"),
|
|
custom_info: Map.get(sig, "custom_info"),
|
|
# Use character_eve_id from sig if provided, otherwise use the default
|
|
character_eve_id: Map.get(sig, "character_eve_id", character_eve_id),
|
|
deleted: false
|
|
}
|
|
|
|
# Only include linked_system_id when explicitly provided in the payload.
|
|
# Frontend sends "linked_system" (object), not "linked_system_id" (integer).
|
|
# Including nil would silently clear the DB value via the Ash :update action.
|
|
if Map.has_key?(sig, "linked_system_id") do
|
|
Map.put(base, :linked_system_id, sig["linked_system_id"])
|
|
else
|
|
base
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp get_time_status(nil), do: nil
|
|
|
|
defp get_time_status(custom_info_json) do
|
|
custom_info_json
|
|
|> Jason.decode!()
|
|
|> Map.get("time_status")
|
|
end
|
|
|
|
defp get_mass_status(nil), do: nil
|
|
|
|
defp get_mass_status(custom_info_json) do
|
|
custom_info_json
|
|
|> Jason.decode!()
|
|
|> Map.get("mass_status")
|
|
end
|
|
end
|