Files
wanderer/lib/wanderer_app/map/server/map_server_connections_impl.ex
2025-05-13 12:26:58 +02:00

592 lines
17 KiB
Elixir

defmodule WandererApp.Map.Server.ConnectionsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
# @ccp1 -1
@c1 1
@c2 2
@c3 3
@c4 4
@c5 5
@c6 6
@hs 7
@ls 8
@ns 9
# @ccp2 10
# @ccp3 11
@thera 12
@c13 13
@sentinel 14
@barbican 15
@vidette 16
@conflux 17
@redoubt 18
@a1 19
@a2 20
@a3 21
@a4 22
@a5 23
@ccp4 24
# @pochven 25
# @zarzakh 10100
@jita 30_000_142
@wh_space [
@c1,
@c2,
@c3,
@c4,
@c5,
@c6,
@c13,
@thera,
@sentinel,
@barbican,
@vidette,
@conflux,
@redoubt
]
@known_space [@hs, @ls, @ns]
@prohibited_systems [@jita]
@prohibited_system_classes [
@a1,
@a2,
@a3,
@a4,
@a5,
@ccp4
]
# this class of systems will guaranty that no one real class will take that place
# @unknown 100_100
#
@connection_time_status_eol 1
@connection_type_wormhole 0
@connection_type_stargate 1
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
def get_connection_auto_eol_hours(), do: WandererApp.Env.map_connection_auto_eol_hours()
def get_eol_expire_timeout_mins(), do: WandererApp.Env.map_connection_eol_expire_timeout_mins()
def get_eol_expire_timeout(),
do:
:timer.hours(get_connection_auto_expire_hours() - get_connection_auto_eol_hours()) +
:timer.minutes(get_eol_expire_timeout_mins())
def get_connection_expire_timeout(),
do:
:timer.hours(get_connection_auto_expire_hours()) +
:timer.minutes(get_eol_expire_timeout_mins())
def init_eol_cache(map_id, connections_eol_time) do
connections_eol_time
|> Enum.each(fn {connection_id, connection_eol_time} ->
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
connection_eol_time
)
end)
end
def init_start_cache(map_id, connections_start_time) when not is_nil(connections_start_time) do
connections_start_time
|> Enum.each(fn {connection_id, start_time} ->
set_start_time(map_id, connection_id, start_time)
end)
end
def init_start_cache(_map_id, _connections_start_time), do: :ok
def add_connection(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
character_id: character_id
} = _connection_info
) do
:ok =
maybe_add_connection(
map_id,
%{solar_system_id: solar_system_target_id},
%{
solar_system_id: solar_system_source_id
},
character_id,
true
)
state
end
def delete_connection(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = _connection_info
) do
:ok =
maybe_remove_connection(map_id, %{solar_system_id: solar_system_target_id}, %{
solar_system_id: solar_system_source_id
})
state
end
def get_connection_info(
%{map_id: map_id} = _state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = _connection_info
) do
WandererApp.Map.find_connection(
map_id,
solar_system_source_id,
solar_system_target_id
)
|> case do
{:ok, %{id: connection_id}} ->
connection_mark_eol_time = get_connection_mark_eol_time(map_id, connection_id, nil)
{:ok, %{marl_eol_time: connection_mark_eol_time}}
_ ->
{:error, :not_found}
end
end
def update_connection_time_status(
%{map_id: map_id} = state,
connection_update
),
do:
update_connection(state, :update_time_status, [:time_status], connection_update, fn
%{time_status: old_time_status}, %{id: connection_id, time_status: time_status} ->
case time_status == @connection_time_status_eol do
true ->
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
DateTime.utc_now()
)
_ ->
if old_time_status == @connection_time_status_eol do
WandererApp.Cache.delete("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
set_start_time(map_id, connection_id, DateTime.utc_now())
end
end
end)
def update_connection_type(
state,
connection_update
),
do: update_connection(state, :update_type, [:type], connection_update)
def update_connection_mass_status(
state,
connection_update
),
do: update_connection(state, :update_mass_status, [:mass_status], connection_update)
def update_connection_ship_size_type(
state,
connection_update
),
do: update_connection(state, :update_ship_size_type, [:ship_size_type], connection_update)
def update_connection_locked(
state,
connection_update
),
do: update_connection(state, :update_locked, [:locked], connection_update)
def update_connection_custom_info(
state,
connection_update
),
do: update_connection(state, :update_custom_info, [:custom_info], connection_update)
def cleanup_connections(%{map_id: map_id} = state) do
connection_auto_expire_hours = get_connection_auto_expire_hours()
connection_auto_eol_hours = get_connection_auto_eol_hours()
connection_eol_expire_timeout_hours = get_eol_expire_timeout_mins() / 60
state =
map_id
|> WandererApp.Map.list_connections!()
|> Enum.filter(fn %{
id: connection_id,
inserted_at: inserted_at,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
type: type
} ->
connection_start_time = get_start_time(map_id, connection_id)
type != @connection_type_stargate &&
DateTime.diff(DateTime.utc_now(), connection_start_time, :hour) >=
connection_auto_eol_hours &&
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
end)
|> Enum.reduce(state, fn %{
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id
},
state ->
state
|> update_connection_time_status(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
time_status: @connection_time_status_eol
})
end)
state =
map_id
|> WandererApp.Map.list_connections!()
|> Enum.filter(fn %{
id: connection_id,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
type: type
} ->
connection_mark_eol_time =
get_connection_mark_eol_time(map_id, connection_id)
reverse_connection =
WandererApp.Map.get_connection(
map_id,
solar_system_target_id,
solar_system_source_id
)
is_connection_exist =
is_connection_exist(
map_id,
solar_system_source_id,
solar_system_target_id
) || not is_nil(reverse_connection)
is_connection_valid =
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
not is_connection_exist ||
(type != @connection_type_stargate && is_connection_valid &&
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
connection_auto_expire_hours - connection_auto_eol_hours +
+connection_eol_expire_timeout_hours)
end)
|> Enum.reduce(state, fn %{
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id
},
state ->
state
|> delete_connection(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
})
end)
state
end
def maybe_add_connection(map_id, location, old_location, character_id, is_manual)
when not is_nil(location) and not is_nil(old_location) and
not is_nil(old_location.solar_system_id) and
location.solar_system_id != old_location.solar_system_id do
if not is_manual do
character_id
|> WandererApp.Character.get_character!()
|> case do
nil ->
:ok
character ->
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
end
case WandererApp.Map.check_connection(map_id, location, old_location) do
:ok ->
connection_type =
is_connection_valid(
:stargates,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
@connection_type_stargate
_ ->
@connection_type_wormhole
end
{:ok, connection} =
WandererApp.MapConnectionRepo.create(%{
map_id: map_id,
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id,
type: connection_type
})
if connection_type == @connection_type_wormhole do
set_start_time(map_id, connection.id, DateTime.utc_now())
end
WandererApp.Map.add_connection(map_id, connection)
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
Impl.broadcast!(map_id, :add_connection, connection)
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
character_id: character_id,
user_id: character.user_id,
map_id: map_id,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
Impl.broadcast!(map_id, :maybe_link_signature, %{
character_id: character_id,
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id
})
:ok
{:error, :already_exists} ->
# Still broadcast location change in case of followed character
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
end
end
def maybe_add_connection(_map_id, _location, _old_location, _character_id, _is_manual), do: :ok
def get_start_time(map_id, connection_id) do
case WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:start_time") do
nil ->
set_start_time(map_id, connection_id, DateTime.utc_now())
DateTime.utc_now()
value ->
value
end
end
def set_start_time(map_id, connection_id, start_time) do
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:start_time",
start_time
)
end
def can_add_location(_scope, nil), do: false
def can_add_location(:none, _solar_system_id), do: false
def can_add_location(scope, solar_system_id) do
{:ok, system_static_info} = get_system_static_info(solar_system_id)
case scope do
:wormholes ->
not is_prohibited_system_class?(system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
@wh_space |> Enum.member?(system_static_info.system_class)
:stargates ->
not is_prohibited_system_class?(system_static_info.system_class) and
@known_space |> Enum.member?(system_static_info.system_class)
:all ->
not is_prohibited_system_class?(system_static_info.system_class)
_ ->
false
end
end
def is_prohibited_system_class?(system_class) do
@prohibited_system_classes |> Enum.member?(system_class)
end
def is_connection_exist(map_id, from_solar_system_id, to_solar_system_id),
do:
not is_nil(
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: from_solar_system_id}
)
) &&
not is_nil(
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: to_solar_system_id}
)
)
def is_connection_valid(:all, _from_solar_system_id, _to_solar_system_id), do: true
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
{:ok, known_jumps} =
WandererApp.Api.MapSolarSystemJumps.find(%{
before_system_id: from_solar_system_id,
current_system_id: to_solar_system_id
})
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?()
:stargates ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
end
end
def is_connection_valid(_scope, _from_solar_system_id, _to_solar_system_id), do: false
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|> case do
nil ->
default
value ->
value
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
{:ok, system_static_info}
_ ->
{:ok, %{system_class: nil}}
end
end
defp maybe_remove_connection(map_id, location, old_location)
when not is_nil(location) and not is_nil(old_location) and
location.solar_system_id != old_location.solar_system_id do
case WandererApp.Map.find_connection(
map_id,
location.solar_system_id,
old_location.solar_system_id
) do
{:ok, connection} when not is_nil(connection) ->
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
Impl.broadcast!(map_id, :remove_connections, [connection])
map_id |> WandererApp.Map.remove_connection(connection)
WandererApp.Cache.delete("map_#{map_id}:conn_#{connection.id}:start_time")
_error ->
:ok
end
end
defp maybe_remove_connection(_map_id, _location, _old_location), do: :ok
defp update_connection(
%{map_id: map_id} = state,
update_method,
attributes,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = update,
callback_fn \\ nil
) do
with {:ok, connection} <-
WandererApp.Map.find_connection(
map_id,
solar_system_source_id,
solar_system_target_id
),
{:ok, update_map} <- Impl.get_update_map(update, attributes),
{:ok, updated_connection} <-
apply(WandererApp.MapConnectionRepo, update_method, [
connection,
update_map
]),
:ok <-
WandererApp.Map.update_connection(
map_id,
connection |> Map.merge(update_map)
) do
if not is_nil(callback_fn) do
callback_fn.(connection, updated_connection)
end
Impl.broadcast!(map_id, :update_connection, updated_connection)
state
else
{:error, error} ->
Logger.error("Failed to update connection: #{inspect(error, pretty: true)}")
state
end
end
end