using Content.Server.Mind;
using Content.Shared._Horizon.CCVar;
using Content.Shared._Horizon.Shipyard.Components;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._Horizon.Shipyard;
///
/// Manages ship ownership and handles cleanup of ships when owners are offline too long
///
public sealed class ShipOwnershipSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly ISawmill _sawmill = Logger.GetSawmill("shipOwnership");
private readonly HashSet _pendingDeletionShips = new();
// Timer for deletion checks
private TimeSpan _nextDeletionCheckTime;
private const int DeletionCheckIntervalSeconds = 60;
private bool _autoDeleteEnabled;
public override void Initialize()
{
base.Initialize();
// Subscribe to player events to track when they join/leave
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
// Initialize tracking for ships
SubscribeLocalEvent(OnShipOwnershipStartup);
SubscribeLocalEvent(OnShipOwnershipShutdown);
// Initialize the deletion check timer
_nextDeletionCheckTime = _gameTiming.CurTime;
Subs.CVar(_cfg, HorizonCCVars.AutoDeleteEnabled, value => _autoDeleteEnabled = value, true);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
///
/// Register a ship as being owned by a player
///
public void RegisterShipOwnership(EntityUid gridUid, ICommonSession owningPlayer)
{
// Don't register ownership if the entity isn't valid
if (!EntityManager.EntityExists(gridUid))
return;
// Add ownership component to the ship
var comp = EnsureComp(gridUid);
comp.OwnerUserId = owningPlayer.UserId;
comp.IsOwnerOnline = true;
comp.LastStatusChangeTime = _gameTiming.CurTime;
Dirty(gridUid, comp);
// Log ship registration
_sawmill.Info($"Registered ship {ToPrettyString(gridUid)} to player {owningPlayer.Name} ({owningPlayer.UserId})");
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_autoDeleteEnabled)
return;
// Only check for ship deletion every DeletionCheckIntervalSeconds
if (_gameTiming.CurTime < _nextDeletionCheckTime)
return;
// Update next check time
_nextDeletionCheckTime = _gameTiming.CurTime + TimeSpan.FromSeconds(DeletionCheckIntervalSeconds);
// Log that we're checking for ships to delete
_sawmill.Debug($"Checking for abandoned ships to delete");
// Check for ships that need to be deleted due to owner absence
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var ownership))
{
// Skip ships with online owners
if (ownership.IsOwnerOnline)
continue;
// Calculate how long the owner has been offline
var offlineTime = _gameTiming.CurTime - ownership.LastStatusChangeTime;
var timeoutSeconds = TimeSpan.FromSeconds(ownership.DeletionTimeoutSeconds);
// Check if we've passed the timeout
if (offlineTime >= timeoutSeconds)
{
// Check if there are any living beings on the ship before deleting
var mobQuery = GetEntityQuery();
var xformQuery = GetEntityQuery();
if (HasLivingBeingsOnShip(uid, mobQuery, xformQuery))
{
// Skip deletion if living beings are on the ship
_sawmill.Debug($"Skipping deletion of abandoned ship {ToPrettyString(uid)} because there are living beings on it");
// Reset the timer to check again later
ownership.LastStatusChangeTime = _gameTiming.CurTime;
Dirty(uid, ownership);
continue;
}
// Queue ship for deletion
_pendingDeletionShips.Add(uid);
}
}
// Process deletions outside of enumeration
foreach (var shipUid in _pendingDeletionShips)
{
if (!EntityManager.EntityExists(shipUid))
continue;
// Only handle deletion if this entity has a transform and is a grid
var transform = Transform(shipUid);
if (transform.GridUid == shipUid)
{
_sawmill.Info($"Deleting abandoned ship {ToPrettyString(shipUid)}");
// Delete the grid entity
QueueDel(shipUid);
}
}
_pendingDeletionShips.Clear();
}
///
/// Checks if there are any living beings aboard a ship
///
/// The ship entity to check
/// Query for accessing MobState components
/// Query for accessing Transform components
/// True if living beings are found, false otherwise
private bool HasLivingBeingsOnShip(EntityUid uid, EntityQuery mobQuery, EntityQuery xformQuery)
{
// Check if a living entity is on this ship
return FoundOrganics(uid, mobQuery, xformQuery) != null;
}
///
/// Looks for a living, sapient being aboard a particular entity.
/// Only considers online players as obstacles - offline players (former players) don't prevent deletion.
///
/// The entity to search (e.g. a shuttle, a station)
/// A query to get the MobState from an entity
/// A query to get the transform component of an entity
/// The name of the sapient being if one was found, null otherwise.
private string? FoundOrganics(EntityUid uid, EntityQuery mobQuery, EntityQuery xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
// Ghosts don't stop a ship deletion
if (HasComp(child))
continue;
// Check if we have a player entity with an active session
if (_mind.TryGetMind(child, out var mind, out var mindComp))
{
// Only consider players with an active session (online) as obstacles
// Offline players (former players) don't prevent ship deletion
if (mindComp.UserId != null && _playerManager.TryGetSessionById(mindComp.UserId.Value, out _))
{
// Player is online and character is not dead, so they prevent deletion
if (!_mind.IsCharacterDeadPhysically(mindComp))
{
return Name(child);
}
}
// If player is offline (no active session), they don't prevent deletion - continue searching
}
// Recursively check children
var charName = FoundOrganics(child, mobQuery, xformQuery);
if (charName != null)
return charName;
}
return null;
}
private void OnShipOwnershipStartup(EntityUid uid, ShipOwnershipComponent component, ComponentStartup args)
{
// If player is already online, mark them as such
if (_playerManager.TryGetSessionById(component.OwnerUserId, out var player))
{
component.IsOwnerOnline = true;
component.LastStatusChangeTime = _gameTiming.CurTime;
Dirty(uid, component);
}
}
private void OnShipOwnershipShutdown(EntityUid uid, ShipOwnershipComponent component, ComponentShutdown args)
{
// Nothing to do here for now
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session == null)
return;
var userId = e.Session.UserId;
var query = EntityQueryEnumerator();
// Update all ships owned by this player
while (query.MoveNext(out var shipUid, out var ownership))
{
if (ownership.OwnerUserId != userId)
continue;
switch (e.NewStatus)
{
case SessionStatus.Connected:
case SessionStatus.InGame:
// Player has connected, update ownership
ownership.IsOwnerOnline = true;
ownership.LastStatusChangeTime = _gameTiming.CurTime;
_sawmill.Debug($"Owner of ship {ToPrettyString(shipUid)} has connected");
break;
case SessionStatus.Disconnected:
// Player has disconnected, update ownership
ownership.IsOwnerOnline = false;
ownership.LastStatusChangeTime = _gameTiming.CurTime;
_sawmill.Debug($"Owner of ship {ToPrettyString(shipUid)} has disconnected");
break;
}
Dirty(shipUid, ownership);
}
}
}