using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Timing; using Robust.Shared.Configuration; using Robust.Shared.Maths; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Server.Player; using Robust.Shared.Player; using Content.Server.Salvage.Expeditions; using Content.Server.Gateway.Components; using Content.Server.Mind; using Content.Shared.Ghost; using Content.Shared.Parallax.Biomes; using Content.Shared._Horizon.CCVar; namespace Content.Server._Horizon.GridCleanup; /// /// Автоматически удаляет мелкие гриды (менее 10 тайлов) после задержки в 600 секунд. /// public sealed class GridCleanupSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly MindSystem _mind = default!; private readonly ISawmill _sawmill = Logger.GetSawmill("gridcleanup"); // The minimum number of tiles a grid needs to avoid being cleaned up private const int MinimumTiles = 10; // The delay before cleaning up a small grid (in seconds) private const float CleanupDelay = 600f; // Interval for rechecking grids that were skipped due to players (in seconds) private const float RecheckInterval = 60f; // Dictionary to track grids scheduled for deletion private readonly Dictionary _pendingCleanup = new(); // Set of grids that were skipped due to players - need to be rechecked periodically private readonly HashSet _skippedDueToPlayers = new(); // Last time we rechecked skipped grids private TimeSpan _lastRecheckTime = TimeSpan.Zero; public override void Initialize() { base.Initialize(); // Subscribe to grid events SubscribeLocalEvent(OnGridStartup); SubscribeLocalEvent(OnTileChanged); SubscribeLocalEvent(OnExpeditionStartup); } private bool IsCleanupEnabled() { return _cfg.GetCVar(HorizonCCVars.AutoGridCleanupEnabled); } private void OnGridStartup(GridStartupEvent ev) { // Check newly created grids if (TryComp(ev.EntityUid, out var grid)) CheckGrid((ev.EntityUid, grid)); } private void OnTileChanged(Entity ent, ref TileChangedEvent args) { // When a grid is modified (tiles added/removed), check if it needs cleanup CheckGrid(ent); } private void OnExpeditionStartup(EntityUid uid, SalvageExpeditionComponent component, ComponentStartup args) { // Make sure any grid that gets the expedition component is removed from cleanup if (_pendingCleanup.ContainsKey(uid)) { _sawmill.Debug($"Expedition startup: Removing grid {uid} from cleanup queue"); _pendingCleanup.Remove(uid); } // Check if this entity also has a grid component and ensure it's not marked for cleanup if (TryComp(uid, out var grid)) { // Make sure we don't clean up very small expedition grids var tileCount = CountTiles((uid, grid)); _sawmill.Debug($"Expedition grid {uid} has {tileCount} tiles"); } } private void CheckGrid(Entity ent) { if (!IsCleanupEnabled()) return; var gridUid = ent.Owner; var grid = ent.Comp; // Skip gateway destination grids if (HasComp(gridUid)) return; // Skip if already scheduled for deletion if (_pendingCleanup.ContainsKey(gridUid)) return; // Skip if this is a planet expedition grid if (HasComp(gridUid)) { //Logger.DebugS("salvage", $"CheckGrid: Skipping grid {gridUid} with SalvageExpeditionComponent"); return; } // Skip if the parent map has a SalvageExpeditionComponent var transform = Transform(gridUid); var mapId = transform.MapID; var mapUid = _mapSystem.GetMapOrInvalid(mapId); if (mapUid != EntityUid.Invalid) { // Skip if map has SalvageExpeditionComponent if (HasComp(mapUid)) { //_sawmill.Debug($"CheckGrid: Skipping grid {gridUid} on expedition map {mapUid}"); return; } // Skip if map has BiomeComponent - biomes generate tiles dynamically // and LocalAABB may be inaccurate until tiles are generated if (HasComp(mapUid)) { _sawmill.Debug($"CheckGrid: Skipping grid {gridUid} on biome map {mapUid} - tiles generated dynamically"); return; } } // Check if there are players on the grid if (HasPlayersOnGrid(gridUid)) { _sawmill.Debug($"CheckGrid: Skipping grid {gridUid} - players present"); // Add to recheck list so we can check again when players leave _skippedDueToPlayers.Add(gridUid); return; } // Remove from recheck list if it was there (players left) _skippedDueToPlayers.Remove(gridUid); // Count tiles var tileCount = CountTiles((gridUid, grid)); // If the tile count is below our threshold, schedule it for deletion if (tileCount < MinimumTiles) { _sawmill.Debug($"CheckGrid: Scheduling grid {gridUid} for cleanup with {tileCount} tiles"); ScheduleGridCleanup(gridUid); } } private void ScheduleGridCleanup(EntityUid gridUid) { // Skip if already scheduled if (_pendingCleanup.ContainsKey(gridUid)) return; // Remove from recheck list if it was there _skippedDueToPlayers.Remove(gridUid); var targetTime = _timing.CurTime + TimeSpan.FromSeconds(CleanupDelay); _pendingCleanup[gridUid] = targetTime; } public override void Update(float frameTime) { base.Update(frameTime); if (!IsCleanupEnabled()) return; var currentTime = _timing.CurTime; // Periodically recheck grids that were skipped due to players if (currentTime - _lastRecheckTime >= TimeSpan.FromSeconds(RecheckInterval)) { _lastRecheckTime = currentTime; RecheckSkippedGrids(); } if (_pendingCleanup.Count == 0) return; // Check if any grids need to be cleaned up var toRemove = new List(); foreach (var (gridUid, targetTime) in _pendingCleanup) { // Skip gateway destination grids if (HasComp(gridUid)) { toRemove.Add(gridUid); continue; } // Skip if the time hasn't elapsed yet if (currentTime < targetTime) continue; // Check if the entity still exists if (!EntityManager.EntityExists(gridUid)) { toRemove.Add(gridUid); continue; } // Skip if this is a planet expedition grid if (HasComp(gridUid)) { _sawmill.Debug($"Update: Removing expedition grid {gridUid} from cleanup queue"); toRemove.Add(gridUid); continue; } // Skip if the parent map has an expedition component or biome component var xform = Transform(gridUid); var mapId = xform.MapID; var mapUid = _mapSystem.GetMapOrInvalid(mapId); if (mapUid != EntityUid.Invalid) { if (HasComp(mapUid)) { _sawmill.Debug($"Update: Removing grid {gridUid} on expedition map {mapUid} from cleanup queue"); toRemove.Add(gridUid); continue; } // Skip if map has BiomeComponent - biomes generate tiles dynamically if (HasComp(mapUid)) { _sawmill.Debug($"Update: Removing grid {gridUid} on biome map {mapUid} from cleanup queue - tiles generated dynamically"); toRemove.Add(gridUid); continue; } } // Verify it still has a grid component if (!TryComp(gridUid, out var grid)) { toRemove.Add(gridUid); continue; } // Check if there are players on the grid before deletion if (HasPlayersOnGrid(gridUid)) { _sawmill.Debug($"Update: Removing grid {gridUid} from cleanup queue - players present"); // Add to recheck list so we can check again when players leave _skippedDueToPlayers.Add(gridUid); toRemove.Add(gridUid); continue; } // Check tile count again to make sure it still needs to be deleted var tileCount = CountTiles((gridUid, grid)); if (tileCount >= MinimumTiles) { toRemove.Add(gridUid); continue; } // Queue the grid for deletion QueueDel(gridUid); _sawmill.Debug($"Update: Queuing grid {gridUid} for deletion with {CountTiles((gridUid, grid))} tiles"); toRemove.Add(gridUid); } // Remove processed grids from the pending list foreach (var gridUid in toRemove) { _pendingCleanup.Remove(gridUid); } } private int CountTiles(Entity ent) { var gridUid = ent.Owner; var grid = ent.Comp; var tileCount = 0; // Get AABB of the grid var aabb = grid.LocalAABB; // Convert to grid coordinates var localTL = new Vector2i((int) Math.Floor(aabb.Left), (int) Math.Floor(aabb.Bottom)); var localBR = new Vector2i((int) Math.Ceiling(aabb.Right), (int) Math.Ceiling(aabb.Top)); // Iterate through all tiles in the grid's area for (var x = localTL.X; x < localBR.X; x++) { for (var y = localTL.Y; y < localBR.Y; y++) { var position = new Vector2i(x, y); // Check if tile exists at position and is not empty var tile = _mapSystem.GetTileRef(gridUid, grid, position); if (!tile.Tile.IsEmpty) { tileCount++; // Early return if we've found enough tiles if (tileCount >= MinimumTiles) return tileCount; } } } return tileCount; } /// /// Checks if there are any players on the grid or its children. /// private bool HasPlayersOnGrid(EntityUid uid) { var xform = Transform(uid); var childEnumerator = xform.ChildEnumerator; while (childEnumerator.MoveNext(out var child)) { // Ghosts don't prevent grid cleanup if (HasComp(child)) continue; // Check if we have a player entity that's either still around or alive and may come back if (_mind.TryGetMind(child, out _, out var mindComp) && (mindComp.UserId != null && _player.ValidSessionId(mindComp.UserId.Value) || !_mind.IsCharacterDeadPhysically(mindComp))) { return true; } // Recursively check children if (HasPlayersOnGrid(child)) return true; } return false; } /// /// Rechecks grids that were previously skipped due to players. /// If players have left and grid is still small, adds it back to cleanup queue. /// private void RecheckSkippedGrids() { if (_skippedDueToPlayers.Count == 0) return; var toRemove = new List(); foreach (var gridUid in _skippedDueToPlayers) { // Check if grid still exists if (!EntityManager.EntityExists(gridUid)) { toRemove.Add(gridUid); continue; } // Skip if already in cleanup queue if (_pendingCleanup.ContainsKey(gridUid)) { toRemove.Add(gridUid); continue; } // Skip gateway destination grids if (HasComp(gridUid)) { toRemove.Add(gridUid); continue; } // Skip if this is a planet expedition grid if (HasComp(gridUid)) { toRemove.Add(gridUid); continue; } // Skip if the parent map has a SalvageExpeditionComponent or BiomeComponent var transform = Transform(gridUid); var mapId = transform.MapID; var mapUid = _mapSystem.GetMapOrInvalid(mapId); if (mapUid != EntityUid.Invalid) { if (HasComp(mapUid)) { toRemove.Add(gridUid); continue; } // Skip if map has BiomeComponent - biomes generate tiles dynamically if (HasComp(mapUid)) { toRemove.Add(gridUid); continue; } } // Check if players are still present if (HasPlayersOnGrid(gridUid)) { // Players still present, keep in recheck list continue; } // Players left, check if grid is still small if (!TryComp(gridUid, out var grid)) { toRemove.Add(gridUid); continue; } var tileCount = CountTiles((gridUid, grid)); if (tileCount < MinimumTiles) { // Grid is still small and players left, schedule for cleanup _sawmill.Debug($"RecheckSkippedGrids: Re-adding grid {gridUid} to cleanup queue with {tileCount} tiles (players left)"); ScheduleGridCleanup(gridUid); } // Remove from recheck list (either scheduled or no longer small) toRemove.Add(gridUid); } // Clean up removed grids foreach (var gridUid in toRemove) { _skippedDueToPlayers.Remove(gridUid); } } }