diff --git a/Content.Server/AI/Operators/Movement/BaseMover.cs b/Content.Server/AI/Operators/Movement/BaseMover.cs index d17472378f..cbbd5acdae 100644 --- a/Content.Server/AI/Operators/Movement/BaseMover.cs +++ b/Content.Server/AI/Operators/Movement/BaseMover.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding; @@ -240,10 +241,11 @@ namespace Content.Server.AI.Operators.Movement var startGrid = _mapManager.GetGrid(Owner.Transform.GridID).GetTileRef(Owner.Transform.GridPosition); var endGrid = _mapManager.GetGrid(TargetGrid.GridID).GetTileRef(TargetGrid);; - // _routeCancelToken = new CancellationTokenSource(); + var access = AccessReader.FindAccessTags(Owner); RouteJob = _pathfinder.RequestPath(new PathfindingArgs( Owner.Uid, + access, collisionMask, startGrid, endGrid, diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs index 081a569423..25f329fdc1 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs @@ -29,11 +29,17 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee public override void SetupOperators(Blackboard context) { - var moveOperator = new MoveToEntityOperator(Owner, _entity); var equipped = context.GetState().GetValue(); + MoveToEntityOperator moveOperator; if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) { - moveOperator.DesiredRange = meleeWeaponComponent.Range - 0.01f; + moveOperator = new MoveToEntityOperator(Owner, _entity, meleeWeaponComponent.Range - 0.01f); + } + // I think it's possible for this to happen given planning is time-sliced? + // TODO: At this point we should abort + else + { + moveOperator = new MoveToEntityOperator(Owner, _entity); } ActionOperators = new Queue(new AiOperator[] diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs index 82d076ba1a..e886d0ff61 100644 --- a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs +++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs @@ -126,6 +126,9 @@ namespace Content.Server.AI.Utility.AiLogic { damageableComponent.DamageThresholdPassed -= DeathHandle; } + + var currentOp = CurrentAction?.ActionOperators.Peek(); + currentOp?.Shutdown(Outcome.Failed); } private void DeathHandle(object sender, DamageThresholdPassedEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Access/AccessReaderChangeMessage.cs b/Content.Server/GameObjects/Components/Access/AccessReaderChangeMessage.cs new file mode 100644 index 0000000000..cc1ae73793 --- /dev/null +++ b/Content.Server/GameObjects/Components/Access/AccessReaderChangeMessage.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.Components.Access +{ + public sealed class AccessReaderChangeMessage : EntitySystemMessage + { + public EntityUid Uid { get; } + public bool Enabled { get; } + + public AccessReaderChangeMessage(EntityUid uid, bool enabled) + { + Uid = uid; + Enabled = enabled; + } + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs index f9e6c07a9d..2a1d18ac41 100644 --- a/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs +++ b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs @@ -69,7 +69,7 @@ namespace Content.Server.GameObjects.Components.Access } [CanBeNull] - private static ICollection FindAccessTags(IEntity entity) + public static ICollection FindAccessTags(IEntity entity) { if (entity.TryGetComponent(out IAccess accessComponent)) { diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index 68d0c9847a..5ace07e054 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -154,6 +154,8 @@ namespace Content.Server.GameObjects State = DoorState.Open; SetAppearance(DoorVisualState.Open); }, _cancellationTokenSource.Token); + + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner.Uid, false)); } public virtual bool CanClose() @@ -203,6 +205,7 @@ namespace Content.Server.GameObjects occluder.Enabled = true; } }, _cancellationTokenSource.Token); + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner.Uid, true)); return true; } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs deleted file mode 100644 index e5df90943d..0000000000 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.GameObjects.Components.Transform; - -namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates -{ - public struct CollidableMove : IPathfindingGraphUpdate - { - public MoveEvent MoveEvent { get; } - - public CollidableMove(MoveEvent moveEvent) - { - MoveEvent = moveEvent; - } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs deleted file mode 100644 index 0d7dde253a..0000000000 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Robust.Shared.Interfaces.GameObjects; - -namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates -{ - public class CollisionChange : IPathfindingGraphUpdate - { - public IEntity Owner { get; } - public bool Value { get; } - - public CollisionChange(IEntity owner, bool value) - { - Owner = owner; - Value = value; - } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs deleted file mode 100644 index 30ee86f2aa..0000000000 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Map; - -namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates -{ - public struct GridRemoval : IPathfindingGraphUpdate - { - public GridId GridId { get; } - - public GridRemoval(GridId gridId) - { - GridId = gridId; - } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs deleted file mode 100644 index 69aa5c1eac..0000000000 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates -{ - public interface IPathfindingGraphUpdate - { - - } -} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs deleted file mode 100644 index 501e4dabb8..0000000000 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Map; - -namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates -{ - public struct TileUpdate : IPathfindingGraphUpdate - { - public TileUpdate(TileRef tile) - { - Tile = tile; - } - - public TileRef Tile { get; } - } -} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs index 1ac13372ad..e69b9e06d8 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs @@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders } // If we couldn't get a nearby node that's good enough - if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs)) + if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs)) { return null; } @@ -88,9 +88,9 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders } // If tile is untraversable it'll be null - var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, nextNode); + var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode); - if (tileCost == null || !Utils.DirectionTraversable(_pathfindingArgs.CollisionMask, currentNode, direction)) + if (tileCost == null || !PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction)) { continue; } @@ -107,7 +107,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders // pFactor is tie-breaker where the fscore is otherwise equal. // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties // There's other ways to do it but future consideration - var fScore = gScores[nextNode] + Utils.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f); + var fScore = gScores[nextNode] + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f); openTiles.Add((fScore, nextNode)); } } @@ -117,7 +117,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders return null; } - var route = Utils.ReconstructPath(cameFrom, currentNode); + var route = PathfindingHelpers.ReconstructPath(cameFrom, currentNode); if (route.Count == 1) { diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs index 2b12b1b456..ffae0d7d89 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs @@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders } // If we couldn't get a nearby node that's good enough - if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs)) + if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs)) { return null; } @@ -89,7 +89,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders jumpNodes.Add(jumpNode); #endif // GetJumpPoint should already check if we can traverse to the node - var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, jumpNode); + var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, jumpNode); if (tileCost == null) { @@ -108,7 +108,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders // pFactor is tie-breaker where the fscore is otherwise equal. // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties // There's other ways to do it but future consideration - var fScore = gScores[jumpNode] + Utils.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f); + var fScore = gScores[jumpNode] + PathfindingHelpers.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f); openTiles.Add((fScore, jumpNode)); } } @@ -119,7 +119,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders return null; } - var route = Utils.ReconstructJumpPath(cameFrom, currentNode); + var route = PathfindingHelpers.ReconstructJumpPath(cameFrom, currentNode); if (route.Count == 1) { return null; @@ -161,7 +161,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders // We'll do opposite DirectionTraversable just because of how the method's setup // Nodes should be 2-way anyway. if (nextNode == null || - Utils.GetTileCost(_pathfindingArgs, currentNode, nextNode) == null) + PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode) == null) { return null; } @@ -312,14 +312,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders throw new ArgumentOutOfRangeException(); } - if ((closedNeighborOne == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null) - && openNeighborOne != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null) + if ((closedNeighborOne == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null) + && openNeighborOne != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null) { return true; } - if ((closedNeighborTwo == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null) - && openNeighborTwo != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null) + if ((closedNeighborTwo == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null) + && openNeighborTwo != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null) { return true; } @@ -371,14 +371,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders throw new ArgumentOutOfRangeException(); } - if ((closedNeighborOne == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborOne.CollisionMask)) && - (openNeighborOne != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborOne.CollisionMask))) + if ((closedNeighborOne == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborOne)) && + (openNeighborOne != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborOne))) { return true; } - if ((closedNeighborTwo == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborTwo.CollisionMask)) && - (openNeighborTwo != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborTwo.CollisionMask))) + if ((closedNeighborTwo == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborTwo)) && + (openNeighborTwo != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborTwo))) { return true; } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs index d2a01ea1ce..5b7e9ce3c6 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -6,6 +7,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders public struct PathfindingArgs { public EntityUid Uid { get; } + public ICollection Access { get; } public int CollisionMask { get; } public TileRef Start { get; } public TileRef End { get; } @@ -20,6 +22,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders public PathfindingArgs( EntityUid entityUid, + ICollection access, int collisionMask, TileRef start, TileRef end, @@ -29,6 +32,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders bool allowSpace = false) { Uid = entityUid; + Access = access; CollisionMask = collisionMask; Start = start; End = end; diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs similarity index 79% rename from Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs rename to Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs index a72869db78..127c43a8d4 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; using Content.Server.GameObjects.EntitySystems.Pathfinding; using Robust.Shared.Interfaces.GameObjects; @@ -10,19 +11,19 @@ using Robust.Shared.Maths; namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding { - public static class Utils + public static class PathfindingHelpers { public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs) { - if (!Traversable(pathfindingArgs.CollisionMask, endNode.CollisionMask)) + if (!Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, endNode)) { if (pathfindingArgs.Proximity > 0.0f) { // TODO: Should make this account for proximities, // probably some kind of breadth-first search to find a valid one - foreach (var (direction, node) in endNode.Neighbors) + foreach (var (_, node) in endNode.Neighbors) { - if (Traversable(pathfindingArgs.CollisionMask, node.CollisionMask)) + if (Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, node)) { endNode = node; return true; @@ -36,7 +37,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding return true; } - public static bool DirectionTraversable(int collisionMask, PathfindingNode currentNode, Direction direction) + public static bool DirectionTraversable(int collisionMask, ICollection access, PathfindingNode currentNode, Direction direction) { // If it's a diagonal we need to check NSEW to see if we can get to it and stop corner cutting, NE needs N and E etc. // Given there's different collision layers stored for each node in the graph it's probably not worth it to cache this @@ -51,32 +52,32 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding { case Direction.NorthEast: if (northNeighbor == null || eastNeighbor == null) return false; - if (!Traversable(collisionMask, northNeighbor.CollisionMask) || - !Traversable(collisionMask, eastNeighbor.CollisionMask)) + if (!Traversable(collisionMask, access, northNeighbor) || + !Traversable(collisionMask, access, eastNeighbor)) { return false; } break; case Direction.NorthWest: if (northNeighbor == null || westNeighbor == null) return false; - if (!Traversable(collisionMask, northNeighbor.CollisionMask) || - !Traversable(collisionMask, westNeighbor.CollisionMask)) + if (!Traversable(collisionMask, access, northNeighbor) || + !Traversable(collisionMask, access, westNeighbor)) { return false; } break; case Direction.SouthWest: if (southNeighbor == null || westNeighbor == null) return false; - if (!Traversable(collisionMask, southNeighbor.CollisionMask) || - !Traversable(collisionMask, westNeighbor.CollisionMask)) + if (!Traversable(collisionMask, access, southNeighbor) || + !Traversable(collisionMask, access, westNeighbor)) { return false; } break; case Direction.SouthEast: if (southNeighbor == null || eastNeighbor == null) return false; - if (!Traversable(collisionMask, southNeighbor.CollisionMask) || - !Traversable(collisionMask, eastNeighbor.CollisionMask)) + if (!Traversable(collisionMask, access, southNeighbor) || + !Traversable(collisionMask, access, eastNeighbor)) { return false; } @@ -86,11 +87,24 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding return true; } - public static bool Traversable(int collisionMask, int nodeMask) + public static bool Traversable(int collisionMask, ICollection access, PathfindingNode node) { - return (collisionMask & nodeMask) == 0; - } + if ((collisionMask & node.BlockedCollisionMask) != 0) + { + return false; + } + foreach (var reader in node.AccessReaders) + { + if (!reader.IsAllowed(access)) + { + return false; + } + } + + return true; + } + public static Queue ReconstructPath(Dictionary cameFrom, PathfindingNode current) { var running = new Stack(); @@ -194,6 +208,20 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding return 1.4f * dstX + (dstY - dstX); } + + public static float OctileDistance(TileRef endTile, TileRef startTile) + { + // "Fast Euclidean" / octile. + // This implementation is written down in a few sources; it just saves doing sqrt. + int dstX = Math.Abs(startTile.X - endTile.X); + int dstY = Math.Abs(startTile.Y - endTile.Y); + if (dstX > dstY) + { + return 1.4f * dstY + (dstX - dstY); + } + + return 1.4f * dstX + (dstY - dstX); + } public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode) { @@ -202,7 +230,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding public static float? GetTileCost(PathfindingArgs pathfindingArgs, PathfindingNode start, PathfindingNode end) { - if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, end.CollisionMask)) + if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, end)) { return null; } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs index b9b110a566..262631a4b3 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs @@ -1,6 +1,12 @@ using System; using System.Collections.Generic; +using Content.Server.GameObjects.Components.Access; +using Content.Server.GameObjects.Components.Doors; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -8,27 +14,34 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding { public class PathfindingNode { - // TODO: Add access ID here public PathfindingChunk ParentChunk => _parentChunk; private readonly PathfindingChunk _parentChunk; - public TileRef TileRef { get; private set; } - public List CollisionLayers { get; } - public int CollisionMask { get; private set; } + public Dictionary Neighbors => _neighbors; private Dictionary _neighbors = new Dictionary(); + + public TileRef TileRef { get; private set; } + + /// + /// Whenever there's a change in the collision layers we update the mask as the graph has more reads than writes + /// + public int BlockedCollisionMask { get; private set; } + private readonly Dictionary _blockedCollidables = new Dictionary(0); - public PathfindingNode(PathfindingChunk parent, TileRef tileRef, List collisionLayers = null) + public IReadOnlyCollection PhysicsUids => _physicsUids; + private readonly HashSet _physicsUids = new HashSet(0); + + /// + /// The entities on this tile that require access to traverse + /// + /// We don't store the ICollection, at least for now, as we'd need to replicate the access code here + public IReadOnlyCollection AccessReaders => _accessReaders.Values; + private readonly Dictionary _accessReaders = new Dictionary(0); + + public PathfindingNode(PathfindingChunk parent, TileRef tileRef) { _parentChunk = parent; TileRef = tileRef; - if (collisionLayers == null) - { - CollisionLayers = new List(); - } - else - { - CollisionLayers = collisionLayers; - } GenerateMask(); } @@ -105,25 +118,70 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding TileRef = newTile; } - public void AddCollisionLayer(int layer) + /// + /// Call if this entity is relevant for the pathfinder + /// + /// + /// TODO: These 2 methods currently don't account for a bunch of changes (e.g. airlock unpowered, wrenching, etc.) + public void AddEntity(IEntity entity) { - CollisionLayers.Add(layer); - GenerateMask(); + // If we're a door + if (entity.HasComponent() || entity.HasComponent()) + { + // If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future) + // TODO: Check for powered I think (also need an event for when it's depowered + // AccessReader calls this whenever opening / closing but it can seem to get called multiple times + // Which may or may not be intended? + if (entity.TryGetComponent(out AccessReader accessReader) && !_accessReaders.ContainsKey(entity.Uid)) + { + _accessReaders.Add(entity.Uid, accessReader); + } + return; + } + + if (entity.TryGetComponent(out CollidableComponent collidableComponent)) + { + if (entity.TryGetComponent(out PhysicsComponent physicsComponent) && !physicsComponent.Anchored) + { + _physicsUids.Add(entity.Uid); + } + else + { + _blockedCollidables.TryAdd(entity.Uid, collidableComponent.CollisionLayer); + GenerateMask(); + } + } } - public void RemoveCollisionLayer(int layer) + public void RemoveEntity(IEntity entity) { - CollisionLayers.Remove(layer); - GenerateMask(); + if (_accessReaders.ContainsKey(entity.Uid)) + { + _accessReaders.Remove(entity.Uid); + return; + } + + if (entity.HasComponent()) + { + if (entity.TryGetComponent(out PhysicsComponent physicsComponent) && physicsComponent.Anchored) + { + _blockedCollidables.Remove(entity.Uid); + GenerateMask(); + } + else + { + _physicsUids.Remove(entity.Uid); + } + } } private void GenerateMask() { - CollisionMask = 0x0; + BlockedCollisionMask = 0x0; - foreach (var layer in CollisionLayers) + foreach (var layer in _blockedCollidables.Values) { - CollisionMask |= layer; + BlockedCollisionMask |= layer; } } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs index 07c4273664..62a8479306 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Threading; -using Content.Server.GameObjects.Components.Doors; -using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates; +using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; using Content.Server.GameObjects.EntitySystems.JobQueues; using Content.Server.GameObjects.EntitySystems.JobQueues.Queues; using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Content.Shared.Physics; using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components.Transform; using Robust.Shared.GameObjects.Systems; @@ -14,6 +14,7 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Utility; namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding { @@ -29,18 +30,30 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding public class PathfindingSystem : EntitySystem { #pragma warning disable 649 + [Dependency] private readonly IEntityManager _entitymanager; [Dependency] private readonly IMapManager _mapManager; #pragma warning restore 649 public IReadOnlyDictionary> Graph => _graph; private readonly Dictionary> _graph = new Dictionary>(); - // Every tick we queue up all the changes and do them at once - private readonly Queue _queuedGraphUpdates = new Queue(); + private readonly PathfindingJobQueue _pathfindingQueue = new PathfindingJobQueue(); + + // Queued pathfinding graph updates + private readonly Queue _collidableUpdateQueue = new Queue(); + private readonly Queue _moveUpdateQueue = new Queue(); + private readonly Queue _accessReaderUpdateQueue = new Queue(); + private readonly Queue _tileUpdateQueue = new Queue(); // Need to store previously known entity positions for collidables for when they move private readonly Dictionary _lastKnownPositions = new Dictionary(); + public const int TrackedCollisionLayers = (int) + (CollisionGroup.Impassable | + CollisionGroup.MobImpassable | + CollisionGroup.SmallImpassable | + CollisionGroup.VaultImpassable); + /// /// Ask for the pathfinder to gimme somethin /// @@ -68,51 +81,66 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding private void ProcessGraphUpdates() { - for (var i = 0; i < Math.Min(50, _queuedGraphUpdates.Count); i++) + var totalUpdates = 0; + + foreach (var update in _collidableUpdateQueue) { - var update = _queuedGraphUpdates.Dequeue(); - switch (update) + var entity = _entitymanager.GetEntity(update.Owner); + if (update.CanCollide) { - case CollidableMove move: - HandleCollidableMove(move); - break; - case CollisionChange change: - if (change.Value) - { - HandleCollidableAdd(change.Owner); - } - else - { - HandleCollidableRemove(change.Owner); - } - - break; - case GridRemoval removal: - HandleGridRemoval(removal); - break; - case TileUpdate tile: - HandleTileUpdate(tile); - break; - default: - throw new ArgumentOutOfRangeException(); + HandleCollidableAdd(entity); + } + else + { + HandleAccessRemove(entity); } - } - } - private void HandleGridRemoval(GridRemoval removal) - { - if (!_graph.ContainsKey(removal.GridId)) + totalUpdates++; + } + + _collidableUpdateQueue.Clear(); + + foreach (var update in _accessReaderUpdateQueue) { - throw new InvalidOperationException(); + var entity = _entitymanager.GetEntity(update.Uid); + if (update.Enabled) + { + HandleAccessAdd(entity); + } + else + { + HandleAccessRemove(entity); + } + + totalUpdates++; + } + + _accessReaderUpdateQueue.Clear(); + + foreach (var tile in _tileUpdateQueue) + { + HandleTileUpdate(tile); + totalUpdates++; + } + + _tileUpdateQueue.Clear(); + var moveUpdateCount = Math.Max(50 - totalUpdates, 0); + + // Other updates are high priority so for this we'll just defer it if there's a spike (explosion, etc.) + // If the move updates grow too large then we'll just do it + if (_moveUpdateQueue.Count > 100) + { + moveUpdateCount = _moveUpdateQueue.Count - 100; } - _graph.Remove(removal.GridId); - } - - private void HandleTileUpdate(TileUpdate tile) - { - var chunk = GetChunk(tile.Tile); - chunk.UpdateNode(tile.Tile); + moveUpdateCount = Math.Min(moveUpdateCount, _moveUpdateQueue.Count); + + for (var i = 0; i < moveUpdateCount; i++) + { + HandleCollidableMove(_moveUpdateQueue.Dequeue()); + } + + DebugTools.Assert(_moveUpdateQueue.Count < 1000); } public PathfindingChunk GetChunk(TileRef tile) @@ -132,7 +160,6 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding } var newChunk = CreateChunk(tile.GridIndex, mapIndices); - return newChunk; } @@ -179,13 +206,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding public override void Initialize() { - IoCManager.InjectDependencies(this); SubscribeLocalEvent(QueueCollisionEnabledEvent); SubscribeLocalEvent(QueueCollidableMove); + SubscribeLocalEvent(QueueAccessChangeEvent); // Handle all the base grid changes // Anything that affects traversal (i.e. collision layer) is handled separately. - _mapManager.OnGridRemoved += QueueGridRemoval; + _mapManager.OnGridRemoved += HandleGridRemoval; _mapManager.GridChanged += QueueGridChange; _mapManager.TileChanged += QueueTileChange; } @@ -193,32 +220,85 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding public override void Shutdown() { base.Shutdown(); - _mapManager.OnGridRemoved -= QueueGridRemoval; + UnsubscribeLocalEvent(); + UnsubscribeLocalEvent(); + UnsubscribeLocalEvent(); + + _mapManager.OnGridRemoved -= HandleGridRemoval; _mapManager.GridChanged -= QueueGridChange; _mapManager.TileChanged -= QueueTileChange; } + + private void HandleTileUpdate(TileRef tile) + { + var node = GetNode(tile); + node.UpdateTile(tile); + } public void ResettingCleanup() { - _queuedGraphUpdates.Clear(); + _graph.Clear(); + _collidableUpdateQueue.Clear(); + _moveUpdateQueue.Clear(); + _accessReaderUpdateQueue.Clear(); + _tileUpdateQueue.Clear(); + _lastKnownPositions.Clear(); } - private void QueueGridRemoval(GridId gridId) + private void HandleGridRemoval(GridId gridId) { - _queuedGraphUpdates.Enqueue(new GridRemoval(gridId)); + if (_graph.ContainsKey(gridId)) + { + _graph.Remove(gridId); + } } private void QueueGridChange(object sender, GridChangedEventArgs eventArgs) { foreach (var (position, _) in eventArgs.Modified) { - _queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.Grid.GetTileRef(position))); + _tileUpdateQueue.Enqueue(eventArgs.Grid.GetTileRef(position)); } } private void QueueTileChange(object sender, TileChangedEventArgs eventArgs) { - _queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.NewTile)); + _tileUpdateQueue.Enqueue(eventArgs.NewTile); + } + + private void QueueAccessChangeEvent(AccessReaderChangeMessage message) + { + _accessReaderUpdateQueue.Enqueue(message); + } + + private void HandleAccessAdd(IEntity entity) + { + if (entity.Deleted || !entity.HasComponent()) + { + return; + } + + var grid = _mapManager.GetGrid(entity.Transform.GridID); + var tileRef = grid.GetTileRef(entity.Transform.GridPosition); + + var chunk = GetChunk(tileRef); + var node = chunk.GetNode(tileRef); + node.AddEntity(entity); + } + + private void HandleAccessRemove(IEntity entity) + { + if (entity.Deleted || !entity.HasComponent()) + { + return; + } + + var grid = _mapManager.GetGrid(entity.Transform.GridID); + var tileRef = grid.GetTileRef(entity.Transform.GridPosition); + + var chunk = GetChunk(tileRef); + var node = chunk.GetNode(tileRef); + node.RemoveEntity(entity); } #region collidable @@ -228,25 +308,22 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding /// private void HandleCollidableAdd(IEntity entity) { - // It's a grid / gone / a door / we already have it (which probably shouldn't happen) if (entity.Prototype == null || entity.Deleted || - entity.HasComponent() || - entity.HasComponent() || - _lastKnownPositions.ContainsKey(entity)) + _lastKnownPositions.ContainsKey(entity) || + !entity.TryGetComponent(out CollidableComponent collidableComponent) || + !collidableComponent.CanCollide || + (TrackedCollisionLayers & collidableComponent.CollisionLayer) == 0) { return; } var grid = _mapManager.GetGrid(entity.Transform.GridID); var tileRef = grid.GetTileRef(entity.Transform.GridPosition); - - var collisionLayer = entity.GetComponent().CollisionLayer; - var chunk = GetChunk(tileRef); var node = chunk.GetNode(tileRef); - node.AddCollisionLayer(collisionLayer); + node.AddEntity(entity); _lastKnownPositions.Add(entity, tileRef); } @@ -258,46 +335,37 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding { if (entity.Prototype == null || entity.Deleted || - entity.HasComponent() || - entity.HasComponent() || - !_lastKnownPositions.ContainsKey(entity)) + !_lastKnownPositions.ContainsKey(entity) || + !entity.TryGetComponent(out CollidableComponent collidableComponent) || + !collidableComponent.CanCollide || + (TrackedCollisionLayers & collidableComponent.CollisionLayer) == 0) { return; } - _lastKnownPositions.Remove(entity); - var grid = _mapManager.GetGrid(entity.Transform.GridID); var tileRef = grid.GetTileRef(entity.Transform.GridPosition); - - if (!entity.TryGetComponent(out CollidableComponent collidableComponent)) - { - return; - } - - var collisionLayer = collidableComponent.CollisionLayer; - var chunk = GetChunk(tileRef); var node = chunk.GetNode(tileRef); - node.RemoveCollisionLayer(collisionLayer); + + node.RemoveEntity(entity); + _lastKnownPositions.Remove(entity); } private void QueueCollidableMove(MoveEvent moveEvent) { - _queuedGraphUpdates.Enqueue(new CollidableMove(moveEvent)); + _moveUpdateQueue.Enqueue(moveEvent); } - private void HandleCollidableMove(CollidableMove move) + private void HandleCollidableMove(MoveEvent moveEvent) { - if (!_lastKnownPositions.ContainsKey(move.MoveEvent.Sender)) + if (!_lastKnownPositions.ContainsKey(moveEvent.Sender)) { return; } // The pathfinding graph is tile-based so first we'll check if they're on a different tile and if we need to update. // If you get entities bigger than 1 tile wide you'll need some other system so god help you. - var moveEvent = move.MoveEvent; - if (moveEvent.Sender.Deleted) { HandleCollidableRemove(moveEvent.Sender); @@ -314,14 +382,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding _lastKnownPositions[moveEvent.Sender] = newTile; - if (!moveEvent.Sender.TryGetComponent(out CollidableComponent collidableComponent)) + if (!moveEvent.Sender.HasComponent()) { HandleCollidableRemove(moveEvent.Sender); return; } - var collisionLayer = collidableComponent.CollisionLayer; - var gridIds = new HashSet(2) {oldTile.GridIndex, newTile.GridIndex}; foreach (var gridId in gridIds) @@ -330,33 +396,53 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding { var oldChunk = GetChunk(oldTile); var oldNode = oldChunk.GetNode(oldTile); - oldNode.RemoveCollisionLayer(collisionLayer); + oldNode.RemoveEntity(moveEvent.Sender); } if (newTile.GridIndex == gridId) { var newChunk = GetChunk(newTile); var newNode = newChunk.GetNode(newTile); - newNode.RemoveCollisionLayer(collisionLayer); + newNode.AddEntity(moveEvent.Sender); } } } private void QueueCollisionEnabledEvent(CollisionChangeEvent collisionEvent) { - // TODO: Handle containers - var entityManager = IoCManager.Resolve(); - var entity = entityManager.GetEntity(collisionEvent.Owner); - switch (collisionEvent.CanCollide) - { - case true: - _queuedGraphUpdates.Enqueue(new CollisionChange(entity, true)); - break; - case false: - _queuedGraphUpdates.Enqueue(new CollisionChange(entity, false)); - break; - } + _collidableUpdateQueue.Enqueue(collisionEvent); } #endregion + + // TODO: Need to rethink the pathfinder utils (traversable etc.). Maybe just chuck them all in PathfindingSystem + // Otherwise you get the steerer using this and the pathfinders using a different traversable. + // Also look at increasing tile cost the more physics entities are on it + public bool CanTraverse(IEntity entity, GridCoordinates grid) + { + var tile = _mapManager.GetGrid(grid.GridID).GetTileRef(grid); + var node = GetNode(tile); + return CanTraverse(entity, node); + } + + public bool CanTraverse(IEntity entity, PathfindingNode node) + { + if (entity.TryGetComponent(out CollidableComponent collidableComponent) && + (collidableComponent.CollisionMask & node.BlockedCollisionMask) != 0) + { + return false; + } + + var access = AccessReader.FindAccessTags(entity); + + foreach (var reader in node.AccessReaders) + { + if (!reader.IsAllowed(access)) + { + return false; + } + } + + return true; + } } }