mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-14 10:50:20 +00:00
Compare commits
26 Commits
v1.90.1
...
advent-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1226b6abf3 | ||
|
|
7a1f5c0966 | ||
|
|
7039ced11e | ||
|
|
42b5bb337f | ||
|
|
1dbb24f6ec | ||
|
|
c242f510e0 | ||
|
|
c59d51636e | ||
|
|
c5a8aa1b4d | ||
|
|
cba050a9e7 | ||
|
|
59fcbef3b1 | ||
|
|
2f1eb6eeaa | ||
|
|
71ae326cf7 | ||
|
|
07829caf0f | ||
|
|
a5850b5a8d | ||
|
|
9f6849209b | ||
|
|
7bd295cbad | ||
|
|
078e5fc19e | ||
|
|
3877e121c3 | ||
|
|
dcb2a0cdb2 | ||
|
|
f5294eee84 | ||
|
|
a5c87b6fa4 | ||
|
|
eae275f515 | ||
|
|
68ae6706dd | ||
|
|
a34b30af15 | ||
|
|
38b49266ed | ||
|
|
049884bb4c |
63
CHANGELOG.md
63
CHANGELOG.md
@@ -2,6 +2,69 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: skip systems or connections cleanup for not started maps
|
||||
|
||||
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed scopes
|
||||
|
||||
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes & signatures clean up behaviour
|
||||
|
||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added pagination for long ACL lists
|
||||
|
||||
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added system position updates to SSE
|
||||
|
||||
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
};
|
||||
|
||||
refreshZone.addEventListener('click', handleUpdate);
|
||||
refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
// refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
|
||||
this.updated();
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/static/images/news/2025/12-18-advent-giveaway/cover.jpg
Normal file
BIN
assets/static/images/news/2025/12-18-advent-giveaway/cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -98,8 +98,8 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"id" => payload["system_id"] || payload[:system_id],
|
||||
"attributes" => %{
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"x" => payload["x"] || payload[:x],
|
||||
"y" => payload["y"] || payload[:y],
|
||||
"position_x" => payload["position_x"] || payload[:position_x],
|
||||
"position_y" => payload["position_y"] || payload[:position_y],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule WandererApp.Map do
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
scope: :none,
|
||||
scopes: nil,
|
||||
owner_id: nil,
|
||||
characters: [],
|
||||
systems: Map.new(),
|
||||
@@ -22,11 +23,15 @@ defmodule WandererApp.Map do
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} = input) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
|
||||
map =
|
||||
struct!(__MODULE__,
|
||||
map_id: map_id,
|
||||
scope: scope,
|
||||
scopes: scopes,
|
||||
owner_id: owner_id,
|
||||
name: name,
|
||||
acls: acls,
|
||||
|
||||
@@ -822,15 +822,25 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
) do
|
||||
scopes = get_effective_scopes(map)
|
||||
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
is_valid =
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
|
||||
Logger.debug(
|
||||
"[CharacterTracking] update_location: map=#{map_id}, " <>
|
||||
"from=#{old_location.solar_system_id}, to=#{location.solar_system_id}, " <>
|
||||
"scopes=#{inspect(scopes)}, map.scopes=#{inspect(map[:scopes])}, " <>
|
||||
"map.scope=#{inspect(map[:scope])}, is_valid=#{is_valid}"
|
||||
)
|
||||
|> case do
|
||||
|
||||
case is_valid do
|
||||
true ->
|
||||
# Add new location system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts) do
|
||||
# Connection is valid (at least one system matches scopes)
|
||||
# Add systems that match the map's scopes - individual system filtering by maybe_add_system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -840,8 +850,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
end
|
||||
|
||||
# Add old location system (in case it wasn't on map)
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts) do
|
||||
# Add old location system (in case it wasn't on map) - only if it matches scopes
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -881,13 +891,16 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
|
||||
do: is_nil(structure_id) && is_nil(station_id)
|
||||
|
||||
# Get effective scopes from map, with fallback to legacy scope
|
||||
defp get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
@doc """
|
||||
Get effective scopes from map, with fallback to legacy scope.
|
||||
Returns the scopes array that should be used for filtering.
|
||||
"""
|
||||
def get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
|
||||
defp get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
def get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
do: legacy_scope_to_scopes(scope)
|
||||
|
||||
defp get_effective_scopes(_), do: [:wormholes]
|
||||
def get_effective_scopes(_), do: [:wormholes]
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
|
||||
@@ -296,6 +296,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
do: update_connection(map_id, :update_custom_info, [:custom_info], connection_update)
|
||||
|
||||
def cleanup_connections(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting connections when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_connections] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_connections, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_connections(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_connections(map_id) 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
|
||||
@@ -744,15 +768,33 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
when is_list(scopes) and from_solar_system_id != to_solar_system_id do
|
||||
with {: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) do
|
||||
# Connection is valid if:
|
||||
# 1. Neither system is prohibited
|
||||
# 2. At least one system matches one of the selected scopes
|
||||
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
|
||||
(system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes))
|
||||
# First check: neither system is prohibited
|
||||
not_prohibited =
|
||||
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))
|
||||
|
||||
if not_prohibited do
|
||||
from_is_wormhole = from_system_static_info.system_class in @wh_space
|
||||
to_is_wormhole = to_system_static_info.system_class in @wh_space
|
||||
wormholes_enabled = :wormholes in scopes
|
||||
|
||||
# Wormhole border behavior: if wormholes scope is enabled AND at least one
|
||||
# system is a wormhole, allow the connection (adds border k-space systems)
|
||||
# Otherwise: BOTH systems must match the configured scopes
|
||||
if wormholes_enabled and (from_is_wormhole or to_is_wormhole) do
|
||||
# At least one system matches (wormhole matches :wormholes, or other matches its scope)
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
else
|
||||
# Non-wormhole movement: both systems must match scopes
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@@ -256,6 +256,37 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
@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,
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
|
||||
@ddrt Application.compile_env(:wanderer_app, :ddrt)
|
||||
@system_auto_expire_minutes 15
|
||||
@@ -146,6 +147,30 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
|
||||
def cleanup_systems(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting systems when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_systems] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_systems, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_systems(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_systems(map_id) do
|
||||
expired_systems =
|
||||
map_id
|
||||
|> WandererApp.Map.list_systems!()
|
||||
@@ -403,64 +428,77 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
# When destination systems are deleted, unlink signatures instead of destroying them.
|
||||
# This preserves the user's scan data while removing the stale link.
|
||||
defp cleanup_linked_signatures(map_id, removed_solar_system_ids) do
|
||||
removed_solar_system_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq_by(& &1.system_id)
|
||||
|> Enum.each(fn s ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
|
||||
# Group signatures by their source system for efficient broadcasting
|
||||
signatures_by_system =
|
||||
removed_solar_system_ids
|
||||
|> Enum.flat_map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|> Enum.group_by(fn sig -> sig.system_id end)
|
||||
|
||||
# Use Ash.destroy (not destroy!) to handle already-deleted signatures gracefully
|
||||
case Ash.destroy(s) do
|
||||
:ok ->
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
|
||||
end)
|
||||
signatures_by_system
|
||||
|> Enum.each(fn {_system_id, signatures} ->
|
||||
signatures
|
||||
|> Enum.each(fn sig ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = sig |> Ash.load([:system])
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
|
||||
end)
|
||||
# Clear the linked_system_id instead of destroying the signature
|
||||
# Use the wrapper to log unlink operations
|
||||
case SignaturesImpl.update_signature_linked_system(sig, %{
|
||||
linked_system_id: nil
|
||||
}) do
|
||||
{:ok, _updated_sig} ->
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} unlinked (parent system already deleted)"
|
||||
end)
|
||||
|
||||
# Audit logging for cascade deletion (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: [eve_id]
|
||||
})
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] unlinked signature #{eve_id} in system #{solar_system_id}"
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
end
|
||||
# Audit logging for cascade unlink (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_unlinked, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: [eve_id]
|
||||
})
|
||||
end
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} ->
|
||||
# Check if this is a StaleRecord error (signature already deleted)
|
||||
if Enum.any?(errors, &match?(%Ash.Error.Changes.StaleRecord{}, &1)) do
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} already deleted (StaleRecord)"
|
||||
end)
|
||||
else
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to destroy signature #{eve_id}: #{inspect(errors)}"
|
||||
"[cleanup_linked_signatures] Failed to unlink signature #{sig.eve_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to destroy signature: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end)
|
||||
|
||||
# Broadcast once per source system after all its signatures are processed
|
||||
case List.first(signatures) do
|
||||
%{system: %{solar_system_id: solar_system_id}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
# Try to get the system info if not preloaded
|
||||
case List.first(signatures) |> Ash.load([:system]) do
|
||||
{:ok, %{system: %{solar_system_id: solar_system_id}}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -485,8 +523,47 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts)
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes \\ nil)
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes)
|
||||
when not is_nil(location) do
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
|
||||
# Check if the system matches the map's configured scopes before adding
|
||||
should_add =
|
||||
case scopes do
|
||||
nil ->
|
||||
true
|
||||
|
||||
[] ->
|
||||
true
|
||||
|
||||
scopes when is_list(scopes) ->
|
||||
# First check: does the location directly match scopes?
|
||||
if ConnectionsImpl.can_add_location(scopes, location.solar_system_id) do
|
||||
true
|
||||
else
|
||||
# Second check: wormhole border behavior
|
||||
# If :wormholes scope is enabled AND old_location is a wormhole,
|
||||
# allow this system to be added as a border system (so you can see
|
||||
# where your wormhole exits to)
|
||||
:wormholes in scopes and
|
||||
not is_nil(old_location) and
|
||||
ConnectionsImpl.can_add_location([:wormholes], old_location.solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
if should_add do
|
||||
do_add_system_from_location(map_id, location, old_location, map_opts)
|
||||
else
|
||||
# System filtered out by scope settings - this is expected behavior
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts, _scopes), do: :ok
|
||||
|
||||
defp do_add_system_from_location(map_id, location, old_location, map_opts) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :system_addition, :start],
|
||||
%{system_time: System.system_time()},
|
||||
@@ -694,8 +771,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts), do: :ok
|
||||
|
||||
defp do_add_system(
|
||||
map_id,
|
||||
%{
|
||||
@@ -1029,12 +1104,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
# This may fail if the relay is not available (e.g., in tests), which is fine
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :system_metadata_changed, %{
|
||||
system_id: updated_system.id,
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
name: updated_system.name,
|
||||
temporary_name: updated_system.temporary_name,
|
||||
labels: updated_system.labels,
|
||||
description: updated_system.description,
|
||||
status: updated_system.status
|
||||
status: updated_system.status,
|
||||
locked: updated_system.locked,
|
||||
position_x: updated_system.position_x,
|
||||
position_y: updated_system.position_y
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
@@ -23,6 +23,7 @@ defmodule WandererAppWeb.Layouts do
|
||||
|
||||
attr :app_version, :string
|
||||
attr :enabled, :boolean
|
||||
attr :latest_post, :any, default: nil
|
||||
|
||||
def new_version_banner(assigns) do
|
||||
~H"""
|
||||
@@ -36,27 +37,89 @@ defmodule WandererAppWeb.Layouts do
|
||||
>
|
||||
<div class="hs-overlay-backdrop transition duration absolute left-0 top-0 w-full h-full bg-gray-900 bg-opacity-50 dark:bg-opacity-80 dark:bg-neutral-900">
|
||||
</div>
|
||||
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex items-center">
|
||||
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
|
||||
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
|
||||
</div>
|
||||
<div id="refresh-area">
|
||||
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
|
||||
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-6">
|
||||
<div class="flex items-center">
|
||||
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
|
||||
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
|
||||
</div>
|
||||
<div id="refresh-area">
|
||||
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
|
||||
</div>
|
||||
|
||||
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
|
||||
<div class=" flex items-center w-[200px] h-full">
|
||||
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
|
||||
<div class=" flex flex-col items-center justify-center h-full">
|
||||
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Update Required
|
||||
</div>
|
||||
<a
|
||||
href="/changelog"
|
||||
target="_blank"
|
||||
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
>
|
||||
What's new?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
|
||||
<div class=" flex items-center w-[200px] h-full">
|
||||
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
|
||||
<div class=" flex flex-col items-center justify-center h-full">
|
||||
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Update Required
|
||||
<div class="flex flex-row gap-6 z-20">
|
||||
<div
|
||||
:if={@latest_post}
|
||||
class="bg-gray-800/80 rounded-lg overflow-hidden min-w-[300px] backdrop-blur-sm border border-gray-700"
|
||||
>
|
||||
<a href={"/news/#{@latest_post.id}"} target="_blank" class="block group/post">
|
||||
<div class="relative">
|
||||
<img
|
||||
src={@latest_post.cover_image_uri}
|
||||
class="w-[300px] h-[140px] object-cover opacity-80 group-hover/post:opacity-100 transition-opacity"
|
||||
/>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black/70">
|
||||
</div>
|
||||
<div class="absolute top-2 left-2 flex items-center gap-1 bg-orange-500/90 px-2 py-0.5 rounded text-xs font-semibold">
|
||||
<.icon name="hero-newspaper-solid" class="w-3 h-3" />
|
||||
<span>Latest News</span>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 w-full p-3">
|
||||
<% [first_part | rest] = String.split(@latest_post.title, ":", parts: 2) %>
|
||||
<h3 class="text-white text-sm font-bold ccp-font [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
{first_part}
|
||||
</h3>
|
||||
<p
|
||||
:if={rest != []}
|
||||
class="text-gray-200 text-xs ccp-font text-ellipsis overflow-hidden whitespace-nowrap [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
>
|
||||
{List.first(rest)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800/80 rounded-lg p-4 min-w-[280px] backdrop-blur-sm border border-gray-700">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<.icon name="hero-gift-solid" class="w-5 h-5 text-green-400" />
|
||||
<span class="text-white font-semibold text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Support Wanderer
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-300 text-xs mb-3 [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Buy PLEX from the official EVE Online store using our promocode to support the development.
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<code class="bg-gray-900/60 px-2 py-1 rounded text-green-400 text-sm font-mono border border-gray-600">
|
||||
WANDERER
|
||||
</code>
|
||||
<a
|
||||
href="/changelog"
|
||||
href="https://www.eveonline.com/plex"
|
||||
target="_blank"
|
||||
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-sm text-green-400 hover:text-green-300 transition-colors"
|
||||
>
|
||||
What's new?
|
||||
<span>Get PLEX</span>
|
||||
<.icon name="hero-arrow-top-right-on-square-mini" class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
|
||||
<.new_version_banner
|
||||
app_version={@app_version}
|
||||
enabled={true}
|
||||
latest_post={@latest_post}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />
|
||||
|
||||
@@ -455,7 +455,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}")
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -42,12 +42,18 @@ defmodule WandererAppWeb.AuthController do
|
||||
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Update corporation/alliance data from ESI to ensure access control is current
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok, character} = WandererApp.Api.Character.create(character_data)
|
||||
:telemetry.execute([:wanderer_app, :user, :character, :registered], %{count: 1})
|
||||
|
||||
# Fetch initial corporation/alliance data for new characters
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
end
|
||||
|
||||
@@ -113,4 +119,102 @@ defmodule WandererAppWeb.AuthController do
|
||||
end
|
||||
|
||||
def maybe_update_character_user_id(_character, _user_id), do: :ok
|
||||
|
||||
# Updates character's corporation and alliance data from ESI.
|
||||
# This ensures ACL-based access control uses current corporation membership,
|
||||
# even for characters not actively being tracked on any map.
|
||||
defp update_character_affiliation(%{id: character_id, eve_id: eve_id} = character) do
|
||||
# Run async to not block the SSO callback
|
||||
Task.start(fn ->
|
||||
character_eve_id = eve_id |> String.to_integer()
|
||||
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [affiliation_info]} when is_map(affiliation_info) ->
|
||||
new_corporation_id = Map.get(affiliation_info, "corporation_id")
|
||||
new_alliance_id = Map.get(affiliation_info, "alliance_id")
|
||||
|
||||
# Check if corporation changed
|
||||
corporation_changed = character.corporation_id != new_corporation_id
|
||||
alliance_changed = character.alliance_id != new_alliance_id
|
||||
|
||||
if corporation_changed or alliance_changed do
|
||||
update_affiliation_data(character_id, character, new_corporation_id, new_alliance_id)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"[AuthController] Failed to fetch affiliation for character #{character_id}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_character_affiliation(_character), do: :ok
|
||||
|
||||
defp update_affiliation_data(character_id, character, corporation_id, alliance_id) do
|
||||
# Fetch corporation info
|
||||
corporation_update =
|
||||
case WandererApp.Esi.get_corporation_info(corporation_id) do
|
||||
{:ok, %{"name" => corp_name, "ticker" => corp_ticker}} ->
|
||||
%{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corp_name,
|
||||
corporation_ticker: corp_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{corporation_id: corporation_id}
|
||||
end
|
||||
|
||||
# Fetch alliance info if present
|
||||
alliance_update =
|
||||
case alliance_id do
|
||||
nil ->
|
||||
%{alliance_id: nil, alliance_name: nil, alliance_ticker: nil}
|
||||
|
||||
_ ->
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
|
||||
%{
|
||||
alliance_id: alliance_id,
|
||||
alliance_name: alliance_name,
|
||||
alliance_ticker: alliance_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{alliance_id: alliance_id}
|
||||
end
|
||||
end
|
||||
|
||||
full_update = Map.merge(corporation_update, alliance_update)
|
||||
|
||||
# Update database
|
||||
case character.corporation_id != corporation_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_corporation(character, corporation_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
case character.alliance_id != alliance_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_alliance(character, alliance_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
# Update cache
|
||||
WandererApp.Character.update_character(character_id, full_update)
|
||||
|
||||
Logger.info(
|
||||
"[AuthController] Updated affiliation for character #{character_id}: " <>
|
||||
"corp #{character.corporation_id} -> #{corporation_id}, " <>
|
||||
"alliance #{character.alliance_id} -> #{alliance_id}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
@members_per_page 50
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
{:ok, characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
@@ -25,7 +27,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
user_id: user_id,
|
||||
access_lists: access_lists |> Enum.map(fn acl -> map_ui_acl(acl, nil) end),
|
||||
characters: characters,
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -39,7 +43,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
allow_acl_creation: false,
|
||||
access_lists: [],
|
||||
characters: [],
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -93,10 +99,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(:page_title, "Access Lists - Members")
|
||||
|> assign(:selected_acl_id, acl_id)
|
||||
|> assign(:access_list, access_list)
|
||||
|> assign(
|
||||
:members,
|
||||
members
|
||||
)
|
||||
|> assign(:members, members)
|
||||
|> assign(:members_page, 1)
|
||||
else
|
||||
_ ->
|
||||
socket
|
||||
@@ -324,6 +328,20 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_prev_page", _, socket) do
|
||||
new_page = max(1, socket.assigns.members_page - 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_next_page", _, socket) do
|
||||
total_members = length(socket.assigns.members)
|
||||
max_page = max(1, ceil(total_members / socket.assigns.members_per_page))
|
||||
new_page = min(max_page, socket.assigns.members_page + 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("noop", _, socket) do
|
||||
{:noreply, socket}
|
||||
@@ -719,6 +737,17 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
acl |> Map.put(:selected, acl.id == selected_id)
|
||||
end
|
||||
|
||||
defp paginated_members(members, page, per_page) do
|
||||
members
|
||||
|> Enum.sort_by(&{&1.role, &1.name}, &<=/2)
|
||||
|> Enum.drop((page - 1) * per_page)
|
||||
|> Enum.take(per_page)
|
||||
end
|
||||
|
||||
defp total_pages(members, per_page) do
|
||||
max(1, ceil(length(members) / per_page))
|
||||
end
|
||||
|
||||
# Broadcast ACL update and invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
@@ -742,7 +771,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}")
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,11 +82,14 @@
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-211px)] !overflow-y-auto"
|
||||
class={[
|
||||
"dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-191px)] !overflow-y-auto",
|
||||
classes("!h-[calc(100vh-240px)]": length(@members) > @members_per_page)
|
||||
]}
|
||||
id="acl_members"
|
||||
>
|
||||
<div
|
||||
:for={member <- @members |> Enum.sort_by(&{&1.role, &1.name}, &<=/2)}
|
||||
:for={member <- paginated_members(@members, @members_page, @members_per_page)}
|
||||
draggable="true"
|
||||
id={member.id}
|
||||
class="draggable !p-1 h-10 cursor-move bg-black bg-opacity-25 hover:text-white"
|
||||
@@ -113,10 +116,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :if={length(@members) > @members_per_page} class="flex items-center justify-between px-3 py-2 border-t border-gray-500 bg-black bg-opacity-25">
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@members_page} of {total_pages(@members, @members_per_page)} ({length(@members)} members)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="members_prev_page"
|
||||
disabled={@members_page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="members_next_page"
|
||||
disabled={@members_page >= total_pages(@members, @members_per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page >= total_pages(@members, @members_per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.link
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
class="btn w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
@@ -129,6 +154,7 @@
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -153,10 +179,10 @@
|
||||
placeholder="Select an owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
|
||||
|
||||
<!-- Divider between above inputs and the API key section -->
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="mt-2">
|
||||
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||
|
||||
@@ -363,8 +363,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
|
||||
s
|
||||
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
|
||||
# Use the wrapper to log unlink operations
|
||||
WandererApp.Map.Server.SignaturesImpl.update_signature_linked_system(s, %{
|
||||
linked_system_id: nil
|
||||
})
|
||||
end)
|
||||
|
||||
@@ -16,6 +16,8 @@ defmodule WandererAppWeb.Nav do
|
||||
show_admin =
|
||||
socket.assigns.current_user_role == :admin
|
||||
|
||||
latest_post = WandererApp.Blog.recent_posts(1) |> List.first()
|
||||
|
||||
{:cont,
|
||||
socket
|
||||
|> attach_hook(:active_tab, :handle_params, &set_active_tab/3)
|
||||
@@ -25,7 +27,8 @@ defmodule WandererAppWeb.Nav do
|
||||
show_admin: show_admin,
|
||||
show_sidebar: true,
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
|
||||
app_version: WandererApp.Env.vsn()
|
||||
app_version: WandererApp.Env.vsn(),
|
||||
latest_post: latest_post
|
||||
)}
|
||||
end
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
|
||||
require Logger
|
||||
|
||||
# 1 hour grace period before removing disconnected characters
|
||||
@grace_period_ms :timer.hours(1)
|
||||
# 15 minutes grace period before removing disconnected characters
|
||||
@grace_period_ms :timer.minutes(15)
|
||||
|
||||
defstruct pending_removals: %{}, timers: %{}
|
||||
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.90.1"
|
||||
@version "1.90.8"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
68
priv/posts/2025/12-18-advent-giveaway-challenge.md
Normal file
68
priv/posts/2025/12-18-advent-giveaway-challenge.md
Normal file
@@ -0,0 +1,68 @@
|
||||
%{
|
||||
title: "Christmas Giveaway Challenge",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/2025/12-18-advent-giveaway/cover.jpg",
|
||||
tags: ~w(event giveaway challenge christmas advent partnership),
|
||||
description: "Join our Advent Christmas Giveaway Challenge! Win exclusive partnership codes every day for a week. Be the fastest to claim your reward!"
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
|
||||

|
||||
|
||||
### The Season of Giving
|
||||
|
||||
This holiday season, we're spreading some festive cheer with a special event for our community: the **Advent Christmas Giveaway Challenge**!
|
||||
|
||||
Starting next week, we'll be giving away **1 exclusive partnership code every day for 7 days**. But here's the twist — it's a challenge!
|
||||
|
||||
---
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Daily Giveaway:**
|
||||
- Every day during the event week, a partnership code will be revealed at a specific scheduled time.
|
||||
- The exact reveal time will be announced for each day.
|
||||
|
||||
2. **The Challenge:**
|
||||
- When the code is revealed, it becomes visible to **all participants** at the exact same moment.
|
||||
- **First person to activate the code wins!**
|
||||
- Speed and timing are everything.
|
||||
|
||||
3. **One Code Per Day:**
|
||||
- Each day features a single partnership code.
|
||||
- Miss today? Come back tomorrow for another chance!
|
||||
|
||||
---
|
||||
|
||||
### Event Details
|
||||
|
||||
- **Event Name:** Advent Christmas Giveaway
|
||||
- **Duration:** 1 week (7 days, 7 codes)
|
||||
- **Organizer:** @Demiro (Wanderer core developer, EventCortex CTO)
|
||||
- **Event Link:** [Advent Christmas Giveaway - EventCortex](https://eventcortex.com/events/invite/cYdBywu1ygfVS3UN6ZZcmDzL1q85aDmH)
|
||||
|
||||
---
|
||||
|
||||
### Tips for Participants
|
||||
|
||||
- **Be Ready:** Know the reveal time and be online a few minutes early.
|
||||
- **Stay Alert:** The code appears for everyone simultaneously — every second counts!
|
||||
- **Keep Trying:** Didn't win today? There's always tomorrow's code.
|
||||
|
||||
---
|
||||
|
||||
### Why Participate?
|
||||
|
||||
Partnership codes can be redeemed in EVE Online for **exclusive partnership SKINs** — unique ship skins that let you fly in style! This is your chance to grab one for free — if you're fast enough!
|
||||
|
||||
|
||||
Good luck, and may the fastest capsuleer win!
|
||||
|
||||
---
|
||||
|
||||
Fly safe and happy holidays,
|
||||
**The Wanderer Team**
|
||||
|
||||
---
|
||||
473
test/integration/map/map_scope_filtering_test.exs
Normal file
473
test/integration/map/map_scope_filtering_test.exs
Normal file
@@ -0,0 +1,473 @@
|
||||
defmodule WandererApp.Map.MapScopeFilteringTest do
|
||||
@moduledoc """
|
||||
Integration tests for map scope filtering during character location tracking.
|
||||
|
||||
These tests verify that systems are correctly filtered based on map scope settings
|
||||
when characters move between systems. The key scenarios tested:
|
||||
|
||||
1. Characters moving between systems with [:wormholes, :null] scopes:
|
||||
- Wormhole systems should be added
|
||||
- Null-sec systems should be added
|
||||
- High-sec systems should NOT be added (filtered out)
|
||||
- Low-sec systems should NOT be added (filtered out)
|
||||
|
||||
2. Wormhole border behavior:
|
||||
- When a character jumps from wormhole to k-space, the wormhole should be added
|
||||
- K-space border systems should only be added if they match the scopes
|
||||
|
||||
3. K-space only movement:
|
||||
- Characters moving within k-space should only track systems matching scopes
|
||||
- No "border system" behavior for k-space to k-space movement
|
||||
|
||||
Reference bug: Characters with [:wormholes, :null] scopes were getting
|
||||
high-sec (0.6) and low-sec (0.4) systems added to the map when traveling.
|
||||
"""
|
||||
|
||||
use WandererApp.DataCase
|
||||
|
||||
# System class constants (matching ConnectionsImpl)
|
||||
@c1 1
|
||||
@c2 2
|
||||
@hs 7
|
||||
@ls 8
|
||||
@ns 9
|
||||
|
||||
# Test solar system IDs
|
||||
# C1 wormhole
|
||||
@wh_system_j100001 31_000_001
|
||||
# C2 wormhole
|
||||
@wh_system_j100002 31_000_002
|
||||
# High-sec system (0.6)
|
||||
@hs_system_halenan 30_000_001
|
||||
# High-sec system (0.6)
|
||||
@hs_system_mili 30_000_002
|
||||
# Low-sec system (0.4)
|
||||
@ls_system_halmah 30_000_100
|
||||
# Null-sec system
|
||||
@ns_system_geminate 30_000_200
|
||||
|
||||
setup do
|
||||
# Setup system static info cache with both wormhole and k-space systems
|
||||
setup_scope_test_systems()
|
||||
:ok
|
||||
end
|
||||
|
||||
# Setup system static info for scope testing
|
||||
defp setup_scope_test_systems do
|
||||
test_systems = %{
|
||||
# C1 Wormhole
|
||||
@wh_system_j100001 => %{
|
||||
solar_system_id: @wh_system_j100001,
|
||||
solar_system_name: "J100001",
|
||||
solar_system_name_lc: "j100001",
|
||||
region_id: 11_000_001,
|
||||
constellation_id: 21_000_001,
|
||||
region_name: "A-R00001",
|
||||
constellation_name: "A-C00001",
|
||||
system_class: @c1,
|
||||
security: "-1.0",
|
||||
type_description: "Class 1",
|
||||
class_title: "C1",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: ["H121"],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# C2 Wormhole
|
||||
@wh_system_j100002 => %{
|
||||
solar_system_id: @wh_system_j100002,
|
||||
solar_system_name: "J100002",
|
||||
solar_system_name_lc: "j100002",
|
||||
region_id: 11_000_001,
|
||||
constellation_id: 21_000_001,
|
||||
region_name: "A-R00001",
|
||||
constellation_name: "A-C00001",
|
||||
system_class: @c2,
|
||||
security: "-1.0",
|
||||
type_description: "Class 2",
|
||||
class_title: "C2",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: ["D382", "L005"],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# High-sec system (Halenan 0.6)
|
||||
@hs_system_halenan => %{
|
||||
solar_system_id: @hs_system_halenan,
|
||||
solar_system_name: "Halenan",
|
||||
solar_system_name_lc: "halenan",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @hs,
|
||||
security: "0.6",
|
||||
type_description: "High Security",
|
||||
class_title: "High Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# High-sec system (Mili 0.6)
|
||||
@hs_system_mili => %{
|
||||
solar_system_id: @hs_system_mili,
|
||||
solar_system_name: "Mili",
|
||||
solar_system_name_lc: "mili",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @hs,
|
||||
security: "0.6",
|
||||
type_description: "High Security",
|
||||
class_title: "High Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# Low-sec system (Halmah 0.4)
|
||||
@ls_system_halmah => %{
|
||||
solar_system_id: @ls_system_halmah,
|
||||
solar_system_name: "Halmah",
|
||||
solar_system_name_lc: "halmah",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @ls,
|
||||
security: "0.4",
|
||||
type_description: "Low Security",
|
||||
class_title: "Low Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# Null-sec system
|
||||
@ns_system_geminate => %{
|
||||
solar_system_id: @ns_system_geminate,
|
||||
solar_system_name: "Geminate",
|
||||
solar_system_name_lc: "geminate",
|
||||
region_id: 10_000_029,
|
||||
constellation_id: 20_000_400,
|
||||
region_name: "Geminate",
|
||||
constellation_name: "Geminate",
|
||||
system_class: @ns,
|
||||
security: "-0.5",
|
||||
type_description: "Null Security",
|
||||
class_title: "Null Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
}
|
||||
}
|
||||
|
||||
Enum.each(test_systems, fn {solar_system_id, system_info} ->
|
||||
Cachex.put(:system_static_info_cache, solar_system_id, system_info)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "Scope filtering logic tests" do
|
||||
# These tests verify the filtering logic without full integration
|
||||
# The actual filtering is tested more comprehensively in map_scopes_test.exs
|
||||
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
alias WandererApp.Map.Server.SystemsImpl
|
||||
|
||||
test "can_add_location correctly filters high-sec with [:wormholes, :null] scopes" do
|
||||
# High-sec should NOT be allowed with [:wormholes, :null]
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_halenan),
|
||||
"High-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_mili),
|
||||
"High-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly filters low-sec with [:wormholes, :null] scopes" do
|
||||
# Low-sec should NOT be allowed with [:wormholes, :null]
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @ls_system_halmah),
|
||||
"Low-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly allows wormholes with [:wormholes, :null] scopes" do
|
||||
# Wormholes should be allowed
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100001),
|
||||
"Wormhole should be allowed with [:wormholes, :null] scopes"
|
||||
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100002),
|
||||
"Wormhole should be allowed with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly allows null-sec with [:wormholes, :null] scopes" do
|
||||
# Null-sec should be allowed
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @ns_system_geminate),
|
||||
"Null-sec should be allowed with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "maybe_add_system filters out high-sec when not jumping from wormhole" do
|
||||
# When scopes is [:wormholes, :null] and NOT jumping from wormhole,
|
||||
# high-sec systems should be filtered
|
||||
location = %{solar_system_id: @hs_system_halenan}
|
||||
# old_location is nil (no previous system)
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, nil, [], [:wormholes, :null])
|
||||
assert result == :ok
|
||||
|
||||
# old_location is also high-sec (k-space to k-space)
|
||||
old_location = %{solar_system_id: @hs_system_mili}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "maybe_add_system filters out low-sec when not jumping from wormhole" do
|
||||
location = %{solar_system_id: @ls_system_halmah}
|
||||
# old_location is high-sec (k-space to k-space)
|
||||
old_location = %{solar_system_id: @hs_system_halenan}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "maybe_add_system allows border high-sec when jumping FROM wormhole" do
|
||||
# When jumping FROM a wormhole TO high-sec with :wormholes scope,
|
||||
# the high-sec should be added as a border system
|
||||
location = %{solar_system_id: @hs_system_halenan}
|
||||
old_location = %{solar_system_id: @wh_system_j100001}
|
||||
|
||||
# This should attempt to add the system (not filter it out)
|
||||
# The result will be an error because the map doesn't exist,
|
||||
# but that proves the filtering logic allowed it through
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes])
|
||||
|
||||
# The function attempts to add (returns error because map doesn't exist)
|
||||
# This proves border behavior is working - system was NOT filtered out
|
||||
assert match?({:error, _}, result),
|
||||
"Border system should attempt to be added (error because map doesn't exist)"
|
||||
end
|
||||
|
||||
test "is_connection_valid allows WH to HS with [:wormholes, :null] (border behavior)" do
|
||||
# The connection is valid for border behavior - but individual systems are filtered
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@wh_system_j100001,
|
||||
@hs_system_halenan
|
||||
),
|
||||
"WH to HS connection should be valid (border behavior)"
|
||||
end
|
||||
|
||||
test "is_connection_valid rejects HS to LS with [:wormholes, :null] (no border)" do
|
||||
# HS to LS should be rejected - neither system matches scopes and no wormhole involved
|
||||
refute ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_halenan,
|
||||
@ls_system_halmah
|
||||
),
|
||||
"HS to LS connection should be rejected with [:wormholes, :null]"
|
||||
end
|
||||
|
||||
test "is_connection_valid rejects HS to HS with [:wormholes, :null]" do
|
||||
# HS to HS should be rejected
|
||||
refute ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_halenan,
|
||||
@hs_system_mili
|
||||
),
|
||||
"HS to HS connection should be rejected with [:wormholes, :null]"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_effective_scopes behavior" do
|
||||
alias WandererApp.Map.Server.CharactersImpl
|
||||
|
||||
test "get_effective_scopes returns scopes array when present" do
|
||||
# Create a map struct with scopes array
|
||||
map = %{scopes: [:wormholes, :null]}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes, :null]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :all scope" do
|
||||
map = %{scope: :all}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes, :hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :wormholes scope" do
|
||||
map = %{scope: :wormholes}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :stargates scope" do
|
||||
map = %{scope: :stargates}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :none scope" do
|
||||
map = %{scope: :none}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == []
|
||||
end
|
||||
|
||||
test "get_effective_scopes defaults to [:wormholes] when no scope" do
|
||||
map = %{}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes]
|
||||
end
|
||||
end
|
||||
|
||||
describe "WandererApp.Map struct and new/1 function" do
|
||||
alias WandererApp.Map.Server.CharactersImpl
|
||||
|
||||
test "Map struct includes scopes field" do
|
||||
# Verify the struct has the scopes field
|
||||
map_struct = %WandererApp.Map{}
|
||||
assert Map.has_key?(map_struct, :scopes)
|
||||
assert map_struct.scopes == nil
|
||||
end
|
||||
|
||||
test "Map.new/1 extracts scopes from input" do
|
||||
# Simulate input from database (Ash resource)
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :wormholes,
|
||||
scopes: [:wormholes, :null],
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
|
||||
assert map.map_id == "test-map-id"
|
||||
assert map.name == "Test Map"
|
||||
assert map.scope == :wormholes
|
||||
assert map.scopes == [:wormholes, :null]
|
||||
end
|
||||
|
||||
test "Map.new/1 handles missing scopes (nil)" do
|
||||
# When scopes is not present in input, it should be nil
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :all,
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
|
||||
assert map.map_id == "test-map-id"
|
||||
assert map.scope == :all
|
||||
assert map.scopes == nil
|
||||
end
|
||||
|
||||
test "get_effective_scopes uses scopes field from Map struct when present" do
|
||||
# Create map struct with both scope and scopes
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :all,
|
||||
scopes: [:wormholes, :null],
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
|
||||
# get_effective_scopes should prioritize scopes over scope
|
||||
effective = CharactersImpl.get_effective_scopes(map)
|
||||
assert effective == [:wormholes, :null]
|
||||
end
|
||||
|
||||
test "get_effective_scopes falls back to legacy scope when scopes is nil" do
|
||||
# Create map struct with only legacy scope
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :all,
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
|
||||
# get_effective_scopes should convert legacy :all scope
|
||||
effective = CharactersImpl.get_effective_scopes(map)
|
||||
assert effective == [:wormholes, :hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "get_effective_scopes falls back to legacy scope when scopes is empty list" do
|
||||
# Empty scopes list should fall back to legacy scope
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :stargates,
|
||||
scopes: [],
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
|
||||
# get_effective_scopes should fall back to legacy scope conversion
|
||||
effective = CharactersImpl.get_effective_scopes(map)
|
||||
assert effective == [:hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "Map.new/1 extracts all scope variations correctly" do
|
||||
# Test various scope combinations
|
||||
test_cases = [
|
||||
{[:wormholes], [:wormholes]},
|
||||
{[:hi, :low], [:hi, :low]},
|
||||
{[:wormholes, :hi, :low, :null, :pochven], [:wormholes, :hi, :low, :null, :pochven]},
|
||||
{[:null], [:null]}
|
||||
]
|
||||
|
||||
for {input_scopes, expected_scopes} <- test_cases do
|
||||
input = %{
|
||||
id: "test-map-id",
|
||||
name: "Test Map",
|
||||
scope: :wormholes,
|
||||
scopes: input_scopes,
|
||||
owner_id: "owner-123",
|
||||
acls: [],
|
||||
hubs: []
|
||||
}
|
||||
|
||||
map = WandererApp.Map.new(input)
|
||||
effective = CharactersImpl.get_effective_scopes(map)
|
||||
|
||||
assert effective == expected_scopes,
|
||||
"Expected #{inspect(expected_scopes)}, got #{inspect(effective)} for input #{inspect(input_scopes)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -300,7 +300,7 @@ defmodule WandererAppWeb.Factory do
|
||||
# Include owner_id in the form data just like the LiveView does
|
||||
create_attrs =
|
||||
built_attrs
|
||||
|> Map.take([:name, :slug, :description, :scope, :only_tracked_characters])
|
||||
|> Map.take([:name, :slug, :description, :scope, :scopes, :only_tracked_characters])
|
||||
|> Map.put(:owner_id, owner_id)
|
||||
|
||||
# Debug: ensure owner_id is valid
|
||||
|
||||
@@ -206,37 +206,55 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @hs_system_id, @hs_system_id) == false
|
||||
end
|
||||
|
||||
test "connection valid when at least one system matches a scope" do
|
||||
# WH to HS: valid if either :wormholes or :hi is selected
|
||||
test "wormhole border behavior: WH connections allow border k-space systems" do
|
||||
# WH to HS with [:wormholes]: valid (wormhole border behavior)
|
||||
# At least one system is WH, :wormholes is enabled -> border k-space allowed
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) ==
|
||||
true
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @wh_system_id, @hs_system_id) == true
|
||||
# WH to HS with [:hi] only: INVALID (no wormhole scope, WH doesn't match :hi)
|
||||
# Neither system matches when we require both to match (no wormhole border behavior)
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @wh_system_id, @hs_system_id) == false
|
||||
|
||||
# WH to WH: valid only if :wormholes is selected
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @c2_system_id) ==
|
||||
true
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @wh_system_id, @c2_system_id) == false
|
||||
|
||||
# HS to LS: valid if :hi or :low is selected
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @hs_system_id, @ls_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:low], @hs_system_id, @ls_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:null], @hs_system_id, @ls_system_id) == false
|
||||
end
|
||||
|
||||
test "connection with multiple scopes allows cross-space movement" do
|
||||
# With [:wormholes, :hi], all of these should be valid:
|
||||
# - WH to WH (wormholes matches)
|
||||
# - HS to HS (hi matches)
|
||||
# - WH to HS (either matches)
|
||||
test "k-space connections require BOTH systems to match scopes" do
|
||||
# HS to LS: requires BOTH to match, so single scope is not enough
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @hs_system_id, @ls_system_id) == false
|
||||
assert ConnectionsImpl.is_connection_valid([:low], @hs_system_id, @ls_system_id) == false
|
||||
assert ConnectionsImpl.is_connection_valid([:null], @hs_system_id, @ls_system_id) == false
|
||||
|
||||
# HS to LS with [:hi, :low]: valid (both match)
|
||||
assert ConnectionsImpl.is_connection_valid([:hi, :low], @hs_system_id, @ls_system_id) == true
|
||||
|
||||
# HS to HS: valid with [:hi] (both match)
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @hs_system_id, 30_000_002) == true
|
||||
|
||||
# NS to NS: valid with [:null] (both match)
|
||||
assert ConnectionsImpl.is_connection_valid([:null], @ns_system_id, @ns_system_id) == false
|
||||
# (same system returns false)
|
||||
end
|
||||
|
||||
test "connection with multiple scopes" do
|
||||
# With [:wormholes, :hi]:
|
||||
# - WH to WH: valid (both match :wormholes)
|
||||
# - HS to HS: valid (both match :hi)
|
||||
# - WH to HS: valid (wormhole border behavior - WH is wormhole, :wormholes enabled)
|
||||
scopes = [:wormholes, :hi]
|
||||
assert ConnectionsImpl.is_connection_valid(scopes, @wh_system_id, @c2_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid(scopes, @hs_system_id, 30_000_002) == true
|
||||
assert ConnectionsImpl.is_connection_valid(scopes, @wh_system_id, @hs_system_id) == true
|
||||
|
||||
# But LS to NS should not be valid with [:wormholes, :hi]
|
||||
# LS to NS should not be valid with [:wormholes, :hi] (neither is WH, neither matches)
|
||||
assert ConnectionsImpl.is_connection_valid(scopes, @ls_system_id, @ns_system_id) == false
|
||||
|
||||
# HS to LS should not be valid with [:wormholes, :hi] (neither is WH, only HS matches)
|
||||
assert ConnectionsImpl.is_connection_valid(scopes, @hs_system_id, @ls_system_id) == false
|
||||
end
|
||||
|
||||
test "all scopes allows any connection" do
|
||||
@@ -294,4 +312,127 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
assert ConnectionsImpl.is_prohibited_system_class?(25) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "maybe_add_system/5 scope filtering" do
|
||||
alias WandererApp.Map.Server.SystemsImpl
|
||||
|
||||
test "returns :ok without filtering when scopes is nil" do
|
||||
# When scopes is nil, should not filter (backward compatibility)
|
||||
result = SystemsImpl.maybe_add_system("map_id", nil, nil, [])
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "returns :ok without filtering when scopes is empty list" do
|
||||
# Empty scopes should not filter (let through)
|
||||
result = SystemsImpl.maybe_add_system("map_id", nil, nil, [], [])
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "filters system when scopes provided and system doesn't match" do
|
||||
# When scopes is [:wormholes] and system is Hi-Sec, should filter (return :ok without adding)
|
||||
location = %{solar_system_id: @hs_system_id}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, nil, [], [:wormholes])
|
||||
# Returns :ok because system was filtered out (not an error, just skipped)
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "allows system through when scopes match (verified via can_add_location)" do
|
||||
# When scopes is [:wormholes] and system is WH, filtering should allow it
|
||||
# We test this via can_add_location which is what maybe_add_system uses internally
|
||||
assert ConnectionsImpl.can_add_location([:wormholes], @wh_system_id) == true
|
||||
assert ConnectionsImpl.can_add_location([:null], @ns_system_id) == true
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_id) == true
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @ns_system_id) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "border system auto-addition behavior" do
|
||||
# Tests that verify bordered systems are correctly auto-added ONLY for wormholes.
|
||||
# Key behavior:
|
||||
# - Wormhole border: WH to Hi-Sec with [:wormholes] -> BOTH added (border behavior)
|
||||
# - K-space only: Null to Hi-Sec with [:wormholes, :null] -> REJECTED (no border for k-space)
|
||||
# - K-space must match: both systems must match scopes when no wormhole involved
|
||||
|
||||
test "WORMHOLE BORDER: WH->Hi-Sec with [:wormholes] is VALID (border k-space added)" do
|
||||
# Border case: moving from WH to k-space
|
||||
# Valid because :wormholes enabled AND one system is WH
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) == true
|
||||
end
|
||||
|
||||
test "WORMHOLE BORDER: Hi-Sec->WH with [:wormholes] is VALID (border k-space added)" do
|
||||
# Border case: moving from k-space to WH
|
||||
# Valid because :wormholes enabled AND one system is WH
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @wh_system_id) == true
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Hi-Sec->Hi-Sec with [:wormholes] is REJECTED" do
|
||||
# No wormhole involved, neither matches :wormholes
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, 30_000_002) == false
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Null->Hi-Sec with [:wormholes, :null] is REJECTED (no border for k-space)" do
|
||||
# Neither system is a wormhole, so no border behavior
|
||||
# Null matches :null, but Hi-Sec doesn't match any scope -> BOTH must match
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @ns_system_id, @hs_system_id) ==
|
||||
false
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Hi-Sec->Low-Sec with [:wormholes, :null] is REJECTED" do
|
||||
# Neither Hi-Sec nor Low-Sec match [:wormholes, :null], no WH involved
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @hs_system_id, @ls_system_id) ==
|
||||
false
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Low-Sec->Hi-Sec with [:low] is REJECTED (no border for k-space)" do
|
||||
# Low-Sec matches :low, but Hi-Sec doesn't match
|
||||
# No wormhole involved, so BOTH must match -> rejected
|
||||
assert ConnectionsImpl.is_connection_valid([:low], @ls_system_id, @hs_system_id) == false
|
||||
end
|
||||
|
||||
test "K-SPACE MATCH: Low-Sec->Low-Sec with [:low] is VALID (both match)" do
|
||||
# Both systems match :low
|
||||
assert ConnectionsImpl.is_connection_valid([:low], @ls_system_id, 30_000_101) == true
|
||||
end
|
||||
|
||||
test "K-SPACE MATCH: Null->Null with [:null] is VALID (both match)" do
|
||||
# Would need two different null-sec systems for this test
|
||||
# Using same system returns false (same system check)
|
||||
assert ConnectionsImpl.is_connection_valid([:null], @ns_system_id, @ns_system_id) == false
|
||||
end
|
||||
|
||||
test "WORMHOLE BORDER: Pochven->WH with [:wormholes, :pochven] is VALID" do
|
||||
# WH is wormhole, :wormholes enabled -> border behavior applies
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :pochven], @pochven_id, @wh_system_id) ==
|
||||
true
|
||||
end
|
||||
|
||||
test "WORMHOLE BORDER: WH->Pochven with [:wormholes] is VALID (border k-space)" do
|
||||
# WH is wormhole, :wormholes enabled -> border behavior, Pochven added as border
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @pochven_id) == true
|
||||
end
|
||||
|
||||
test "border systems: WH->Hi-Sec->WH path with [:wormholes] scope" do
|
||||
# Simulates a character path through k-space between WHs
|
||||
# First jump: WH to Hi-Sec - valid (wormhole border)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) == true
|
||||
# Second jump: Hi-Sec to WH - valid (wormhole border)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @c2_system_id) == true
|
||||
end
|
||||
|
||||
test "excluded path: k-space chain with [:wormholes] scope remains excluded" do
|
||||
# If character moves within k-space (no WH involved), should be excluded
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, 30_000_002) == false
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], 30_000_002, @ls_system_id) == false
|
||||
end
|
||||
|
||||
test "excluded path: Null->Hi-Sec->Low-Sec with [:wormholes, :null] - only Null tracked" do
|
||||
# Character in Null (tracked) jumps to Hi-Sec (border - but NO wormhole!) -> REJECTED
|
||||
# This is the key case: k-space to k-space should NOT add border systems
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @ns_system_id, @hs_system_id) ==
|
||||
false
|
||||
# Hi-Sec to Low-Sec also rejected (neither matches)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @hs_system_id, @ls_system_id) ==
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user