mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-05-01 23:10:30 +00:00
329 lines
11 KiB
Elixir
329 lines
11 KiB
Elixir
defmodule WandererApp.Map.Operations.Systems do
|
|
@moduledoc """
|
|
CRUD and batch upsert for map systems.
|
|
"""
|
|
|
|
alias WandererApp.MapSystemRepo
|
|
alias WandererApp.Map.Server
|
|
alias WandererApp.Map.Operations.Connections
|
|
require Logger
|
|
|
|
@spec list_systems(String.t()) :: [map()]
|
|
def list_systems(map_id) do
|
|
with {:ok, systems} <- MapSystemRepo.get_visible_by_map(map_id) do
|
|
systems
|
|
else
|
|
_ -> []
|
|
end
|
|
end
|
|
|
|
@spec get_system(String.t(), integer()) :: {:ok, map()} | {:error, :not_found}
|
|
def get_system(map_id, system_id) do
|
|
MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id)
|
|
end
|
|
|
|
@spec create_system(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
|
def create_system(
|
|
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
|
_conn,
|
|
params
|
|
) do
|
|
do_create_system(map_id, user_id, char_id, params)
|
|
end
|
|
|
|
def create_system(_conn, _params), do: {:error, :missing_params}
|
|
|
|
# Private helper for batch upsert
|
|
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
|
with {:ok, solar_system_id} <- fetch_system_id(params) do
|
|
# Default to true so re-submitting with new position updates the system
|
|
update_existing = fetch_update_existing(params, true)
|
|
|
|
map_id
|
|
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|
|
|> case do
|
|
{:ok, _location} ->
|
|
do_create_system(map_id, user_id, char_id, params)
|
|
|
|
{:error, :already_exists} ->
|
|
if update_existing do
|
|
# Mark as skip so it counts as "updated" not "created"
|
|
case do_update_system(map_id, user_id, char_id, solar_system_id, params) do
|
|
{:ok, _} -> {:skip, :updated}
|
|
error -> error
|
|
end
|
|
else
|
|
{:skip, :already_exists}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp do_create_system(map_id, user_id, char_id, params) do
|
|
with {:ok, system_id} <- fetch_system_id(params),
|
|
update_existing <- fetch_update_existing(params, false),
|
|
coords <- normalize_coordinates(params),
|
|
:ok <-
|
|
Server.add_system(
|
|
map_id,
|
|
%{solar_system_id: system_id, coordinates: coords, extra: params},
|
|
user_id,
|
|
char_id,
|
|
update_existing: update_existing
|
|
) do
|
|
# System creation is async, but if add_system returns :ok,
|
|
# it means the operation was queued successfully
|
|
{:ok, %{solar_system_id: system_id}}
|
|
else
|
|
{:error, reason} when is_binary(reason) ->
|
|
Logger.warning("[do_create_system] Expected error: #{inspect(reason)}")
|
|
{:error, :expected_error}
|
|
|
|
error ->
|
|
Logger.error("[do_create_system] Unexpected error: #{inspect(error)}")
|
|
{:error, :unexpected_error}
|
|
end
|
|
end
|
|
|
|
@spec update_system(Plug.Conn.t(), integer(), map()) :: {:ok, map()} | {:error, atom()}
|
|
def update_system(
|
|
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
|
_conn,
|
|
solar_system_id,
|
|
attrs
|
|
) do
|
|
do_update_system(map_id, user_id, char_id, solar_system_id, attrs)
|
|
end
|
|
|
|
def update_system(_conn, _solar_system_id, _attrs), do: {:error, :missing_params}
|
|
|
|
defp do_update_system(map_id, _user_id, _char_id, solar_system_id, params) do
|
|
with {:ok, current} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id),
|
|
x_raw <- Map.get(params, "position_x", Map.get(params, :position_x, current.position_x)),
|
|
y_raw <- Map.get(params, "position_y", Map.get(params, :position_y, current.position_y)),
|
|
{:ok, x} <- parse_int(x_raw, "position_x"),
|
|
{:ok, y} <- parse_int(y_raw, "position_y"),
|
|
coords = %{x: x, y: y},
|
|
:ok <- apply_system_updates(map_id, solar_system_id, params, coords),
|
|
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
|
|
{:ok, system}
|
|
else
|
|
{:error, reason} when is_binary(reason) ->
|
|
Logger.warning("[update_system] Expected error: #{inspect(reason)}")
|
|
{:error, :expected_error}
|
|
|
|
error ->
|
|
Logger.error("[update_system] Unexpected error: #{inspect(error)}")
|
|
{:error, :unexpected_error}
|
|
end
|
|
end
|
|
|
|
@spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()}
|
|
def delete_system(
|
|
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
|
_conn,
|
|
system_id
|
|
) do
|
|
with {:ok, _} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
|
:ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do
|
|
{:ok, 1}
|
|
else
|
|
{:error, :not_found} ->
|
|
Logger.warning("[delete_system] System not found: #{inspect(system_id)}")
|
|
{:error, :not_found}
|
|
|
|
_ ->
|
|
Logger.error("[delete_system] Unexpected error")
|
|
{:error, :unexpected_error}
|
|
end
|
|
end
|
|
|
|
def delete_system(_conn, _system_id), do: {:error, :missing_params}
|
|
|
|
@spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) ::
|
|
{:ok, map()} | {:error, atom()}
|
|
def upsert_systems_and_connections(
|
|
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn,
|
|
systems,
|
|
connections
|
|
) do
|
|
assigns = %{map_id: map_id, user_id: user_id, char_id: char_id}
|
|
|
|
{created_s, updated_s, _skipped_s} =
|
|
upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0)
|
|
|
|
conn_results =
|
|
connections
|
|
|> Enum.reduce(%{created: 0, updated: 0, skipped: 0}, fn conn_data, acc ->
|
|
case Connections.upsert_single(conn, conn_data) do
|
|
{:ok, :created} -> %{acc | created: acc.created + 1}
|
|
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
|
|
_ -> %{acc | skipped: acc.skipped + 1}
|
|
end
|
|
end)
|
|
|
|
{:ok,
|
|
%{
|
|
systems: %{created: created_s, updated: updated_s},
|
|
connections: %{created: conn_results.created, updated: conn_results.updated}
|
|
}}
|
|
end
|
|
|
|
def upsert_systems_and_connections(_conn, _systems, _connections), do: {:error, :missing_params}
|
|
|
|
# -- Internal Helpers -------------------------------------------------------
|
|
|
|
defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id")
|
|
|
|
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id),
|
|
do: parse_int(id, "solar_system_id")
|
|
|
|
defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"}
|
|
|
|
defp fetch_update_existing(%{"update_existing" => update_existing}, _default),
|
|
do: update_existing
|
|
|
|
defp fetch_update_existing(%{update_existing: update_existing}, _default)
|
|
when not is_nil(update_existing),
|
|
do: update_existing
|
|
|
|
defp fetch_update_existing(_, default), do: default
|
|
|
|
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
|
|
|
defp parse_int(val, _field) when is_float(val), do: {:ok, trunc(val)}
|
|
|
|
defp parse_int(val, field) when is_binary(val) do
|
|
case Integer.parse(val) do
|
|
{i, _} -> {:ok, i}
|
|
_ -> {:error, "Invalid #{field}: #{val}"}
|
|
end
|
|
end
|
|
|
|
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
|
|
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
|
|
|
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}})
|
|
when is_number(x) and is_number(y),
|
|
do: %{"x" => x, "y" => y}
|
|
|
|
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
|
|
do: %{"x" => x, "y" => y}
|
|
|
|
defp normalize_coordinates(params) do
|
|
x = params |> Map.get("position_x", Map.get(params, :position_x))
|
|
y = params |> Map.get("position_y", Map.get(params, :position_y))
|
|
|
|
# Only return coordinates if both x and y are provided
|
|
# Otherwise return nil to let the server use auto-positioning
|
|
if is_number(x) and is_number(y) do
|
|
%{"x" => x, "y" => y}
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
|
with :ok <-
|
|
Server.update_system_position(map_id, %{
|
|
solar_system_id: system_id,
|
|
position_x: round(x),
|
|
position_y: round(y)
|
|
}) do
|
|
attrs
|
|
|> Map.drop([
|
|
:coordinates,
|
|
:position_x,
|
|
:position_y,
|
|
:solar_system_id,
|
|
"coordinates",
|
|
"position_x",
|
|
"position_y",
|
|
"solar_system_id"
|
|
])
|
|
|> Enum.reduce_while(:ok, fn {key, val}, _ ->
|
|
case update_system_field(map_id, system_id, to_string(key), val) do
|
|
:ok -> {:cont, :ok}
|
|
err -> {:halt, err}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
defp update_system_field(map_id, system_id, field, val) do
|
|
case field do
|
|
"status" ->
|
|
Server.update_system_status(map_id, %{
|
|
solar_system_id: system_id,
|
|
status: convert_status(val)
|
|
})
|
|
|
|
"description" ->
|
|
Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
|
|
|
|
"tag" ->
|
|
Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
|
|
|
|
"locked" ->
|
|
bool = val in [true, "true", 1, "1"]
|
|
Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool})
|
|
|
|
f when f in ["label", "labels"] ->
|
|
labels =
|
|
cond do
|
|
is_list(val) -> val
|
|
is_binary(val) -> String.split(val, ",", trim: true)
|
|
true -> []
|
|
end
|
|
|
|
Server.update_system_labels(map_id, %{
|
|
solar_system_id: system_id,
|
|
labels: Enum.join(labels, ",")
|
|
})
|
|
|
|
"custom_name" ->
|
|
Server.update_system_custom_name(map_id, %{
|
|
solar_system_id: system_id,
|
|
custom_name: val
|
|
})
|
|
|
|
"temporary_name" ->
|
|
Server.update_system_temporary_name(map_id, %{
|
|
solar_system_id: system_id,
|
|
temporary_name: val
|
|
})
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp convert_status("CLEAR"), do: 0
|
|
defp convert_status("DANGEROUS"), do: 1
|
|
defp convert_status("OCCUPIED"), do: 2
|
|
defp convert_status("MASS_CRITICAL"), do: 3
|
|
defp convert_status("TIME_CRITICAL"), do: 4
|
|
defp convert_status("REINFORCED"), do: 5
|
|
defp convert_status(i) when is_integer(i), do: i
|
|
|
|
defp convert_status(s) when is_binary(s) do
|
|
case Integer.parse(s) do
|
|
{i, _} -> i
|
|
_ -> 0
|
|
end
|
|
end
|
|
|
|
defp convert_status(_), do: 0
|
|
|
|
defp upsert_each([], _fun, c, u, d), do: {c, u, d}
|
|
|
|
defp upsert_each([item | rest], fun, c, u, d) do
|
|
case fun.(item) do
|
|
{:ok, _} -> upsert_each(rest, fun, c + 1, u, d)
|
|
:ok -> upsert_each(rest, fun, c + 1, u, d)
|
|
{:skip, _} -> upsert_each(rest, fun, c, u + 1, d)
|
|
_ -> upsert_each(rest, fun, c, u, d + 1)
|
|
end
|
|
end
|
|
end
|