Add utility AI (#806)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
metalgearsloth
2020-06-18 22:52:44 +10:00
committed by GitHub
parent 9b8cedf6c6
commit 5391d3c72a
211 changed files with 10335 additions and 527 deletions

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.Interfaces.GameObjects.Components.Movement;
using JetBrains.Annotations;
using Robust.Server.AI;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI
{
[UsedImplicitly]
internal class AiSystem : EntitySystem
{
#pragma warning disable 649
[Dependency] private readonly IPauseManager _pauseManager;
[Dependency] private readonly IDynamicTypeFactory _typeFactory;
[Dependency] private readonly IReflectionManager _reflectionManager;
#pragma warning restore 649
private readonly Dictionary<string, Type> _processorTypes = new Dictionary<string, Type>();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
// register entity query
EntityQuery = new TypeEntityQuery(typeof(AiControllerComponent));
var processors = _reflectionManager.GetAllChildren<AiLogicProcessor>();
foreach (var processor in processors)
{
var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute));
if (att != null)
{
_processorTypes.Add(att.SerializeName, processor);
}
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var entities = EntityManager.GetEntities(EntityQuery);
foreach (var entity in entities)
{
if (_pauseManager.IsEntityPaused(entity))
{
continue;
}
var aiComp = entity.GetComponent<AiControllerComponent>();
ProcessorInitialize(aiComp);
var processor = aiComp.Processor;
processor.Update(frameTime);
}
}
/// <summary>
/// Will start up the controller's processor if not already done so
/// </summary>
/// <param name="controller"></param>
public void ProcessorInitialize(AiControllerComponent controller)
{
if (controller.Processor != null) return;
controller.Processor = CreateProcessor(controller.LogicName);
controller.Processor.SelfEntity = controller.Owner;
controller.Processor.VisionRadius = controller.VisionRadius;
controller.Processor.Setup();
}
private UtilityAi CreateProcessor(string name)
{
if (_processorTypes.TryGetValue(name, out var type))
{
return (UtilityAi)_typeFactory.CreateInstance(type);
}
// processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name
throw new ArgumentException($"Processor type {name} could not be found.", nameof(name));
}
public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name);
private class AddAiCommand : IClientCommand
{
public string Command => "addai";
public string Description => "Add an ai component with a given processor to an entity.";
public string Help => "Usage: addai <processorId> <entityId>"
+ "\n processorId: Class that inherits AiLogicProcessor and has an AiLogicProcessor attribute."
+ "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this.";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if(args.Length != 2)
{
shell.SendText(player, "Wrong number of args.");
return;
}
var processorId = args[0];
var entId = new EntityUid(int.Parse(args[1]));
var ent = IoCManager.Resolve<IEntityManager>().GetEntity(entId);
var aiSystem = EntitySystem.Get<AiSystem>();
if (!aiSystem.ProcessorTypeExists(processorId))
{
shell.SendText(player, "Invalid processor type. Processor must inherit AiLogicProcessor and have an AiLogicProcessor attribute.");
return;
}
if (ent.HasComponent<AiControllerComponent>())
{
shell.SendText(player, "Entity already has an AI component.");
return;
}
if (ent.HasComponent<IMoverComponent>())
{
ent.RemoveComponent<IMoverComponent>();
}
var comp = ent.AddComponent<AiControllerComponent>();
comp.LogicName = processorId;
shell.SendText(player, "AI component added.");
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.WorldState;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
public class AiActionRequest
{
public EntityUid EntityUid { get; }
public Blackboard Context { get; }
public IEnumerable<IAiUtility> Actions { get; }
public AiActionRequest(EntityUid uid, Blackboard context, IEnumerable<IAiUtility> actions)
{
EntityUid = uid;
Context = context;
Actions = actions;
}
}
}

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Utility;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.AI;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
public class AiActionRequestJob : Job<UtilityAction>
{
#if DEBUG
public static event Action<SharedAiDebug.UtilityAiDebugMessage> FoundAction;
#endif
private readonly AiActionRequest _request;
public AiActionRequestJob(
double maxTime,
AiActionRequest request,
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
{
_request = request;
}
protected override async Task<UtilityAction> Process()
{
if (_request.Context == null)
{
return null;
}
var entity = _request.Context.GetState<SelfState>().GetValue();
if (entity == null || !entity.HasComponent<AiControllerComponent>())
{
return null;
}
if (_request.Actions == null || _request.Context == null)
{
return null;
}
var consideredTaskCount = 0;
// Actions are pre-sorted
var actions = new Stack<IAiUtility>(_request.Actions);
// So essentially we go through and once we have a valid score that score becomes the cutoff;
// once the bonus of new tasks is below the cutoff we can stop evaluating.
// Use last action as the basis for the cutoff
var cutoff = _request.Context.GetState<LastUtilityScoreState>().GetValue();
UtilityAction foundAction = null;
// To see what I was trying to do watch these 2 videos about Infinite Axis Utility System (IAUS):
// Architecture Tricks: Managing Behaviors in Time, Space, and Depth
// Building a Better Centaur
// We'll want to cap the considered entities at some point, e.g. if 500 guns are in a stack cap it at 256 or whatever
while (actions.Count > 0)
{
if (consideredTaskCount > 0 && consideredTaskCount % 5 == 0)
{
await SuspendIfOutOfTime();
// If this happens then that means something changed when we resumed so ABORT
if (actions.Count == 0 || _request.Context == null)
{
return null;
}
}
var action = actions.Pop();
switch (action)
{
case ExpandableUtilityAction expandableUtilityAction:
foreach (var expanded in expandableUtilityAction.GetActions(_request.Context))
{
actions.Push(expanded);
}
break;
case UtilityAction utilityAction:
consideredTaskCount++;
var bonus = (float) utilityAction.Bonus;
if (bonus < cutoff)
{
// We know none of the other actions can beat this as they're pre-sorted
actions.Clear();
break;
}
var score = utilityAction.GetScore(_request.Context, cutoff);
if (score > cutoff)
{
foundAction = utilityAction;
cutoff = score;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
_request.Context.GetState<LastUtilityScoreState>().SetValue(cutoff);
#if DEBUG
if (foundAction != null)
{
FoundAction?.Invoke(new SharedAiDebug.UtilityAiDebugMessage(
_request.Context.GetState<SelfState>().GetValue().Uid,
DebugTime,
cutoff,
foundAction.GetType().Name,
consideredTaskCount));
}
#endif
_request.Context.ResetPlanning();
return foundAction;
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading;
using Content.Server.GameObjects.EntitySystems.JobQueues.Queues;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
/// <summary>
/// This will queue up an AI's request for an action and give it one when possible
/// </summary>
public class AiActionSystem : EntitySystem
{
private readonly AiActionJobQueue _aiRequestQueue = new AiActionJobQueue();
public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken)
{
var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
// AI should already know if it shouldn't request again
_aiRequestQueue.EnqueueJob(job);
return job;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_aiRequestQueue.Process();
}
}
}

View File

@@ -0,0 +1,14 @@
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;
}
}
}

View File

@@ -0,0 +1,16 @@
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;
}
}
}

View File

@@ -0,0 +1,14 @@
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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates
{
public interface IPathfindingGraphUpdate
{
}
}

View File

@@ -0,0 +1,14 @@
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; }
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Content.Shared.AI;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class AStarPathfindingJob : Job<Queue<TileRef>>
{
public static event Action<SharedAiDebug.AStarRouteDebug> DebugRoute;
private PathfindingNode _startNode;
private PathfindingNode _endNode;
private PathfindingArgs _pathfindingArgs;
public AStarPathfindingJob(
double maxTime,
PathfindingNode startNode,
PathfindingNode endNode,
PathfindingArgs pathfindingArgs,
CancellationToken cancellationToken) : base(maxTime, cancellationToken)
{
_startNode = startNode;
_endNode = endNode;
_pathfindingArgs = pathfindingArgs;
}
protected override async Task<Queue<TileRef>> Process()
{
if (_startNode == null ||
_endNode == null ||
Status == JobStatus.Finished)
{
return null;
}
// If we couldn't get a nearby node that's good enough
if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs))
{
return null;
}
var openTiles = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
var gScores = new Dictionary<PathfindingNode, float>();
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
var closedTiles = new HashSet<PathfindingNode>();
PathfindingNode currentNode = null;
openTiles.Add((0.0f, _startNode));
gScores[_startNode] = 0.0f;
var routeFound = false;
var count = 0;
while (openTiles.Count > 0)
{
count++;
if (count % 20 == 0 && count > 0)
{
await SuspendIfOutOfTime();
}
if (_startNode == null || _endNode == null)
{
return null;
}
(_, currentNode) = openTiles.Take();
if (currentNode.Equals(_endNode))
{
routeFound = true;
break;
}
closedTiles.Add(currentNode);
foreach (var (direction, nextNode) in currentNode.Neighbors)
{
if (closedTiles.Contains(nextNode))
{
continue;
}
// If tile is untraversable it'll be null
var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, nextNode);
if (tileCost == null || !Utils.DirectionTraversable(_pathfindingArgs.CollisionMask, currentNode, direction))
{
continue;
}
var gScore = gScores[currentNode] + tileCost.Value;
if (gScores.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[nextNode] = currentNode;
gScores[nextNode] = gScore;
// 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);
openTiles.Add((fScore, nextNode));
}
}
if (!routeFound)
{
return null;
}
var route = Utils.ReconstructPath(cameFrom, currentNode);
if (route.Count == 1)
{
return null;
}
#if DEBUG
// Need to get data into an easier format to send to the relevant clients
if (DebugRoute != null && route.Count > 0)
{
var debugCameFrom = new Dictionary<TileRef, TileRef>(cameFrom.Count);
var debugGScores = new Dictionary<TileRef, float>(gScores.Count);
var debugClosedTiles = new HashSet<TileRef>(closedTiles.Count);
foreach (var (node, parent) in cameFrom)
{
debugCameFrom.Add(node.TileRef, parent.TileRef);
}
foreach (var (node, score) in gScores)
{
debugGScores.Add(node.TileRef, score);
}
foreach (var node in closedTiles)
{
debugClosedTiles.Add(node.TileRef);
}
var debugRoute = new SharedAiDebug.AStarRouteDebug(
_pathfindingArgs.Uid,
route,
debugCameFrom,
debugGScores,
debugClosedTiles,
DebugTime);
DebugRoute.Invoke(debugRoute);
}
#endif
return route;
}
}
}

View File

@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Content.Shared.AI;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class JpsPathfindingJob : Job<Queue<TileRef>>
{
public static event Action<SharedAiDebug.JpsRouteDebug> DebugRoute;
private PathfindingNode _startNode;
private PathfindingNode _endNode;
private PathfindingArgs _pathfindingArgs;
public JpsPathfindingJob(double maxTime,
PathfindingNode startNode,
PathfindingNode endNode,
PathfindingArgs pathfindingArgs,
CancellationToken cancellationToken) : base(maxTime, cancellationToken)
{
_startNode = startNode;
_endNode = endNode;
_pathfindingArgs = pathfindingArgs;
}
protected override async Task<Queue<TileRef>> Process()
{
// VERY similar to A*; main difference is with the neighbor tiles you look for jump nodes instead
if (_startNode == null ||
_endNode == null)
{
return null;
}
// If we couldn't get a nearby node that's good enough
if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs))
{
return null;
}
var openTiles = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
var gScores = new Dictionary<PathfindingNode, float>();
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
var closedTiles = new HashSet<PathfindingNode>();
#if DEBUG
var jumpNodes = new HashSet<PathfindingNode>();
#endif
PathfindingNode currentNode = null;
openTiles.Add((0, _startNode));
gScores[_startNode] = 0.0f;
var routeFound = false;
var count = 0;
while (openTiles.Count > 0)
{
count++;
// JPS probably getting a lot fewer nodes than A* is
if (count % 5 == 0 && count > 0)
{
await SuspendIfOutOfTime();
}
(_, currentNode) = openTiles.Take();
if (currentNode.Equals(_endNode))
{
routeFound = true;
break;
}
foreach (var (direction, _) in currentNode.Neighbors)
{
var jumpNode = GetJumpPoint(currentNode, direction, _endNode);
if (jumpNode != null && !closedTiles.Contains(jumpNode))
{
closedTiles.Add(jumpNode);
#if DEBUG
jumpNodes.Add(jumpNode);
#endif
// GetJumpPoint should already check if we can traverse to the node
var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, jumpNode);
if (tileCost == null)
{
throw new InvalidOperationException();
}
var gScore = gScores[currentNode] + tileCost.Value;
if (gScores.TryGetValue(jumpNode, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[jumpNode] = currentNode;
gScores[jumpNode] = gScore;
// 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);
openTiles.Add((fScore, jumpNode));
}
}
}
if (!routeFound)
{
return null;
}
var route = Utils.ReconstructJumpPath(cameFrom, currentNode);
if (route.Count == 1)
{
return null;
}
#if DEBUG
// Need to get data into an easier format to send to the relevant clients
if (DebugRoute != null && route.Count > 0)
{
var debugJumpNodes = new HashSet<TileRef>(jumpNodes.Count);
foreach (var node in jumpNodes)
{
debugJumpNodes.Add(node.TileRef);
}
var debugRoute = new SharedAiDebug.JpsRouteDebug(
_pathfindingArgs.Uid,
route,
debugJumpNodes,
DebugTime);
DebugRoute.Invoke(debugRoute);
}
#endif
return route;
}
private PathfindingNode GetJumpPoint(PathfindingNode currentNode, Direction direction, PathfindingNode endNode)
{
var count = 0;
while (count < 1000)
{
count++;
var nextNode = currentNode.GetNeighbor(direction);
// 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)
{
return null;
}
if (nextNode == endNode)
{
return endNode;
}
// Horizontal and vertical are treated the same i.e.
// They only check in their specific direction
// (So Going North means you check NorthWest and NorthEast to see if we're a jump point)
// Diagonals also check the cardinal directions at the same time at the same time
// See https://harablog.wordpress.com/2011/09/07/jump-point-search/ for original description
switch (direction)
{
case Direction.East:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.NorthEast:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
{
return nextNode;
}
break;
case Direction.North:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.NorthWest:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
{
return nextNode;
}
break;
case Direction.West:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.SouthWest:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
{
return nextNode;
}
break;
case Direction.South:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.SouthEast:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
{
return nextNode;
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
currentNode = nextNode;
}
Logger.WarningS("pathfinding", "Recursion found in JPS pathfinder");
return null;
}
private bool IsDiagonalJumpPoint(Direction direction, PathfindingNode currentNode)
{
// If we're going diagonally need to check all cardinals.
// I just just using casts int casts and offset to make it smaller but brain no workyand it wasn't working.
// From NorthEast we check (Closed / Open) S - SE, W - NW
PathfindingNode openNeighborOne;
PathfindingNode closedNeighborOne;
PathfindingNode openNeighborTwo;
PathfindingNode closedNeighborTwo;
switch (direction)
{
case Direction.NorthEast:
openNeighborOne = currentNode.GetNeighbor(Direction.SouthEast);
closedNeighborOne = currentNode.GetNeighbor(Direction.South);
openNeighborTwo = currentNode.GetNeighbor(Direction.NorthWest);
closedNeighborTwo = currentNode.GetNeighbor(Direction.West);
break;
case Direction.SouthEast:
openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast);
closedNeighborOne = currentNode.GetNeighbor(Direction.North);
openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest);
closedNeighborTwo = currentNode.GetNeighbor(Direction.West);
break;
case Direction.SouthWest:
openNeighborOne = currentNode.GetNeighbor(Direction.NorthWest);
closedNeighborOne = currentNode.GetNeighbor(Direction.North);
openNeighborTwo = currentNode.GetNeighbor(Direction.SouthEast);
closedNeighborTwo = currentNode.GetNeighbor(Direction.East);
break;
case Direction.NorthWest:
openNeighborOne = currentNode.GetNeighbor(Direction.SouthWest);
closedNeighborOne = currentNode.GetNeighbor(Direction.South);
openNeighborTwo = currentNode.GetNeighbor(Direction.NorthEast);
closedNeighborTwo = currentNode.GetNeighbor(Direction.East);
break;
default:
throw new ArgumentOutOfRangeException();
}
if ((closedNeighborOne == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null)
&& openNeighborOne != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null)
{
return true;
}
if ((closedNeighborTwo == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null)
&& openNeighborTwo != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null)
{
return true;
}
return false;
}
/// <summary>
/// Check to see if the node is a jump point (only works for cardinal directions)
/// </summary>
private bool IsCardinalJumpPoint(Direction direction, PathfindingNode currentNode)
{
PathfindingNode openNeighborOne;
PathfindingNode closedNeighborOne;
PathfindingNode openNeighborTwo;
PathfindingNode closedNeighborTwo;
switch (direction)
{
case Direction.North:
openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast);
closedNeighborOne = currentNode.GetNeighbor(Direction.East);
openNeighborTwo = currentNode.GetNeighbor(Direction.NorthWest);
closedNeighborTwo = currentNode.GetNeighbor(Direction.West);
break;
case Direction.East:
openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast);
closedNeighborOne = currentNode.GetNeighbor(Direction.North);
openNeighborTwo = currentNode.GetNeighbor(Direction.SouthEast);
closedNeighborTwo = currentNode.GetNeighbor(Direction.South);
break;
case Direction.South:
openNeighborOne = currentNode.GetNeighbor(Direction.SouthEast);
closedNeighborOne = currentNode.GetNeighbor(Direction.East);
openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest);
closedNeighborTwo = currentNode.GetNeighbor(Direction.West);
break;
case Direction.West:
openNeighborOne = currentNode.GetNeighbor(Direction.NorthWest);
closedNeighborOne = currentNode.GetNeighbor(Direction.North);
openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest);
closedNeighborTwo = currentNode.GetNeighbor(Direction.South);
break;
default:
throw new ArgumentOutOfRangeException();
}
if ((closedNeighborOne == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborOne.CollisionMask)) &&
(openNeighborOne != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborOne.CollisionMask)))
{
return true;
}
if ((closedNeighborTwo == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborTwo.CollisionMask)) &&
(openNeighborTwo != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborTwo.CollisionMask)))
{
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,41 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public struct PathfindingArgs
{
public EntityUid Uid { get; }
public int CollisionMask { get; }
public TileRef Start { get; }
public TileRef End { get; }
// How close we need to get to the endpoint to be 'done'
public float Proximity { get; }
// Whether we use cardinal only or not
public bool AllowDiagonals { get; }
// Can we go through walls
public bool NoClip { get; }
// Can we traverse space tiles
public bool AllowSpace { get; }
public PathfindingArgs(
EntityUid entityUid,
int collisionMask,
TileRef start,
TileRef end,
float proximity = 0.0f,
bool allowDiagonals = true,
bool noClip = false,
bool allowSpace = false)
{
Uid = entityUid;
CollisionMask = collisionMask;
Start = start;
End = end;
Proximity = proximity;
AllowDiagonals = allowDiagonals;
NoClip = noClip;
AllowSpace = allowSpace;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class PathfindingComparer : IComparer<ValueTuple<float, PathfindingNode>>
{
public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y)
{
return y.Item1.CompareTo(x.Item1);
}
}
}

View File

@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
public class PathfindingChunk
{
public GridId GridId { get; }
public MapIndices Indices => _indices;
private readonly MapIndices _indices;
// Nodes per chunk row
public static int ChunkSize => 16;
public PathfindingNode[,] Nodes => _nodes;
private PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
public Dictionary<Direction, PathfindingChunk> Neighbors { get; } = new Dictionary<Direction, PathfindingChunk>(8);
public PathfindingChunk(GridId gridId, MapIndices indices)
{
GridId = gridId;
_indices = indices;
}
public void Initialize()
{
var grid = IoCManager.Resolve<IMapManager>().GetGrid(GridId);
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var tileRef = grid.GetTileRef(new MapIndices(x + _indices.X, y + _indices.Y));
CreateNode(tileRef);
}
}
RefreshNodeNeighbors();
}
/// <summary>
/// Updates all internal nodes with references to every other internal node
/// </summary>
private void RefreshNodeNeighbors()
{
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var node = _nodes[x, y];
// West
if (x != 0)
{
if (y != ChunkSize - 1)
{
node.AddNeighbor(Direction.NorthWest, _nodes[x - 1, y + 1]);
}
node.AddNeighbor(Direction.West, _nodes[x - 1, y]);
if (y != 0)
{
node.AddNeighbor(Direction.SouthWest, _nodes[x - 1, y - 1]);
}
}
// Same column
if (y != ChunkSize - 1)
{
node.AddNeighbor(Direction.North, _nodes[x, y + 1]);
}
if (y != 0)
{
node.AddNeighbor(Direction.South, _nodes[x, y - 1]);
}
// East
if (x != ChunkSize - 1)
{
if (y != ChunkSize - 1)
{
node.AddNeighbor(Direction.NorthEast, _nodes[x + 1, y + 1]);
}
node.AddNeighbor(Direction.East, _nodes[x + 1, y]);
if (y != 0)
{
node.AddNeighbor(Direction.SouthEast, _nodes[x + 1, y - 1]);
}
}
}
}
}
/// <summary>
/// This will work both ways
/// </summary>
/// <param name="chunk"></param>
/// <exception cref="InvalidOperationException"></exception>
public void AddNeighbor(PathfindingChunk chunk)
{
if (chunk == this) return;
if (Neighbors.ContainsValue(chunk))
{
return;
}
Direction direction;
if (chunk.Indices.X < _indices.X)
{
if (chunk.Indices.Y > _indices.Y)
{
direction = Direction.NorthWest;
} else if (chunk.Indices.Y < _indices.Y)
{
direction = Direction.SouthWest;
}
else
{
direction = Direction.West;
}
}
else if (chunk.Indices.X > _indices.X)
{
if (chunk.Indices.Y > _indices.Y)
{
direction = Direction.NorthEast;
} else if (chunk.Indices.Y < _indices.Y)
{
direction = Direction.SouthEast;
}
else
{
direction = Direction.East;
}
}
else
{
if (chunk.Indices.Y > _indices.Y)
{
direction = Direction.North;
} else if (chunk.Indices.Y < _indices.Y)
{
direction = Direction.South;
}
else
{
throw new InvalidOperationException();
}
}
Neighbors.TryAdd(direction, chunk);
foreach (var node in GetBorderNodes(direction))
{
foreach (var counter in chunk.GetCounterpartNodes(direction))
{
var xDiff = node.TileRef.X - counter.TileRef.X;
var yDiff = node.TileRef.Y - counter.TileRef.Y;
if (Math.Abs(xDiff) <= 1 && Math.Abs(yDiff) <= 1)
{
node.AddNeighbor(counter);
counter.AddNeighbor(node);
}
}
}
chunk.Neighbors.TryAdd(OppositeDirection(direction), this);
if (Neighbors.Count > 8)
{
throw new InvalidOperationException();
}
}
private Direction OppositeDirection(Direction direction)
{
return (Direction) (((int) direction + 4) % 8);
}
// TODO I was too tired to think of an easier system. Could probably just google an array wraparound
private IEnumerable<PathfindingNode> GetCounterpartNodes(Direction direction)
{
switch (direction)
{
case Direction.West:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[ChunkSize - 1, i];
}
break;
case Direction.SouthWest:
yield return _nodes[ChunkSize - 1, ChunkSize - 1];
break;
case Direction.South:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[i, ChunkSize - 1];
}
break;
case Direction.SouthEast:
yield return _nodes[0, ChunkSize - 1];
break;
case Direction.East:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[0, i];
}
break;
case Direction.NorthEast:
yield return _nodes[0, 0];
break;
case Direction.North:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[i, 0];
}
break;
case Direction.NorthWest:
yield return _nodes[ChunkSize - 1, 0];
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
}
public IEnumerable<PathfindingNode> GetBorderNodes(Direction direction)
{
switch (direction)
{
case Direction.East:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[ChunkSize - 1, i];
}
break;
case Direction.NorthEast:
yield return _nodes[ChunkSize - 1, ChunkSize - 1];
break;
case Direction.North:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[i, ChunkSize - 1];
}
break;
case Direction.NorthWest:
yield return _nodes[0, ChunkSize - 1];
break;
case Direction.West:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[0, i];
}
break;
case Direction.SouthWest:
yield return _nodes[0, 0];
break;
case Direction.South:
for (var i = 0; i < ChunkSize; i++)
{
yield return _nodes[i, 0];
}
break;
case Direction.SouthEast:
yield return _nodes[ChunkSize - 1, 0];
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
}
public bool InBounds(TileRef tile)
{
if (tile.X < _indices.X || tile.Y < _indices.Y) return false;
if (tile.X >= _indices.X + ChunkSize || tile.Y >= _indices.Y + ChunkSize) return false;
return true;
}
/// <summary>
/// Returns true if the tile is on the outer edge
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public bool OnEdge(PathfindingNode node)
{
if (node.TileRef.X == _indices.X) return true;
if (node.TileRef.Y == _indices.Y) return true;
if (node.TileRef.X == _indices.X + ChunkSize - 1) return true;
if (node.TileRef.Y == _indices.Y + ChunkSize - 1) return true;
return false;
}
public PathfindingNode GetNode(TileRef tile)
{
var chunkX = tile.X - _indices.X;
var chunkY = tile.Y - _indices.Y;
return _nodes[chunkX, chunkY];
}
public void UpdateNode(TileRef tile)
{
var node = GetNode(tile);
node.UpdateTile(tile);
}
private void CreateNode(TileRef tile, PathfindingChunk parent = null)
{
if (parent == null)
{
parent = this;
}
var node = new PathfindingNode(parent, tile);
var offsetX = tile.X - Indices.X;
var offsetY = tile.Y - Indices.Y;
_nodes[offsetX, offsetY] = node;
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
using Robust.Shared.Map;
using Robust.Shared.Maths;
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<int> CollisionLayers { get; }
public int CollisionMask { get; private set; }
public Dictionary<Direction, PathfindingNode> Neighbors => _neighbors;
private Dictionary<Direction, PathfindingNode> _neighbors = new Dictionary<Direction, PathfindingNode>();
public PathfindingNode(PathfindingChunk parent, TileRef tileRef, List<int> collisionLayers = null)
{
_parentChunk = parent;
TileRef = tileRef;
if (collisionLayers == null)
{
CollisionLayers = new List<int>();
}
else
{
CollisionLayers = collisionLayers;
}
GenerateMask();
}
public void AddNeighbor(Direction direction, PathfindingNode node)
{
_neighbors.Add(direction, node);
}
public void AddNeighbor(PathfindingNode node)
{
if (node.TileRef.GridIndex != TileRef.GridIndex)
{
throw new InvalidOperationException();
}
Direction direction;
if (node.TileRef.X < TileRef.X)
{
if (node.TileRef.Y > TileRef.Y)
{
direction = Direction.NorthWest;
} else if (node.TileRef.Y < TileRef.Y)
{
direction = Direction.SouthWest;
}
else
{
direction = Direction.West;
}
}
else if (node.TileRef.X > TileRef.X)
{
if (node.TileRef.Y > TileRef.Y)
{
direction = Direction.NorthEast;
} else if (node.TileRef.Y < TileRef.Y)
{
direction = Direction.SouthEast;
}
else
{
direction = Direction.East;
}
}
else
{
if (node.TileRef.Y > TileRef.Y)
{
direction = Direction.North;
}
else
{
direction = Direction.South;
}
}
if (_neighbors.ContainsKey(direction))
{
// Should we verify that they align?
return;
}
_neighbors.Add(direction, node);
}
public PathfindingNode GetNeighbor(Direction direction)
{
_neighbors.TryGetValue(direction, out var node);
return node;
}
public void UpdateTile(TileRef newTile)
{
TileRef = newTile;
}
public void AddCollisionLayer(int layer)
{
CollisionLayers.Add(layer);
GenerateMask();
}
public void RemoveCollisionLayer(int layer)
{
CollisionLayers.Remove(layer);
GenerateMask();
}
private void GenerateMask()
{
CollisionMask = 0x0;
foreach (var layer in CollisionLayers)
{
CollisionMask |= layer;
}
}
}
}

View File

@@ -0,0 +1,357 @@
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.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Server.GameObjects.EntitySystems.JobQueues.Queues;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
/*
// TODO: IMO use rectangular symmetry reduction on the nodes with collision at all., or
alternatively store all rooms and have an alternative graph for humanoid mobs (same collision mask, needs access etc). You could also just path from room to room as needed.
// TODO: Longer term -> Handle collision layer changes?
*/
/// <summary>
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
/// </summary>
public class PathfindingSystem : EntitySystem
{
#pragma warning disable 649
[Dependency] private readonly IMapManager _mapManager;
#pragma warning restore 649
public IReadOnlyDictionary<GridId, Dictionary<MapIndices, PathfindingChunk>> Graph => _graph;
private readonly Dictionary<GridId, Dictionary<MapIndices, PathfindingChunk>> _graph = new Dictionary<GridId, Dictionary<MapIndices, PathfindingChunk>>();
// Every tick we queue up all the changes and do them at once
private readonly Queue<IPathfindingGraphUpdate> _queuedGraphUpdates = new Queue<IPathfindingGraphUpdate>();
private readonly PathfindingJobQueue _pathfindingQueue = new PathfindingJobQueue();
// Need to store previously known entity positions for collidables for when they move
private readonly Dictionary<IEntity, TileRef> _lastKnownPositions = new Dictionary<IEntity, TileRef>();
/// <summary>
/// Ask for the pathfinder to gimme somethin
/// </summary>
/// <param name="pathfindingArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Job<Queue<TileRef>> RequestPath(PathfindingArgs pathfindingArgs, CancellationToken cancellationToken)
{
var startNode = GetNode(pathfindingArgs.Start);
var endNode = GetNode(pathfindingArgs.End);
var job = new AStarPathfindingJob(0.003, startNode, endNode, pathfindingArgs, cancellationToken);
_pathfindingQueue.EnqueueJob(job);
return job;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Make sure graph is updated, then get pathfinders
ProcessGraphUpdates();
_pathfindingQueue.Process();
}
private void ProcessGraphUpdates()
{
for (var i = 0; i < Math.Min(50, _queuedGraphUpdates.Count); i++)
{
var update = _queuedGraphUpdates.Dequeue();
switch (update)
{
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();
}
}
}
private void HandleGridRemoval(GridRemoval removal)
{
if (!_graph.ContainsKey(removal.GridId))
{
throw new InvalidOperationException();
}
_graph.Remove(removal.GridId);
}
private void HandleTileUpdate(TileUpdate tile)
{
var chunk = GetChunk(tile.Tile);
chunk.UpdateNode(tile.Tile);
}
public PathfindingChunk GetChunk(TileRef tile)
{
var chunkX = (int) (Math.Floor((float) tile.X / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
var chunkY = (int) (Math.Floor((float) tile.Y / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
var mapIndices = new MapIndices(chunkX, chunkY);
if (_graph.TryGetValue(tile.GridIndex, out var chunks))
{
if (!chunks.ContainsKey(mapIndices))
{
CreateChunk(tile.GridIndex, mapIndices);
}
return chunks[mapIndices];
}
var newChunk = CreateChunk(tile.GridIndex, mapIndices);
return newChunk;
}
private PathfindingChunk CreateChunk(GridId gridId, MapIndices indices)
{
var newChunk = new PathfindingChunk(gridId, indices);
newChunk.Initialize();
if (_graph.TryGetValue(gridId, out var chunks))
{
for (var x = -1; x < 2; x++)
{
for (var y = -1; y < 2; y++)
{
if (x == 0 && y == 0) continue;
var neighborIndices = new MapIndices(
indices.X + x * PathfindingChunk.ChunkSize,
indices.Y + y * PathfindingChunk.ChunkSize);
if (chunks.TryGetValue(neighborIndices, out var neighborChunk))
{
neighborChunk.AddNeighbor(newChunk);
}
}
}
}
else
{
_graph.Add(gridId, new Dictionary<MapIndices, PathfindingChunk>());
}
_graph[gridId].Add(indices, newChunk);
return newChunk;
}
public PathfindingNode GetNode(TileRef tile)
{
var chunk = GetChunk(tile);
var node = chunk.GetNode(tile);
return node;
}
public override void Initialize()
{
IoCManager.InjectDependencies(this);
SubscribeLocalEvent<CollisionChangeEvent>(QueueCollisionEnabledEvent);
SubscribeLocalEvent<MoveEvent>(QueueCollidableMove);
// Handle all the base grid changes
// Anything that affects traversal (i.e. collision layer) is handled separately.
_mapManager.OnGridRemoved += QueueGridRemoval;
_mapManager.GridChanged += QueueGridChange;
_mapManager.TileChanged += QueueTileChange;
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.OnGridRemoved -= QueueGridRemoval;
_mapManager.GridChanged -= QueueGridChange;
_mapManager.TileChanged -= QueueTileChange;
}
private void QueueGridRemoval(GridId gridId)
{
_queuedGraphUpdates.Enqueue(new GridRemoval(gridId));
}
private void QueueGridChange(object sender, GridChangedEventArgs eventArgs)
{
foreach (var (position, _) in eventArgs.Modified)
{
_queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.Grid.GetTileRef(position)));
}
}
private void QueueTileChange(object sender, TileChangedEventArgs eventArgs)
{
_queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.NewTile));
}
#region collidable
/// <summary>
/// If an entity's collision gets turned on then we need to update its current position
/// </summary>
/// <param name="entity"></param>
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<ServerDoorComponent>() ||
entity.HasComponent<AirlockComponent>() ||
_lastKnownPositions.ContainsKey(entity))
{
return;
}
var grid = _mapManager.GetGrid(entity.Transform.GridID);
var tileRef = grid.GetTileRef(entity.Transform.GridPosition);
var collisionLayer = entity.GetComponent<CollidableComponent>().CollisionLayer;
var chunk = GetChunk(tileRef);
var node = chunk.GetNode(tileRef);
node.AddCollisionLayer(collisionLayer);
_lastKnownPositions.Add(entity, tileRef);
}
/// <summary>
/// If an entity's collision is removed then stop tracking it from the graph
/// </summary>
/// <param name="entity"></param>
private void HandleCollidableRemove(IEntity entity)
{
if (entity.Prototype == null ||
entity.Deleted ||
entity.HasComponent<ServerDoorComponent>() ||
entity.HasComponent<AirlockComponent>() ||
!_lastKnownPositions.ContainsKey(entity))
{
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);
}
private void QueueCollidableMove(MoveEvent moveEvent)
{
_queuedGraphUpdates.Enqueue(new CollidableMove(moveEvent));
}
private void HandleCollidableMove(CollidableMove move)
{
if (!_lastKnownPositions.ContainsKey(move.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);
return;
}
_lastKnownPositions.TryGetValue(moveEvent.Sender, out var oldTile);
var newTile = _mapManager.GetGrid(moveEvent.NewPosition.GridID).GetTileRef(moveEvent.NewPosition);
if (oldTile == newTile)
{
return;
}
_lastKnownPositions[moveEvent.Sender] = newTile;
if (!moveEvent.Sender.TryGetComponent(out CollidableComponent collidableComponent))
{
HandleCollidableRemove(moveEvent.Sender);
return;
}
var collisionLayer = collidableComponent.CollisionLayer;
var gridIds = new HashSet<GridId>(2) {oldTile.GridIndex, newTile.GridIndex};
foreach (var gridId in gridIds)
{
if (oldTile.GridIndex == gridId)
{
var oldChunk = GetChunk(oldTile);
var oldNode = oldChunk.GetNode(oldTile);
oldNode.RemoveCollisionLayer(collisionLayer);
}
if (newTile.GridIndex == gridId)
{
var newChunk = GetChunk(newTile);
var newNode = newChunk.GetNode(newTile);
newNode.RemoveCollisionLayer(collisionLayer);
}
}
}
private void QueueCollisionEnabledEvent(CollisionChangeEvent collisionEvent)
{
// TODO: Handle containers
var entityManager = IoCManager.Resolve<IEntityManager>();
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;
}
}
#endregion
}
}

View File

@@ -0,0 +1,136 @@
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Shared.AI;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
#if DEBUG
[UsedImplicitly]
public class ServerPathfindingDebugSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
SubscribeNetworkEvent<SharedAiDebug.RequestPathfindingGraphMessage>(DispatchGraph);
}
public override void Shutdown()
{
base.Shutdown();
AStarPathfindingJob.DebugRoute -= DispatchAStarDebug;
JpsPathfindingJob.DebugRoute -= DispatchJpsDebug;
}
private void DispatchAStarDebug(SharedAiDebug.AStarRouteDebug routeDebug)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var route = new List<Vector2>();
foreach (var tile in routeDebug.Route)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
route.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position);
}
var cameFrom = new Dictionary<Vector2, Vector2>();
foreach (var (from, to) in routeDebug.CameFrom)
{
var tileOneGrid = mapManager.GetGrid(from.GridIndex).GridTileToLocal(from.GridIndices);
var tileOneWorld = mapManager.GetGrid(from.GridIndex).LocalToWorld(tileOneGrid).Position;
var tileTwoGrid = mapManager.GetGrid(to.GridIndex).GridTileToLocal(to.GridIndices);
var tileTwoWorld = mapManager.GetGrid(to.GridIndex).LocalToWorld(tileTwoGrid).Position;
cameFrom.Add(tileOneWorld, tileTwoWorld);
}
var gScores = new Dictionary<Vector2, float>();
foreach (var (tile, score) in routeDebug.GScores)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
gScores.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position, score);
}
var closedTiles = new List<Vector2>();
foreach (var tile in routeDebug.ClosedTiles)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
closedTiles.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position);
}
var systemMessage = new SharedAiDebug.AStarRouteMessage(
routeDebug.EntityUid,
route,
cameFrom,
gScores,
closedTiles,
routeDebug.TimeTaken
);
EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
}
private void DispatchJpsDebug(SharedAiDebug.JpsRouteDebug routeDebug)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var route = new List<Vector2>();
foreach (var tile in routeDebug.Route)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
route.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position);
}
var jumpNodes = new List<Vector2>();
foreach (var tile in routeDebug.JumpNodes)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
jumpNodes.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position);
}
var systemMessage = new SharedAiDebug.JpsRouteMessage(
routeDebug.EntityUid,
route,
jumpNodes,
routeDebug.TimeTaken
);
EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
}
private void DispatchGraph(SharedAiDebug.RequestPathfindingGraphMessage message)
{
var pathfindingSystem = EntitySystemManager.GetEntitySystem<PathfindingSystem>();
var mapManager = IoCManager.Resolve<IMapManager>();
var result = new Dictionary<int, List<Vector2>>();
var idx = 0;
foreach (var (gridId, chunks) in pathfindingSystem.Graph)
{
var gridManager = mapManager.GetGrid(gridId);
foreach (var chunk in chunks.Values)
{
var nodes = new List<Vector2>();
foreach (var node in chunk.Nodes)
{
var worldTile = gridManager.GridTileToWorldPos(node.TileRef.GridIndices);
nodes.Add(worldTile);
}
result.Add(idx, nodes);
idx++;
}
}
var systemMessage = new SharedAiDebug.PathfindingGraphMessage(result);
EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
}
}
#endif
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
public static class Utils
{
public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs)
{
if (!Traversable(pathfindingArgs.CollisionMask, endNode.CollisionMask))
{
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)
{
if (Traversable(pathfindingArgs.CollisionMask, node.CollisionMask))
{
endNode = node;
return true;
}
}
}
return false;
}
return true;
}
public static bool DirectionTraversable(int collisionMask, 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
// Also this will help with corner-cutting
currentNode.Neighbors.TryGetValue(Direction.North, out var northNeighbor);
currentNode.Neighbors.TryGetValue(Direction.South, out var southNeighbor);
currentNode.Neighbors.TryGetValue(Direction.East, out var eastNeighbor);
currentNode.Neighbors.TryGetValue(Direction.West, out var westNeighbor);
switch (direction)
{
case Direction.NorthEast:
if (northNeighbor == null || eastNeighbor == null) return false;
if (!Traversable(collisionMask, northNeighbor.CollisionMask) ||
!Traversable(collisionMask, eastNeighbor.CollisionMask))
{
return false;
}
break;
case Direction.NorthWest:
if (northNeighbor == null || westNeighbor == null) return false;
if (!Traversable(collisionMask, northNeighbor.CollisionMask) ||
!Traversable(collisionMask, westNeighbor.CollisionMask))
{
return false;
}
break;
case Direction.SouthWest:
if (southNeighbor == null || westNeighbor == null) return false;
if (!Traversable(collisionMask, southNeighbor.CollisionMask) ||
!Traversable(collisionMask, westNeighbor.CollisionMask))
{
return false;
}
break;
case Direction.SouthEast:
if (southNeighbor == null || eastNeighbor == null) return false;
if (!Traversable(collisionMask, southNeighbor.CollisionMask) ||
!Traversable(collisionMask, eastNeighbor.CollisionMask))
{
return false;
}
break;
}
return true;
}
public static bool Traversable(int collisionMask, int nodeMask)
{
return (collisionMask & nodeMask) == 0;
}
public static Queue<TileRef> ReconstructPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
{
var running = new Stack<TileRef>();
running.Push(current.TileRef);
while (cameFrom.ContainsKey(current))
{
var previousCurrent = current;
current = cameFrom[current];
cameFrom.Remove(previousCurrent);
running.Push(current.TileRef);
}
var result = new Queue<TileRef>(running);
return result;
}
/// <summary>
/// This will reconstruct the path and fill in the tile holes as well
/// </summary>
/// <param name="cameFrom"></param>
/// <param name="current"></param>
/// <returns></returns>
public static Queue<TileRef> ReconstructJumpPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
{
var running = new Stack<TileRef>();
running.Push(current.TileRef);
while (cameFrom.ContainsKey(current))
{
var previousCurrent = current;
current = cameFrom[current];
var intermediate = previousCurrent;
cameFrom.Remove(previousCurrent);
var pathfindingSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
var mapManager = IoCManager.Resolve<IMapManager>();
var grid = mapManager.GetGrid(current.TileRef.GridIndex);
// Get all the intermediate nodes
while (true)
{
var xOffset = 0;
var yOffset = 0;
if (intermediate.TileRef.X < current.TileRef.X)
{
xOffset += 1;
}
else if (intermediate.TileRef.X > current.TileRef.X)
{
xOffset -= 1;
}
else
{
xOffset = 0;
}
if (intermediate.TileRef.Y < current.TileRef.Y)
{
yOffset += 1;
}
else if (intermediate.TileRef.Y > current.TileRef.Y)
{
yOffset -= 1;
}
else
{
yOffset = 0;
}
intermediate = pathfindingSystem.GetNode(grid.GetTileRef(
new MapIndices(intermediate.TileRef.X + xOffset, intermediate.TileRef.Y + yOffset)));
if (intermediate.TileRef != current.TileRef)
{
// Hacky corner cut fix
running.Push(intermediate.TileRef);
continue;
}
break;
}
running.Push(current.TileRef);
}
var result = new Queue<TileRef>(running);
return result;
}
public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode)
{
// "Fast Euclidean" / octile.
// This implementation is written down in a few sources; it just saves doing sqrt.
int dstX = Math.Abs(currentNode.TileRef.X - endNode.TileRef.X);
int dstY = Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
if (dstX > dstY)
{
return 1.4f * dstY + (dstX - dstY);
}
return 1.4f * dstX + (dstY - dstX);
}
public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode)
{
return Math.Abs(currentNode.TileRef.X - endNode.TileRef.X) + Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
}
public static float? GetTileCost(PathfindingArgs pathfindingArgs, PathfindingNode start, PathfindingNode end)
{
if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, end.CollisionMask))
{
return null;
}
if (!pathfindingArgs.AllowSpace && end.TileRef.Tile.IsEmpty)
{
return null;
}
var cost = 1.0f;
switch (pathfindingArgs.AllowDiagonals)
{
case true:
cost *= OctileDistance(end, start);
break;
// Manhattan distance
case false:
cost *= ManhattanDistance(end, start);
break;
}
return cost;
}
}
}

View File

@@ -0,0 +1,30 @@
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Shared.AI;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems.AI
{
#if DEBUG
[UsedImplicitly]
public class ServerAiDebugSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
AiActionRequestJob.FoundAction += NotifyActionJob;
}
public override void Shutdown()
{
base.Shutdown();
AiActionRequestJob.FoundAction -= NotifyActionJob;
}
private void NotifyActionJob(SharedAiDebug.UtilityAiDebugMessage message)
{
EntityManager.EntityNetManager.SendSystemNetworkMessage(message);
}
}
#endif
}