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:
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
@@ -13,8 +15,9 @@ using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems
|
||||
namespace Content.Server.GameObjects.EntitySystems.AI
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal class AiSystem : EntitySystem
|
||||
{
|
||||
#pragma warning disable 649
|
||||
@@ -47,6 +50,7 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
/// <inheritdoc />
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
|
||||
var entities = EntityManager.GetEntities(EntityQuery);
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
@@ -56,12 +60,7 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
}
|
||||
|
||||
var aiComp = entity.GetComponent<AiControllerComponent>();
|
||||
if (aiComp.Processor == null)
|
||||
{
|
||||
aiComp.Processor = CreateProcessor(aiComp.LogicName);
|
||||
aiComp.Processor.SelfEntity = entity;
|
||||
aiComp.Processor.VisionRadius = aiComp.VisionRadius;
|
||||
}
|
||||
ProcessorInitialize(aiComp);
|
||||
|
||||
var processor = aiComp.Processor;
|
||||
|
||||
@@ -69,11 +68,24 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
private AiLogicProcessor CreateProcessor(string name)
|
||||
/// <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 (AiLogicProcessor)_typeFactory.CreateInstance(type);
|
||||
return (UtilityAi)_typeFactory.CreateInstance(type);
|
||||
}
|
||||
|
||||
// processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates
|
||||
{
|
||||
public interface IPathfindingGraphUpdate
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
231
Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs
Normal file
231
Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -319,7 +320,7 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
{
|
||||
CommandBinds.Builder
|
||||
.Bind(EngineKeyFunctions.Use,
|
||||
new PointerInputCmdHandler(HandleUseItemInHand))
|
||||
new PointerInputCmdHandler(HandleClientUseItemInHand))
|
||||
.Bind(ContentKeyFunctions.WideAttack,
|
||||
new PointerInputCmdHandler(HandleWideAttack))
|
||||
.Bind(ContentKeyFunctions.ActivateItemInWorld,
|
||||
@@ -421,7 +422,31 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HandleUseItemInHand(ICommonSession session, GridCoordinates coords, EntityUid uid)
|
||||
/// <summary>
|
||||
/// Entity will try and use their active hand at the target location.
|
||||
/// Don't use for players
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="coords"></param>
|
||||
/// <param name="uid"></param>
|
||||
internal void UseItemInHand(IEntity entity, GridCoordinates coords, EntityUid uid)
|
||||
{
|
||||
if (entity.HasComponent<BasicActorComponent>())
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out CombatModeComponent combatMode) && combatMode.IsInCombatMode)
|
||||
{
|
||||
DoAttack(entity, coords);
|
||||
}
|
||||
else
|
||||
{
|
||||
UserInteraction(entity, coords, uid);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleClientUseItemInHand(ICommonSession session, GridCoordinates coords, EntityUid uid)
|
||||
{
|
||||
// client sanitization
|
||||
if (!_mapManager.GridExists(coords.GridID))
|
||||
|
||||
10
Content.Server/GameObjects/EntitySystems/JobQueues/IJob.cs
Normal file
10
Content.Server/GameObjects/EntitySystems/JobQueues/IJob.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues
|
||||
{
|
||||
public interface IJob
|
||||
{
|
||||
JobStatus Status { get; }
|
||||
void Run();
|
||||
}
|
||||
}
|
||||
232
Content.Server/GameObjects/EntitySystems/JobQueues/Job.cs
Normal file
232
Content.Server/GameObjects/EntitySystems/JobQueues/Job.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues
|
||||
{
|
||||
/// <summary>
|
||||
/// CPU-intensive job that can be suspended and resumed on the main thread
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations should overload <see cref="Process"/>.
|
||||
/// Inside <see cref="Process"/>, implementations should only await on <see cref="SuspendNow"/>,
|
||||
/// <see cref="SuspendIfOutOfTime"/>, or <see cref="WaitAsyncTask"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of result this job generates</typeparam>
|
||||
public abstract class Job<T> : IJob
|
||||
{
|
||||
public JobStatus Status { get; private set; } = JobStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of this job as a regular task.
|
||||
/// </summary>
|
||||
public Task<T> AsTask { get; }
|
||||
|
||||
public T Result { get; private set; }
|
||||
public Exception Exception { get; private set; }
|
||||
protected CancellationToken Cancellation { get; }
|
||||
|
||||
public double DebugTime { get; private set; }
|
||||
private readonly double _maxTime;
|
||||
protected readonly IStopwatch StopWatch;
|
||||
|
||||
// TCS for the Task property.
|
||||
private readonly TaskCompletionSource<T> _taskTcs;
|
||||
|
||||
// TCS to call to resume the suspended job.
|
||||
private TaskCompletionSource<object> _resume;
|
||||
private Task _workInProgress;
|
||||
|
||||
protected Job(double maxTime, CancellationToken cancellation = default)
|
||||
: this(maxTime, new Stopwatch(), cancellation)
|
||||
{
|
||||
}
|
||||
|
||||
protected Job(double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default)
|
||||
{
|
||||
_maxTime = maxTime;
|
||||
StopWatch = stopwatch;
|
||||
Cancellation = cancellation;
|
||||
|
||||
_taskTcs = new TaskCompletionSource<T>();
|
||||
AsTask = _taskTcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the current task immediately, yielding to other running jobs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not stop the job queue from un-suspending the current task immediately again,
|
||||
/// if there is still time left over.
|
||||
/// </remarks>
|
||||
protected Task SuspendNow()
|
||||
{
|
||||
DebugTools.AssertNull(_resume);
|
||||
|
||||
_resume = new TaskCompletionSource<object>();
|
||||
Status = JobStatus.Paused;
|
||||
DebugTime += StopWatch.Elapsed.TotalSeconds;
|
||||
return _resume.Task;
|
||||
}
|
||||
|
||||
protected ValueTask SuspendIfOutOfTime()
|
||||
{
|
||||
DebugTools.AssertNull(_resume);
|
||||
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
if (StopWatch.Elapsed.TotalSeconds <= _maxTime || _maxTime == 0.0)
|
||||
{
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
return new ValueTask(SuspendNow());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper to await on an external task.
|
||||
/// </summary>
|
||||
protected async Task<TTask> WaitAsyncTask<TTask>(Task<TTask> task)
|
||||
{
|
||||
DebugTools.AssertNull(_resume);
|
||||
|
||||
Status = JobStatus.Waiting;
|
||||
DebugTime += StopWatch.Elapsed.TotalSeconds;
|
||||
|
||||
var result = await task;
|
||||
|
||||
// Immediately block on resume so that everything stays correct.
|
||||
Status = JobStatus.Paused;
|
||||
_resume = new TaskCompletionSource<object>();
|
||||
|
||||
await _resume.Task;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper to safely await on an external task.
|
||||
/// </summary>
|
||||
protected async Task WaitAsyncTask(Task task)
|
||||
{
|
||||
DebugTools.AssertNull(_resume);
|
||||
|
||||
Status = JobStatus.Waiting;
|
||||
DebugTime += StopWatch.Elapsed.TotalSeconds;
|
||||
|
||||
await task;
|
||||
|
||||
// Immediately block on resume so that everything stays correct.
|
||||
_resume = new TaskCompletionSource<object>();
|
||||
Status = JobStatus.Paused;
|
||||
|
||||
await _resume.Task;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
_workInProgress ??= ProcessWrap();
|
||||
|
||||
if (Status == JobStatus.Finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.Assert(_resume != null,
|
||||
"Run() called without resume. Was this called while the job is in Waiting state?");
|
||||
var resume = _resume;
|
||||
_resume = null;
|
||||
|
||||
Status = JobStatus.Running;
|
||||
StopWatch.Restart();
|
||||
|
||||
if (Cancellation.IsCancellationRequested)
|
||||
{
|
||||
resume.TrySetCanceled();
|
||||
}
|
||||
else
|
||||
{
|
||||
resume.SetResult(null);
|
||||
}
|
||||
|
||||
if (Status != JobStatus.Finished && Status != JobStatus.Waiting)
|
||||
{
|
||||
DebugTools.Assert(_resume != null,
|
||||
"Job suspended without _resume set. Did you await on an external task without using WaitAsyncTask?");
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task<T> Process();
|
||||
|
||||
private async Task ProcessWrap()
|
||||
{
|
||||
try
|
||||
{
|
||||
Cancellation.ThrowIfCancellationRequested();
|
||||
|
||||
// Making sure that the task starts inside the Running block,
|
||||
// where the stopwatch is correctly set and such.
|
||||
await SuspendNow();
|
||||
Result = await Process();
|
||||
|
||||
// TODO: not sure if it makes sense to connect Task directly up
|
||||
// to the return value of this method/Process.
|
||||
// Maybe?
|
||||
_taskTcs.TrySetResult(Result);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_taskTcs.TrySetCanceled();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO: Should this be exposed differently?
|
||||
// I feel that people might forget to check whether the job failed.
|
||||
Logger.ErrorS("job", "Job failed on exception:\n{0}", e);
|
||||
Exception = e;
|
||||
_taskTcs.TrySetException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Status != JobStatus.Waiting)
|
||||
{
|
||||
// If we're blocked on waiting and the waiting task goes cancel/exception,
|
||||
// this timing info would not be correct.
|
||||
DebugTime += StopWatch.Elapsed.TotalSeconds;
|
||||
}
|
||||
Status = JobStatus.Finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum JobStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Job has been created and has not been ran yet.
|
||||
/// </summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>
|
||||
/// Job is currently (yes, right now!) executing.
|
||||
/// </summary>
|
||||
Running,
|
||||
|
||||
/// <summary>
|
||||
/// Job is paused due to CPU limits.
|
||||
/// </summary>
|
||||
Paused,
|
||||
|
||||
/// <summary>
|
||||
/// Job is paused because of waiting on external task.
|
||||
/// </summary>
|
||||
Waiting,
|
||||
|
||||
/// <summary>
|
||||
/// Job is done.
|
||||
/// </summary>
|
||||
// TODO: Maybe have a different status code for cancelled/failed on exception?
|
||||
Finished,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
|
||||
{
|
||||
public sealed class AiActionJobQueue : JobQueue {}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
|
||||
{
|
||||
public class JobQueue
|
||||
{
|
||||
private readonly IStopwatch _stopwatch;
|
||||
|
||||
public JobQueue() : this(new Stopwatch())
|
||||
{
|
||||
}
|
||||
|
||||
public JobQueue(IStopwatch stopwatch)
|
||||
{
|
||||
_stopwatch = stopwatch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How long the job's allowed to run for before suspending
|
||||
/// </summary>
|
||||
public virtual double MaxTime => 0.002;
|
||||
|
||||
private readonly Queue<IJob> _pendingQueue = new Queue<IJob>();
|
||||
private readonly List<IJob> _waitingJobs = new List<IJob>();
|
||||
|
||||
public void EnqueueJob(IJob job)
|
||||
{
|
||||
_pendingQueue.Enqueue(job);
|
||||
}
|
||||
|
||||
public void Process()
|
||||
{
|
||||
// Move all finished waiting jobs back into the regular queue.
|
||||
foreach (var waitingJob in _waitingJobs)
|
||||
{
|
||||
if (waitingJob.Status != JobStatus.Waiting)
|
||||
{
|
||||
_pendingQueue.Enqueue(waitingJob);
|
||||
}
|
||||
}
|
||||
|
||||
_waitingJobs.RemoveAll(p => p.Status != JobStatus.Waiting);
|
||||
|
||||
// At one point I tried making the pathfinding queue multi-threaded but ehhh didn't go great
|
||||
// Could probably try it again at some point
|
||||
// it just seemed slow af but I was probably doing something dumb with semaphores
|
||||
_stopwatch.Restart();
|
||||
|
||||
// Although the jobs can stop themselves we might be able to squeeze more of them in the allotted time
|
||||
while (_stopwatch.Elapsed.TotalSeconds < MaxTime && _pendingQueue.TryDequeue(out var job))
|
||||
{
|
||||
// Deque and re-enqueue these to cycle them through to avoid starvation if we've got a lot of jobs.
|
||||
|
||||
job.Run();
|
||||
|
||||
switch (job.Status)
|
||||
{
|
||||
case JobStatus.Finished:
|
||||
continue;
|
||||
case JobStatus.Waiting:
|
||||
// If this job goes into waiting we have to move it into a separate list.
|
||||
// Otherwise we'd just be spinning like mad here for external IO or such.
|
||||
_waitingJobs.Add(job);
|
||||
break;
|
||||
default:
|
||||
_pendingQueue.Enqueue(job);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
|
||||
{
|
||||
public sealed class PathfindingJobQueue : JobQueue
|
||||
{
|
||||
public override double MaxTime => 0.003;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user