diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs index 481d192722..8100bfba49 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs @@ -43,42 +43,42 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible [Dependency] private IGameTiming _gameTiming; #pragma warning restore 649 private PathfindingSystem _pathfindingSystem; - + /// /// Queued region updates /// private HashSet _queuedUpdates = new HashSet(); - + // Oh god the nesting. Shouldn't need to go beyond this /// /// The corresponding regions for each PathfindingChunk. /// Regions are groups of nodes with the same profile (for pathfinding purposes) /// i.e. same collision, not-space, same access, etc. /// - private Dictionary>> _regions = + private Dictionary>> _regions = new Dictionary>>(); - + /// /// Minimum time for the cached reachable regions to be stored /// private const float MinCacheTime = 1.0f; - + // Cache what regions are accessible from this region. Cached per ReachableArgs // so multiple entities in the same region with the same args should all be able to share their reachable lookup // Also need to store when we cached it to know if it's stale if the chunks have updated - + // TODO: There's probably a more memory-efficient way to cache this // Then again, there's likely also a more memory-efficient way to implement regions. - + // Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup // Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result - private Dictionary Regions)>> _cachedAccessible = + private Dictionary Regions)>> _cachedAccessible = new Dictionary)>>(); #if DEBUG private int _runningCacheIdx = 0; #endif - + public override void Initialize() { _pathfindingSystem = Get(); @@ -86,6 +86,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible #if DEBUG SubscribeLocalEvent(SendDebugMessage); #endif + _mapmanager.OnGridRemoved += GridRemoved; + } + + private void GridRemoved(GridId gridId) + { + _regions.Remove(gridId); } public override void Update(float frameTime) @@ -165,7 +171,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible targetNode = node; } } - + return CanAccess(entity, targetNode); } @@ -175,7 +181,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { return false; } - + var entityTile = _mapmanager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition); var entityNode = _pathfindingSystem.GetNode(entityTile); var entityRegion = GetRegion(entityNode); @@ -207,7 +213,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { return new HashSet(); } - + var cachedArgs = GetCachedArgs(reachableArgs); (TimeSpan CacheTime, HashSet Regions) cached; @@ -239,7 +245,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible private ReachableArgs GetCachedArgs(ReachableArgs accessibleArgs) { ReachableArgs foundArgs = null; - + foreach (var (cachedAccessible, _) in _cachedAccessible) { if (Equals(cachedAccessible.Access, accessibleArgs.Access) && @@ -269,7 +275,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible _cachedAccessible.Add(accessibleArgs, new Dictionary)>()); return false; } - + if (!cachedArgs.TryGetValue(region, out var regionCache)) { return false; @@ -287,7 +293,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible foreach (var accessibleRegion in regionCache.Regions) { if (checkedAccess.Contains(accessibleRegion)) continue; - + // Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections) // TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up if (accessibleRegion.ParentChunk.LastUpdate > regionCache.CacheTime) @@ -336,7 +342,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { continue; } - + // Distance is an approximation here so we'll be generous with it // TODO: Could do better; the fewer nodes the better it is. if (!neighbor.RegionTraversable(reachableArgs) || @@ -345,12 +351,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible closedSet.Add(neighbor); continue; } - + openSet.Enqueue(neighbor); accessible.Add(neighbor); } } - + return (_gameTiming.CurTime, accessible); } @@ -370,7 +376,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { var directionNode = node.GetNeighbor(direction); if (directionNode == null) continue; - + var directionRegion = GetRegion(directionNode); if (directionRegion == null || directionRegion == region) continue; @@ -403,7 +409,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible // On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes? // Someone smarter than me will know better var parentChunk = node.ParentChunk; - + // No guarantee the node even has a region yet (if we're doing neighbor lookups) if (!_regions[parentChunk.GridId].TryGetValue(parentChunk, out var regions)) { @@ -460,10 +466,10 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible PathfindingRegion bottomRegion; // We'll check if our left or down neighbors are already in a region and join them - + // Is left node valid to connect to if (leftNeighbor != null && - existingRegions.TryGetValue(leftNeighbor, out leftRegion) && + existingRegions.TryGetValue(leftNeighbor, out leftRegion) && !leftRegion.IsDoor) { // We'll try and connect the left node's region to the bottom region if they're separate (yay merge) @@ -483,8 +489,8 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible } //Is bottom node valid to connect to - if (bottomNeighbor != null && - existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) && + if (bottomNeighbor != null && + existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) && !bottomRegion.IsDoor) { bottomRegion.Add(node); @@ -492,13 +498,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible UpdateRegionEdge(bottomRegion, node); return bottomRegion; } - + // If we can't join an existing region then we'll make our own var newRegion = new PathfindingRegion(node, new HashSet {node}, node.AccessReaders.Count > 0); _regions[parentChunk.GridId][parentChunk].Add(newRegion); existingRegions.Add(node, newRegion); UpdateRegionEdge(newRegion, node); - + return newRegion; } @@ -524,7 +530,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible UpdateRegionEdge(target, node); } } - + /// /// Generate all of the regions within a chunk /// @@ -564,7 +570,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { continue; } - + chunkRegions.Add(region); } } @@ -587,8 +593,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible var debugResult = new Dictionary>>(); var chunkIdx = 0; var regionIdx = 0; - - foreach (var (_, regions) in _regions[gridId]) + + if (!_regions.TryGetValue(gridId, out var dict)) + { + return; + } + + foreach (var (_, regions) in dict) { var debugRegions = new Dictionary>(); debugResult.Add(chunkIdx, debugRegions); @@ -622,15 +633,15 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible { var grid = _mapmanager.GetGrid(gridId); var debugResult = new Dictionary>(); - + foreach (var region in regions) { debugResult.Add(_runningCacheIdx, new List()); - + foreach (var node in region.Nodes) { var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(_mapmanager); - + debugResult[_runningCacheIdx].Add(nodeVector); } @@ -641,4 +652,4 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible } #endif } -} \ No newline at end of file +}