Pathfinder rework (#11452)
This commit is contained in:
@@ -1,61 +0,0 @@
|
|||||||
using Content.Client.NPC;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Shared.Console;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Client.Commands
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This is used to handle the tooltips above AI mobs
|
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
|
||||||
internal sealed class DebugAiCommand : IConsoleCommand
|
|
||||||
{
|
|
||||||
// ReSharper disable once StringLiteralTypo
|
|
||||||
public string Command => "debugai";
|
|
||||||
public string Description => "Handles all tooltip debugging above AI mobs";
|
|
||||||
public string Help => "debugai [hide/paths/thonk]";
|
|
||||||
|
|
||||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
if (args.Length < 1)
|
|
||||||
{
|
|
||||||
shell.RemoteExecuteCommand(argStr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var anyAction = false;
|
|
||||||
var debugSystem = EntitySystem.Get<ClientAiDebugSystem>();
|
|
||||||
|
|
||||||
foreach (var arg in args)
|
|
||||||
{
|
|
||||||
switch (arg)
|
|
||||||
{
|
|
||||||
case "hide":
|
|
||||||
debugSystem.Disable();
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// This will show the pathfinding numbers above the mob's head
|
|
||||||
case "paths":
|
|
||||||
debugSystem.ToggleTooltip(AiDebugMode.Paths);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Shows stats on what the AI was thinking.
|
|
||||||
case "thonk":
|
|
||||||
debugSystem.ToggleTooltip(AiDebugMode.Thonk);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!anyAction)
|
|
||||||
shell.RemoteExecuteCommand(argStr);
|
|
||||||
#else
|
|
||||||
shell.RemoteExecuteCommand(argStr);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +1,61 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Client.NPC;
|
using Content.Client.NPC;
|
||||||
|
using Content.Shared.NPC;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Client.Commands
|
namespace Content.Client.Commands
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
internal sealed class DebugPathfindingCommand : IConsoleCommand
|
public sealed class DebugPathfindingCommand : IConsoleCommand
|
||||||
{
|
{
|
||||||
// ReSharper disable once StringLiteralTypo
|
// ReSharper disable once StringLiteralTypo
|
||||||
public string Command => "pathfinder";
|
public string Command => "pathfinder";
|
||||||
public string Description => "Toggles visibility of pathfinding debuggers.";
|
public string Description => "Toggles visibility of pathfinding debuggers.";
|
||||||
public string Help => "pathfinder [hide/nodes/routes/graph/regioncache/regions]";
|
public string Help => "pathfinder [options]";
|
||||||
|
|
||||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
|
||||||
if (args.Length < 1)
|
|
||||||
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
shell.RemoteExecuteCommand(argStr);
|
system.Modes = PathfindingDebugMode.None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var anyAction = false;
|
|
||||||
var debugSystem = EntitySystem.Get<ClientPathfindingDebugSystem>();
|
|
||||||
|
|
||||||
foreach (var arg in args)
|
foreach (var arg in args)
|
||||||
{
|
{
|
||||||
switch (arg)
|
if (!Enum.TryParse<PathfindingDebugMode>(arg, out var mode))
|
||||||
{
|
{
|
||||||
case "hide":
|
shell.WriteError($"Unrecognised pathfinder args {arg}");
|
||||||
debugSystem.Disable();
|
continue;
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Shows all nodes on the closed list
|
|
||||||
case "nodes":
|
|
||||||
debugSystem.ToggleTooltip(PathfindingDebugMode.Nodes);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Will show just the constructed route
|
|
||||||
case "routes":
|
|
||||||
debugSystem.ToggleTooltip(PathfindingDebugMode.Route);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Shows all of the pathfinding chunks
|
|
||||||
case "graph":
|
|
||||||
debugSystem.ToggleTooltip(PathfindingDebugMode.Graph);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Shows every time the cached reachable regions are hit (whether cached already or not)
|
|
||||||
case "regioncache":
|
|
||||||
debugSystem.ToggleTooltip(PathfindingDebugMode.CachedRegions);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
// Shows all of the regions in each chunk
|
|
||||||
case "regions":
|
|
||||||
debugSystem.ToggleTooltip(PathfindingDebugMode.Regions);
|
|
||||||
anyAction = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
system.Modes ^= mode;
|
||||||
|
shell.WriteLine($"Toggled {arg} to {(system.Modes & mode) != 0x0}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
return CompletionResult.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!anyAction)
|
var values = Enum.GetValues<PathfindingDebugMode>().ToList();
|
||||||
shell.RemoteExecuteCommand(argStr);
|
var options = new List<CompletionOption>();
|
||||||
#else
|
|
||||||
shell.RemoteExecuteCommand(argStr);
|
foreach (var val in values)
|
||||||
#endif
|
{
|
||||||
|
if (val == PathfindingDebugMode.None)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
options.Add(new CompletionOption(val.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletionResult.FromOptions(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
using Content.Client.Stylesheets;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Client.UserInterface;
|
|
||||||
using Robust.Client.UserInterface.Controls;
|
|
||||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
|
||||||
|
|
||||||
namespace Content.Client.NPC
|
|
||||||
{
|
|
||||||
public sealed class ClientAiDebugSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
|
||||||
|
|
||||||
public AiDebugMode Tooltips { get; private set; } = AiDebugMode.None;
|
|
||||||
private readonly Dictionary<EntityUid, PanelContainer> _aiBoxes = new();
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
base.Update(frameTime);
|
|
||||||
if (Tooltips == 0)
|
|
||||||
{
|
|
||||||
if (_aiBoxes.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var (_, panel) in _aiBoxes)
|
|
||||||
{
|
|
||||||
panel.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_aiBoxes.Clear();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deletedEntities = new List<EntityUid>(0);
|
|
||||||
foreach (var (entity, panel) in _aiBoxes)
|
|
||||||
{
|
|
||||||
if (Deleted(entity))
|
|
||||||
{
|
|
||||||
deletedEntities.Add(entity);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_eyeManager.GetWorldViewport().Contains(EntityManager.GetComponent<TransformComponent>(entity).WorldPosition))
|
|
||||||
{
|
|
||||||
panel.Visible = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var (x, y) = _eyeManager.CoordinatesToScreen(EntityManager.GetComponent<TransformComponent>(entity).Coordinates).Position;
|
|
||||||
var offsetPosition = new Vector2(x - panel.Width / 2, y - panel.Height - 50f);
|
|
||||||
panel.Visible = true;
|
|
||||||
|
|
||||||
LayoutContainer.SetPosition(panel, offsetPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in deletedEntities)
|
|
||||||
{
|
|
||||||
_aiBoxes.Remove(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
UpdatesOutsidePrediction = true;
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.UtilityAiDebugMessage>(HandleUtilityAiDebugMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleUtilityAiDebugMessage(SharedAiDebug.UtilityAiDebugMessage message)
|
|
||||||
{
|
|
||||||
if ((Tooltips & AiDebugMode.Thonk) != 0)
|
|
||||||
{
|
|
||||||
// I guess if it's out of range we don't know about it?
|
|
||||||
var entity = message.EntityUid;
|
|
||||||
TryCreatePanel(entity);
|
|
||||||
|
|
||||||
// Probably shouldn't access by index but it's a debugging tool so eh
|
|
||||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(0);
|
|
||||||
label.Text = $"Current Task: {message.FoundTask}\n" +
|
|
||||||
$"Task score: {message.ActionScore}\n" +
|
|
||||||
$"Planning time (ms): {message.PlanningTime * 1000:0.0000}\n" +
|
|
||||||
$"Considered {message.ConsideredTaskCount} tasks";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
|
|
||||||
{
|
|
||||||
if ((Tooltips & AiDebugMode.Paths) != 0)
|
|
||||||
{
|
|
||||||
var entity = message.EntityUid;
|
|
||||||
TryCreatePanel(entity);
|
|
||||||
|
|
||||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
|
|
||||||
label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
|
|
||||||
$"Nodes traversed: {message.CameFrom.Count}\n" +
|
|
||||||
$"Nodes per ms: {message.CameFrom.Count / (message.TimeTaken * 1000)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
|
|
||||||
{
|
|
||||||
if ((Tooltips & AiDebugMode.Paths) != 0)
|
|
||||||
{
|
|
||||||
var entity = message.EntityUid;
|
|
||||||
TryCreatePanel(entity);
|
|
||||||
|
|
||||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
|
|
||||||
label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
|
|
||||||
$"Jump Nodes: {message.JumpNodes.Count}\n" +
|
|
||||||
$"Jump Nodes per ms: {message.JumpNodes.Count / (message.TimeTaken * 1000)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
foreach (var tooltip in _aiBoxes.Values)
|
|
||||||
{
|
|
||||||
tooltip.Dispose();
|
|
||||||
}
|
|
||||||
_aiBoxes.Clear();
|
|
||||||
Tooltips = AiDebugMode.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void EnableTooltip(AiDebugMode tooltip)
|
|
||||||
{
|
|
||||||
Tooltips |= tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableTooltip(AiDebugMode tooltip)
|
|
||||||
{
|
|
||||||
Tooltips &= ~tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleTooltip(AiDebugMode tooltip)
|
|
||||||
{
|
|
||||||
if ((Tooltips & tooltip) != 0)
|
|
||||||
{
|
|
||||||
DisableTooltip(tooltip);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EnableTooltip(tooltip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryCreatePanel(EntityUid entity)
|
|
||||||
{
|
|
||||||
if (!_aiBoxes.ContainsKey(entity))
|
|
||||||
{
|
|
||||||
var userInterfaceManager = IoCManager.Resolve<IUserInterfaceManager>();
|
|
||||||
|
|
||||||
var actionLabel = new Label
|
|
||||||
{
|
|
||||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
|
||||||
};
|
|
||||||
|
|
||||||
var pathfindingLabel = new Label
|
|
||||||
{
|
|
||||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
|
||||||
};
|
|
||||||
|
|
||||||
var vBox = new BoxContainer()
|
|
||||||
{
|
|
||||||
Orientation = LayoutOrientation.Vertical,
|
|
||||||
SeparationOverride = 15,
|
|
||||||
Children = {actionLabel, pathfindingLabel},
|
|
||||||
};
|
|
||||||
|
|
||||||
var panel = new PanelContainer
|
|
||||||
{
|
|
||||||
StyleClasses = { StyleNano.StyleClassTooltipPanel },
|
|
||||||
Children = {vBox},
|
|
||||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
|
||||||
ModulateSelfOverride = Color.White.WithAlpha(0.75f),
|
|
||||||
};
|
|
||||||
|
|
||||||
userInterfaceManager.StateRoot.AddChild(panel);
|
|
||||||
|
|
||||||
_aiBoxes[entity] = panel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum AiDebugMode : byte
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Paths = 1 << 1,
|
|
||||||
Thonk = 1 << 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Client.Player;
|
|
||||||
using Robust.Shared.Enums;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Client.NPC
|
|
||||||
{
|
|
||||||
public sealed class ClientPathfindingDebugSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
||||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
||||||
|
|
||||||
public PathfindingDebugMode Modes { get; private set; } = PathfindingDebugMode.None;
|
|
||||||
private float _routeDuration = 4.0f; // How long before we remove a route from the overlay
|
|
||||||
private DebugPathfindingOverlay? _overlay;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.PathfindingGraphMessage>(HandleGraphMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.ReachableChunkRegionsDebugMessage>(HandleRegionsMessage);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.ReachableCacheDebugMessage>(HandleCachedRegionsMessage);
|
|
||||||
// I'm lazy
|
|
||||||
EnableOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Shutdown()
|
|
||||||
{
|
|
||||||
base.Shutdown();
|
|
||||||
DisableOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
|
|
||||||
{
|
|
||||||
if ((Modes & PathfindingDebugMode.Nodes) != 0 ||
|
|
||||||
(Modes & PathfindingDebugMode.Route) != 0)
|
|
||||||
{
|
|
||||||
_overlay?.AStarRoutes.Add(message);
|
|
||||||
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
|
|
||||||
{
|
|
||||||
if (_overlay == null) return;
|
|
||||||
_overlay.AStarRoutes.Remove(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
|
|
||||||
{
|
|
||||||
if ((Modes & PathfindingDebugMode.Nodes) != 0 ||
|
|
||||||
(Modes & PathfindingDebugMode.Route) != 0)
|
|
||||||
{
|
|
||||||
_overlay?.JpsRoutes.Add(message);
|
|
||||||
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
|
|
||||||
{
|
|
||||||
if (_overlay == null) return;
|
|
||||||
_overlay.JpsRoutes.Remove(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message)
|
|
||||||
{
|
|
||||||
EnableOverlay().UpdateGraph(message.Graph);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRegionsMessage(SharedAiDebug.ReachableChunkRegionsDebugMessage message)
|
|
||||||
{
|
|
||||||
EnableOverlay().UpdateRegions(message.GridId, message.Regions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCachedRegionsMessage(SharedAiDebug.ReachableCacheDebugMessage message)
|
|
||||||
{
|
|
||||||
EnableOverlay().UpdateCachedRegions(message.GridId, message.Regions, message.Cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DebugPathfindingOverlay EnableOverlay()
|
|
||||||
{
|
|
||||||
if (_overlay != null)
|
|
||||||
{
|
|
||||||
return _overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
|
||||||
_overlay = new DebugPathfindingOverlay(EntityManager, _eyeManager, _playerManager, _prototypeManager) {Modes = Modes};
|
|
||||||
overlayManager.AddOverlay(_overlay);
|
|
||||||
|
|
||||||
return _overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisableOverlay()
|
|
||||||
{
|
|
||||||
if (_overlay == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_overlay.Modes = 0;
|
|
||||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
|
||||||
overlayManager.RemoveOverlay(_overlay);
|
|
||||||
_overlay = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
Modes = PathfindingDebugMode.None;
|
|
||||||
DisableOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void EnableMode(PathfindingDebugMode tooltip)
|
|
||||||
{
|
|
||||||
Modes |= tooltip;
|
|
||||||
if (Modes != 0)
|
|
||||||
{
|
|
||||||
EnableOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_overlay != null)
|
|
||||||
{
|
|
||||||
_overlay.Modes = Modes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tooltip == PathfindingDebugMode.Graph)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new SharedAiDebug.RequestPathfindingGraphMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tooltip == PathfindingDebugMode.Regions)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new SharedAiDebug.SubscribeReachableMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Request region graph, although the client system messages didn't seem to be going through anymore
|
|
||||||
// So need further investigation.
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableMode(PathfindingDebugMode mode)
|
|
||||||
{
|
|
||||||
if (mode == PathfindingDebugMode.Regions && (Modes & PathfindingDebugMode.Regions) != 0)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new SharedAiDebug.UnsubscribeReachableMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
Modes &= ~mode;
|
|
||||||
if (Modes == 0)
|
|
||||||
{
|
|
||||||
DisableOverlay();
|
|
||||||
}
|
|
||||||
else if (_overlay != null)
|
|
||||||
{
|
|
||||||
_overlay.Modes = Modes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleTooltip(PathfindingDebugMode mode)
|
|
||||||
{
|
|
||||||
if ((Modes & mode) != 0)
|
|
||||||
{
|
|
||||||
DisableMode(mode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EnableMode(mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DebugPathfindingOverlay : Overlay
|
|
||||||
{
|
|
||||||
private readonly IEyeManager _eyeManager;
|
|
||||||
private readonly IPlayerManager _playerManager;
|
|
||||||
private readonly IEntityManager _entities;
|
|
||||||
|
|
||||||
// TODO: Add a box like the debug one and show the most recent path stuff
|
|
||||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
|
||||||
private readonly ShaderInstance _shader;
|
|
||||||
|
|
||||||
public PathfindingDebugMode Modes { get; set; } = PathfindingDebugMode.None;
|
|
||||||
|
|
||||||
// Graph debugging
|
|
||||||
public readonly Dictionary<int, List<Vector2>> Graph = new();
|
|
||||||
private readonly Dictionary<int, Color> _graphColors = new();
|
|
||||||
|
|
||||||
// Cached regions
|
|
||||||
public readonly Dictionary<EntityUid, Dictionary<int, List<Vector2>>> CachedRegions =
|
|
||||||
new();
|
|
||||||
|
|
||||||
private readonly Dictionary<EntityUid, Dictionary<int, Color>> _cachedRegionColors =
|
|
||||||
new();
|
|
||||||
|
|
||||||
// Regions
|
|
||||||
public readonly Dictionary<EntityUid, Dictionary<int, Dictionary<int, List<Vector2>>>> Regions =
|
|
||||||
new();
|
|
||||||
|
|
||||||
private readonly Dictionary<EntityUid, Dictionary<int, Dictionary<int, Color>>> _regionColors =
|
|
||||||
new();
|
|
||||||
|
|
||||||
// Route debugging
|
|
||||||
// As each pathfinder is very different you'll likely want to draw them completely different
|
|
||||||
public readonly List<SharedAiDebug.AStarRouteMessage> AStarRoutes = new();
|
|
||||||
public readonly List<SharedAiDebug.JpsRouteMessage> JpsRoutes = new();
|
|
||||||
|
|
||||||
public DebugPathfindingOverlay(IEntityManager entities, IEyeManager eyeManager, IPlayerManager playerManager, IPrototypeManager prototypeManager)
|
|
||||||
{
|
|
||||||
_entities = entities;
|
|
||||||
_eyeManager = eyeManager;
|
|
||||||
_playerManager = playerManager;
|
|
||||||
_shader = prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Graph
|
|
||||||
public void UpdateGraph(Dictionary<int, List<Vector2>> graph)
|
|
||||||
{
|
|
||||||
Graph.Clear();
|
|
||||||
_graphColors.Clear();
|
|
||||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
||||||
foreach (var (chunk, nodes) in graph)
|
|
||||||
{
|
|
||||||
Graph[chunk] = nodes;
|
|
||||||
_graphColors[chunk] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
|
|
||||||
robustRandom.NextFloat(), 0.3f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawGraph(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
foreach (var (chunk, nodes) in Graph)
|
|
||||||
{
|
|
||||||
foreach (var tile in nodes)
|
|
||||||
{
|
|
||||||
if (!viewport.Contains(tile)) continue;
|
|
||||||
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(tile);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
|
|
||||||
screenHandle.DrawRect(box, _graphColors[chunk]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Regions
|
|
||||||
//Server side debugger should increment every region
|
|
||||||
public void UpdateCachedRegions(EntityUid gridId, Dictionary<int, List<Vector2>> messageRegions, bool cached)
|
|
||||||
{
|
|
||||||
if (!CachedRegions.ContainsKey(gridId))
|
|
||||||
{
|
|
||||||
CachedRegions.Add(gridId, new Dictionary<int, List<Vector2>>());
|
|
||||||
_cachedRegionColors.Add(gridId, new Dictionary<int, Color>());
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (region, nodes) in messageRegions)
|
|
||||||
{
|
|
||||||
CachedRegions[gridId][region] = nodes;
|
|
||||||
if (cached)
|
|
||||||
{
|
|
||||||
_cachedRegionColors[gridId][region] = Color.Blue.WithAlpha(0.3f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_cachedRegionColors[gridId][region] = Color.LimeGreen.WithAlpha(0.3f);
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer.Spawn(3000, () =>
|
|
||||||
{
|
|
||||||
if (CachedRegions[gridId].ContainsKey(region))
|
|
||||||
{
|
|
||||||
CachedRegions[gridId].Remove(region);
|
|
||||||
_cachedRegionColors[gridId].Remove(region);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCachedRegions(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
var transform = _entities.GetComponentOrNull<TransformComponent>(_playerManager.LocalPlayer?.ControlledEntity);
|
|
||||||
if (transform == null || transform.GridUid == null || !CachedRegions.TryGetValue(transform.GridUid.Value, out var entityRegions))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (region, nodes) in entityRegions)
|
|
||||||
{
|
|
||||||
foreach (var tile in nodes)
|
|
||||||
{
|
|
||||||
if (!viewport.Contains(tile)) continue;
|
|
||||||
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(tile);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
|
|
||||||
screenHandle.DrawRect(box, _cachedRegionColors[transform.GridUid.Value][region]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateRegions(EntityUid gridId, Dictionary<int, Dictionary<int, List<Vector2>>> messageRegions)
|
|
||||||
{
|
|
||||||
if (!Regions.ContainsKey(gridId))
|
|
||||||
{
|
|
||||||
Regions.Add(gridId, new Dictionary<int, Dictionary<int, List<Vector2>>>());
|
|
||||||
_regionColors.Add(gridId, new Dictionary<int, Dictionary<int, Color>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
||||||
foreach (var (chunk, regions) in messageRegions)
|
|
||||||
{
|
|
||||||
Regions[gridId][chunk] = new Dictionary<int, List<Vector2>>();
|
|
||||||
_regionColors[gridId][chunk] = new Dictionary<int, Color>();
|
|
||||||
|
|
||||||
foreach (var (region, nodes) in regions)
|
|
||||||
{
|
|
||||||
Regions[gridId][chunk].Add(region, nodes);
|
|
||||||
_regionColors[gridId][chunk][region] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
|
|
||||||
robustRandom.NextFloat(), 0.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRegions(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
var attachedEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
|
||||||
if (!_entities.TryGetComponent(attachedEntity, out TransformComponent? transform) ||
|
|
||||||
transform.GridUid == null ||
|
|
||||||
!Regions.TryGetValue(transform.GridUid.Value, out var entityRegions))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (chunk, regions) in entityRegions)
|
|
||||||
{
|
|
||||||
foreach (var (region, nodes) in regions)
|
|
||||||
{
|
|
||||||
foreach (var tile in nodes)
|
|
||||||
{
|
|
||||||
if (!viewport.Contains(tile)) continue;
|
|
||||||
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(tile);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
|
|
||||||
screenHandle.DrawRect(box, _regionColors[transform.GridUid.Value][chunk][region]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Pathfinder
|
|
||||||
private void DrawAStarRoutes(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
foreach (var route in AStarRoutes)
|
|
||||||
{
|
|
||||||
// Draw box on each tile of route
|
|
||||||
foreach (var position in route.Route)
|
|
||||||
{
|
|
||||||
if (!viewport.Contains(position)) continue;
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(position);
|
|
||||||
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawAStarNodes(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
foreach (var route in AStarRoutes)
|
|
||||||
{
|
|
||||||
var highestGScore = route.GScores.Values.Max();
|
|
||||||
|
|
||||||
foreach (var (tile, score) in route.GScores)
|
|
||||||
{
|
|
||||||
if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
|
|
||||||
!viewport.Contains(tile))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(tile);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
|
|
||||||
screenHandle.DrawRect(box, new Color(
|
|
||||||
0.0f,
|
|
||||||
score / highestGScore,
|
|
||||||
1.0f - (score / highestGScore),
|
|
||||||
0.1f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawJpsRoutes(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
foreach (var route in JpsRoutes)
|
|
||||||
{
|
|
||||||
// Draw box on each tile of route
|
|
||||||
foreach (var position in route.Route)
|
|
||||||
{
|
|
||||||
if (!viewport.Contains(position)) continue;
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(position);
|
|
||||||
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawJpsNodes(DrawingHandleScreen screenHandle, Box2 viewport)
|
|
||||||
{
|
|
||||||
foreach (var route in JpsRoutes)
|
|
||||||
{
|
|
||||||
foreach (var tile in route.JumpNodes)
|
|
||||||
{
|
|
||||||
if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
|
|
||||||
!viewport.Contains(tile))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var screenTile = _eyeManager.WorldToScreen(tile);
|
|
||||||
var box = new UIBox2(
|
|
||||||
screenTile.X - 15.0f,
|
|
||||||
screenTile.Y - 15.0f,
|
|
||||||
screenTile.X + 15.0f,
|
|
||||||
screenTile.Y + 15.0f);
|
|
||||||
|
|
||||||
screenHandle.DrawRect(box, new Color(
|
|
||||||
0.0f,
|
|
||||||
1.0f,
|
|
||||||
0.0f,
|
|
||||||
0.2f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
protected override void Draw(in OverlayDrawArgs args)
|
|
||||||
{
|
|
||||||
if (Modes == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var screenHandle = args.ScreenHandle;
|
|
||||||
screenHandle.UseShader(_shader);
|
|
||||||
var viewport = args.WorldAABB;
|
|
||||||
|
|
||||||
if ((Modes & PathfindingDebugMode.Route) != 0)
|
|
||||||
{
|
|
||||||
DrawAStarRoutes(screenHandle, viewport);
|
|
||||||
DrawJpsRoutes(screenHandle, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Modes & PathfindingDebugMode.Nodes) != 0)
|
|
||||||
{
|
|
||||||
DrawAStarNodes(screenHandle, viewport);
|
|
||||||
DrawJpsNodes(screenHandle, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Modes & PathfindingDebugMode.Graph) != 0)
|
|
||||||
{
|
|
||||||
DrawGraph(screenHandle, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Modes & PathfindingDebugMode.CachedRegions) != 0)
|
|
||||||
{
|
|
||||||
DrawCachedRegions(screenHandle, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Modes & PathfindingDebugMode.Regions) != 0)
|
|
||||||
{
|
|
||||||
DrawRegions(screenHandle, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
screenHandle.UseShader(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum PathfindingDebugMode : byte
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Route = 1 << 0,
|
|
||||||
Graph = 1 << 1,
|
|
||||||
Nodes = 1 << 2,
|
|
||||||
CachedRegions = 1 << 3,
|
|
||||||
Regions = 1 << 4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,11 @@
|
|||||||
<Label Text="Pathfinder" HorizontalAlignment="Center"/>
|
<Label Text="Pathfinder" HorizontalAlignment="Center"/>
|
||||||
</controls:StripeBack>
|
</controls:StripeBack>
|
||||||
<BoxContainer Name="PathfinderBox" Orientation="Vertical">
|
<BoxContainer Name="PathfinderBox" Orientation="Vertical">
|
||||||
<CheckBox Name="PathNodes" Text="Nodes"/>
|
<CheckBox Name="PathCrumbs" Text="Breadcrumbs"/>
|
||||||
|
<CheckBox Name="PathPolys" Text="Polygons"/>
|
||||||
|
<CheckBox Name="PathNeighbors" Text="Neighbors"/>
|
||||||
|
<CheckBox Name="PathRouteCosts" Text="Route costs"/>
|
||||||
<CheckBox Name="PathRoutes" Text="Routes"/>
|
<CheckBox Name="PathRoutes" Text="Routes"/>
|
||||||
<CheckBox Name="PathRegions" Text="Regions"/>
|
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</controls:FancyWindow>
|
</controls:FancyWindow>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Client.UserInterface.Controls;
|
using Content.Client.UserInterface.Controls;
|
||||||
|
using Content.Shared.NPC;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
|
||||||
@@ -12,21 +13,18 @@ public sealed partial class NPCWindow : FancyWindow
|
|||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
IoCManager.InjectDependencies(this);
|
IoCManager.InjectDependencies(this);
|
||||||
var sysManager = IoCManager.Resolve<IEntitySystemManager>();
|
var sysManager = IoCManager.Resolve<IEntitySystemManager>();
|
||||||
var debugSys = sysManager.GetEntitySystem<ClientAiDebugSystem>();
|
var path = sysManager.GetEntitySystem<PathfindingSystem>();
|
||||||
var path = sysManager.GetEntitySystem<ClientPathfindingDebugSystem>();
|
|
||||||
|
|
||||||
NPCPath.Pressed = (debugSys.Tooltips & AiDebugMode.Paths) != 0x0;
|
PathCrumbs.Pressed = (path.Modes & PathfindingDebugMode.Breadcrumbs) != 0x0;
|
||||||
NPCThonk.Pressed = (debugSys.Tooltips & AiDebugMode.Thonk) != 0x0;
|
PathPolys.Pressed = (path.Modes & PathfindingDebugMode.Polys) != 0x0;
|
||||||
|
PathNeighbors.Pressed = (path.Modes & PathfindingDebugMode.PolyNeighbors) != 0x0;
|
||||||
|
PathRouteCosts.Pressed = (path.Modes & PathfindingDebugMode.RouteCosts) != 0x0;
|
||||||
|
PathRoutes.Pressed = (path.Modes & PathfindingDebugMode.Routes) != 0x0;
|
||||||
|
|
||||||
NPCPath.OnToggled += args => debugSys.ToggleTooltip(AiDebugMode.Paths);
|
PathCrumbs.OnToggled += args => path.Modes ^= PathfindingDebugMode.Breadcrumbs;
|
||||||
NPCThonk.OnToggled += args => debugSys.ToggleTooltip(AiDebugMode.Thonk);
|
PathPolys.OnToggled += args => path.Modes ^= PathfindingDebugMode.Polys;
|
||||||
|
PathNeighbors.OnToggled += args => path.Modes ^= PathfindingDebugMode.PolyNeighbors;
|
||||||
PathNodes.Pressed = (path.Modes & PathfindingDebugMode.Nodes) != 0x0;
|
PathRouteCosts.OnToggled += args => path.Modes ^= PathfindingDebugMode.RouteCosts;
|
||||||
PathRegions.Pressed = (path.Modes & PathfindingDebugMode.Regions) != 0x0;
|
PathRoutes.OnToggled += args => path.Modes ^= PathfindingDebugMode.Routes;
|
||||||
PathRoutes.Pressed = (path.Modes & PathfindingDebugMode.Route) != 0x0;
|
|
||||||
|
|
||||||
PathNodes.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Nodes);
|
|
||||||
PathRegions.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Regions);
|
|
||||||
PathRoutes.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Route);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
526
Content.Client/NPC/PathfindingSystem.cs
Normal file
526
Content.Client/NPC/PathfindingSystem.cs
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.Input;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Client.NPC
|
||||||
|
{
|
||||||
|
public sealed class PathfindingSystem : SharedPathfindingSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IResourceCache _cache = default!;
|
||||||
|
|
||||||
|
public PathfindingDebugMode Modes
|
||||||
|
{
|
||||||
|
get => _modes;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||||
|
|
||||||
|
if (value == PathfindingDebugMode.None)
|
||||||
|
{
|
||||||
|
Breadcrumbs.Clear();
|
||||||
|
Polys.Clear();
|
||||||
|
overlayManager.RemoveOverlay<PathfindingOverlay>();
|
||||||
|
}
|
||||||
|
else if (!overlayManager.HasOverlay<PathfindingOverlay>())
|
||||||
|
{
|
||||||
|
overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_modes = value;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(new RequestPathfindingDebugMessage()
|
||||||
|
{
|
||||||
|
Mode = _modes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathfindingDebugMode _modes = PathfindingDebugMode.None;
|
||||||
|
|
||||||
|
// It's debug data IDC if it doesn't support snapshots I just want something fast.
|
||||||
|
public Dictionary<EntityUid, Dictionary<Vector2i, List<PathfindingBreadcrumb>>> Breadcrumbs = new();
|
||||||
|
public Dictionary<EntityUid, Dictionary<Vector2i, Dictionary<Vector2i, List<DebugPathPoly>>>> Polys = new();
|
||||||
|
public readonly List<(TimeSpan Time, PathRouteMessage Message)> Routes = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeNetworkEvent<PathBreadcrumbsMessage>(OnBreadcrumbs);
|
||||||
|
SubscribeNetworkEvent<PathBreadcrumbsRefreshMessage>(OnBreadcrumbsRefresh);
|
||||||
|
SubscribeNetworkEvent<PathPolysMessage>(OnPolys);
|
||||||
|
SubscribeNetworkEvent<PathPolysRefreshMessage>(OnPolysRefresh);
|
||||||
|
SubscribeNetworkEvent<PathRouteMessage>(OnRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!_timing.IsFirstTimePredicted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var i = 0; i < Routes.Count; i++)
|
||||||
|
{
|
||||||
|
var route = Routes[i];
|
||||||
|
|
||||||
|
if (_timing.RealTime < route.Time)
|
||||||
|
break;
|
||||||
|
|
||||||
|
Routes.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoute(PathRouteMessage ev)
|
||||||
|
{
|
||||||
|
Routes.Add((_timing.RealTime + TimeSpan.FromSeconds(0.5), ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPolys(PathPolysMessage ev)
|
||||||
|
{
|
||||||
|
Polys = ev.Polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPolysRefresh(PathPolysRefreshMessage ev)
|
||||||
|
{
|
||||||
|
var chunks = Polys.GetOrNew(ev.GridUid);
|
||||||
|
chunks[ev.Origin] = ev.Polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
// Don't send any messages to server, just shut down quietly.
|
||||||
|
_modes = PathfindingDebugMode.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBreadcrumbs(PathBreadcrumbsMessage ev)
|
||||||
|
{
|
||||||
|
Breadcrumbs = ev.Breadcrumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBreadcrumbsRefresh(PathBreadcrumbsRefreshMessage ev)
|
||||||
|
{
|
||||||
|
if (!Breadcrumbs.TryGetValue(ev.GridUid, out var chunks))
|
||||||
|
return;
|
||||||
|
|
||||||
|
chunks[ev.Origin] = ev.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PathfindingOverlay : Overlay
|
||||||
|
{
|
||||||
|
private readonly IEntityManager _entManager;
|
||||||
|
private readonly IEyeManager _eyeManager;
|
||||||
|
private readonly IInputManager _inputManager;
|
||||||
|
private readonly IMapManager _mapManager;
|
||||||
|
private readonly PathfindingSystem _system;
|
||||||
|
|
||||||
|
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
|
||||||
|
|
||||||
|
private readonly Font _font;
|
||||||
|
|
||||||
|
public PathfindingOverlay(
|
||||||
|
IEntityManager entManager,
|
||||||
|
IEyeManager eyeManager,
|
||||||
|
IInputManager inputManager,
|
||||||
|
IMapManager mapManager,
|
||||||
|
IResourceCache cache,
|
||||||
|
PathfindingSystem system)
|
||||||
|
{
|
||||||
|
_entManager = entManager;
|
||||||
|
_eyeManager = eyeManager;
|
||||||
|
_inputManager = inputManager;
|
||||||
|
_mapManager = mapManager;
|
||||||
|
_system = system;
|
||||||
|
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
switch (args.DrawingHandle)
|
||||||
|
{
|
||||||
|
case DrawingHandleScreen screenHandle:
|
||||||
|
DrawScreen(args, screenHandle);
|
||||||
|
break;
|
||||||
|
case DrawingHandleWorld worldHandle:
|
||||||
|
DrawWorld(args, worldHandle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle)
|
||||||
|
{
|
||||||
|
var mousePos = _inputManager.MouseScreenPosition;
|
||||||
|
var mouseWorldPos = _eyeManager.ScreenToMap(mousePos);
|
||||||
|
var aabb = new Box2(mouseWorldPos.Position - SharedPathfindingSystem.ChunkSize, mouseWorldPos.Position + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Crumb) != 0x0 &&
|
||||||
|
mouseWorldPos.MapId == args.MapId)
|
||||||
|
{
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb))
|
||||||
|
{
|
||||||
|
if (found || !_system.Breadcrumbs.TryGetValue(grid.GridEntityId, out var crumbs))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var localAABB = grid.InvWorldMatrix.TransformBox(aabb.Enlarged(float.Epsilon - SharedPathfindingSystem.ChunkSize));
|
||||||
|
var worldMatrix = grid.WorldMatrix;
|
||||||
|
|
||||||
|
foreach (var chunk in crumbs)
|
||||||
|
{
|
||||||
|
if (found)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if (!chunkAABB.Intersects(localAABB))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PathfindingBreadcrumb? nearest = null;
|
||||||
|
var nearestDistance = float.MaxValue;
|
||||||
|
|
||||||
|
foreach (var crumb in chunk.Value)
|
||||||
|
{
|
||||||
|
var crumbMapPos = worldMatrix.Transform(_system.GetCoordinate(chunk.Key, crumb.Coordinates));
|
||||||
|
var distance = (crumbMapPos - mouseWorldPos.Position).Length;
|
||||||
|
|
||||||
|
if (distance < nearestDistance)
|
||||||
|
{
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearest = crumb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearest != null)
|
||||||
|
{
|
||||||
|
var text = new StringBuilder();
|
||||||
|
|
||||||
|
// Sandbox moment
|
||||||
|
var coords = $"Point coordinates: {nearest.Value.Coordinates.ToString()}";
|
||||||
|
var gridCoords =
|
||||||
|
$"Grid coordinates: {_system.GetCoordinate(chunk.Key, nearest.Value.Coordinates).ToString()}";
|
||||||
|
var layer = $"Layer: {nearest.Value.Data.CollisionLayer.ToString()}";
|
||||||
|
var mask = $"Mask: {nearest.Value.Data.CollisionMask.ToString()}";
|
||||||
|
|
||||||
|
text.AppendLine(coords);
|
||||||
|
text.AppendLine(gridCoords);
|
||||||
|
text.AppendLine(layer);
|
||||||
|
text.AppendLine(mask);
|
||||||
|
text.AppendLine($"Flags:");
|
||||||
|
|
||||||
|
foreach (var flag in Enum.GetValues<PathfindingBreadcrumbFlag>())
|
||||||
|
{
|
||||||
|
if ((flag & nearest.Value.Data.Flags) == 0x0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var flagStr = $"- {flag.ToString()}";
|
||||||
|
text.AppendLine(flagStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
screenHandle.DrawString(_font, mousePos.Position, text.ToString());
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Poly) != 0x0 &&
|
||||||
|
mouseWorldPos.MapId == args.MapId)
|
||||||
|
{
|
||||||
|
if (!_mapManager.TryFindGridAt(mouseWorldPos, out var grid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
if (!_system.Polys.TryGetValue(grid.GridEntityId, out var data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tileRef = grid.GetTileRef(mouseWorldPos);
|
||||||
|
var localPos = tileRef.GridIndices;
|
||||||
|
var chunkOrigin = localPos / SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
if (!data.TryGetValue(chunkOrigin, out var chunk) ||
|
||||||
|
!chunk.TryGetValue(new Vector2i(localPos.X % SharedPathfindingSystem.ChunkSize,
|
||||||
|
localPos.Y % SharedPathfindingSystem.ChunkSize), out var tile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invGridMatrix = grid.InvWorldMatrix;
|
||||||
|
DebugPathPoly? nearest = null;
|
||||||
|
var nearestDistance = float.MaxValue;
|
||||||
|
|
||||||
|
foreach (var poly in tile)
|
||||||
|
{
|
||||||
|
if (poly.Box.Contains(invGridMatrix.Transform(mouseWorldPos.Position)))
|
||||||
|
{
|
||||||
|
nearest = poly;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearest != null)
|
||||||
|
{
|
||||||
|
var text = new StringBuilder();
|
||||||
|
/*
|
||||||
|
|
||||||
|
// Sandbox moment
|
||||||
|
var coords = $"Point coordinates: {nearest.Value.Coordinates.ToString()}";
|
||||||
|
var gridCoords =
|
||||||
|
$"Grid coordinates: {_system.GetCoordinate(chunk.Key, nearest.Value.Coordinates).ToString()}";
|
||||||
|
var layer = $"Layer: {nearest.Value.Data.CollisionLayer.ToString()}";
|
||||||
|
var mask = $"Mask: {nearest.Value.Data.CollisionMask.ToString()}";
|
||||||
|
|
||||||
|
text.AppendLine(coords);
|
||||||
|
text.AppendLine(gridCoords);
|
||||||
|
text.AppendLine(layer);
|
||||||
|
text.AppendLine(mask);
|
||||||
|
text.AppendLine($"Flags:");
|
||||||
|
|
||||||
|
foreach (var flag in Enum.GetValues<PathfindingBreadcrumbFlag>())
|
||||||
|
{
|
||||||
|
if ((flag & nearest.Value.Data.Flags) == 0x0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var flagStr = $"- {flag.ToString()}";
|
||||||
|
text.AppendLine(flagStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var neighbor in )
|
||||||
|
|
||||||
|
screenHandle.DrawString(_font, mousePos.Position, text.ToString());
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle)
|
||||||
|
{
|
||||||
|
var mousePos = _inputManager.MouseScreenPosition;
|
||||||
|
var mouseWorldPos = _eyeManager.ScreenToMap(mousePos);
|
||||||
|
var aabb = new Box2(mouseWorldPos.Position - Vector2.One / 4f, mouseWorldPos.Position + Vector2.One / 4f);
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Breadcrumbs) != 0x0 &&
|
||||||
|
mouseWorldPos.MapId == args.MapId)
|
||||||
|
{
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(mouseWorldPos.MapId, aabb))
|
||||||
|
{
|
||||||
|
if (!_system.Breadcrumbs.TryGetValue(grid.GridEntityId, out var crumbs))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHandle.SetTransform(grid.WorldMatrix);
|
||||||
|
var localAABB = grid.InvWorldMatrix.TransformBox(aabb);
|
||||||
|
|
||||||
|
foreach (var chunk in crumbs)
|
||||||
|
{
|
||||||
|
var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if (!chunkAABB.Intersects(localAABB))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var crumb in chunk.Value)
|
||||||
|
{
|
||||||
|
if (crumb.Equals(PathfindingBreadcrumb.Invalid))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float edge = 1f / SharedPathfindingSystem.SubStep / 4f;
|
||||||
|
|
||||||
|
var masked = crumb.Data.CollisionMask != 0 || crumb.Data.CollisionLayer != 0;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
if ((crumb.Data.Flags & PathfindingBreadcrumbFlag.Space) != 0x0)
|
||||||
|
{
|
||||||
|
color = Color.Green;
|
||||||
|
}
|
||||||
|
else if (masked)
|
||||||
|
{
|
||||||
|
color = Color.Blue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
color = Color.Orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
var coordinate = _system.GetCoordinate(chunk.Key, crumb.Coordinates);
|
||||||
|
worldHandle.DrawRect(new Box2(coordinate - edge, coordinate + edge), color.WithAlpha(0.25f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Polys) != 0x0 &&
|
||||||
|
mouseWorldPos.MapId == args.MapId)
|
||||||
|
{
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, aabb))
|
||||||
|
{
|
||||||
|
if (!_system.Polys.TryGetValue(grid.GridEntityId, out var data))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHandle.SetTransform(grid.WorldMatrix);
|
||||||
|
var localAABB = grid.InvWorldMatrix.TransformBox(aabb);
|
||||||
|
|
||||||
|
foreach (var chunk in data)
|
||||||
|
{
|
||||||
|
var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if (!chunkAABB.Intersects(localAABB))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var tile in chunk.Value)
|
||||||
|
{
|
||||||
|
foreach (var poly in tile.Value)
|
||||||
|
{
|
||||||
|
worldHandle.DrawRect(poly.Box, Color.Green.WithAlpha(0.25f));
|
||||||
|
worldHandle.DrawRect(poly.Box, Color.Red, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.PolyNeighbors) != 0x0 &&
|
||||||
|
mouseWorldPos.MapId == args.MapId)
|
||||||
|
{
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, aabb))
|
||||||
|
{
|
||||||
|
if (!_system.Polys.TryGetValue(grid.GridEntityId, out var data) ||
|
||||||
|
!_entManager.TryGetComponent<TransformComponent>(grid.GridEntityId, out var gridXform))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (_, _, worldMatrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv();
|
||||||
|
worldHandle.SetTransform(worldMatrix);
|
||||||
|
var localAABB = invMatrix.TransformBox(aabb);
|
||||||
|
|
||||||
|
foreach (var chunk in data)
|
||||||
|
{
|
||||||
|
var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if (!chunkAABB.Intersects(localAABB))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var tile in chunk.Value)
|
||||||
|
{
|
||||||
|
foreach (var poly in tile.Value)
|
||||||
|
{
|
||||||
|
foreach (var neighborPoly in poly.Neighbors)
|
||||||
|
{
|
||||||
|
Color color;
|
||||||
|
Vector2 neighborPos;
|
||||||
|
|
||||||
|
if (neighborPoly.EntityId != poly.GraphUid)
|
||||||
|
{
|
||||||
|
color = Color.Green;
|
||||||
|
var neighborMap = neighborPoly.ToMap(_entManager);
|
||||||
|
|
||||||
|
if (neighborMap.MapId != args.MapId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighborPos = invMatrix.Transform(neighborMap.Position);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
color = Color.Blue;
|
||||||
|
neighborPos = neighborPoly.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldHandle.DrawLine(poly.Box.Center, neighborPos, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Chunks) != 0x0)
|
||||||
|
{
|
||||||
|
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
|
||||||
|
{
|
||||||
|
if (!_system.Breadcrumbs.TryGetValue(grid.GridEntityId, out var crumbs))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHandle.SetTransform(grid.WorldMatrix);
|
||||||
|
var localAABB = grid.InvWorldMatrix.TransformBox(args.WorldBounds);
|
||||||
|
|
||||||
|
foreach (var chunk in crumbs)
|
||||||
|
{
|
||||||
|
var origin = chunk.Key * SharedPathfindingSystem.ChunkSize;
|
||||||
|
|
||||||
|
var chunkAABB = new Box2(origin, origin + SharedPathfindingSystem.ChunkSize);
|
||||||
|
|
||||||
|
if (!chunkAABB.Intersects(localAABB))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHandle.DrawRect(chunkAABB, Color.Red, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.Routes) != 0x0)
|
||||||
|
{
|
||||||
|
foreach (var route in _system.Routes)
|
||||||
|
{
|
||||||
|
foreach (var node in route.Message.Path)
|
||||||
|
{
|
||||||
|
if (!_entManager.TryGetComponent<TransformComponent>(node.GraphUid, out var graphXform))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
worldHandle.SetTransform(graphXform.WorldMatrix);
|
||||||
|
worldHandle.DrawRect(node.Box, Color.Orange.WithAlpha(0.10f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_system.Modes & PathfindingDebugMode.RouteCosts) != 0x0)
|
||||||
|
{
|
||||||
|
var matrix = EntityUid.Invalid;
|
||||||
|
|
||||||
|
foreach (var route in _system.Routes)
|
||||||
|
{
|
||||||
|
var highestGScore = route.Message.Costs.Values.Max();
|
||||||
|
|
||||||
|
foreach (var (node, cost) in route.Message.Costs)
|
||||||
|
{
|
||||||
|
if (matrix != node.GraphUid)
|
||||||
|
{
|
||||||
|
if (!_entManager.TryGetComponent<TransformComponent>(node.GraphUid, out var graphXform))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
matrix = node.GraphUid;
|
||||||
|
worldHandle.SetTransform(graphXform.WorldMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
worldHandle.DrawRect(node.Box, new Color(0f, cost / highestGScore, 1f - (cost / highestGScore), 0.10f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worldHandle.SetTransform(Matrix3.Identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace Content.Server.CPUJob.JobQueues.Queues
|
|
||||||
{
|
|
||||||
public sealed class AiActionJobQueue : JobQueue {}
|
|
||||||
}
|
|
||||||
@@ -170,7 +170,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
|||||||
|
|
||||||
args.Verbs.Add(new AlternativeVerb()
|
args.Verbs.Add(new AlternativeVerb()
|
||||||
{
|
{
|
||||||
Text = "Pry door",
|
Text = Loc.GetString("door-pry"),
|
||||||
Impact = LogImpact.Low,
|
Impact = LogImpact.Low,
|
||||||
Act = () => TryPryDoor(uid, args.User, args.User, component, true),
|
Act = () => TryPryDoor(uid, args.User, args.User, component, true),
|
||||||
});
|
});
|
||||||
@@ -180,7 +180,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pry open a door. This does not check if the user is holding the required tool.
|
/// Pry open a door. This does not check if the user is holding the required tool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door, bool force = false)
|
public bool TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door, bool force = false)
|
||||||
{
|
{
|
||||||
if (door.BeingPried)
|
if (door.BeingPried)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ public enum CombatStatus : byte
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
TargetUnreachable,
|
TargetUnreachable,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the target is outside of our melee range.
|
||||||
|
/// </summary>
|
||||||
|
TargetOutOfRange,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set if the weapon we were assigned is no longer valid.
|
/// Set if the weapon we were assigned is no longer valid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Content.Server.CPUJob.JobQueues;
|
using Content.Server.CPUJob.JobQueues;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
namespace Content.Server.NPC.Components;
|
namespace Content.Server.NPC.Components;
|
||||||
@@ -10,24 +11,23 @@ namespace Content.Server.NPC.Components;
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed class NPCSteeringComponent : Component
|
public sealed class NPCSteeringComponent : Component
|
||||||
{
|
{
|
||||||
[ViewVariables] public Job<Queue<TileRef>>? Pathfind = null;
|
/// <summary>
|
||||||
|
/// Have we currently requested a path.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public bool Pathfind => PathfindToken != null;
|
||||||
[ViewVariables] public CancellationTokenSource? PathfindToken = null;
|
[ViewVariables] public CancellationTokenSource? PathfindToken = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current path we're following to our coordinates.
|
/// Current path we're following to our coordinates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables] public Queue<TileRef> CurrentPath = new();
|
[ViewVariables] public Queue<PathPoly> CurrentPath = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// End target that we're trying to move to.
|
/// End target that we're trying to move to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite)] public EntityCoordinates Coordinates;
|
[ViewVariables(VVAccess.ReadWrite)] public EntityCoordinates Coordinates;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Target that we're trying to move to. If we have a path then this will be the first node on the path.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables] public EntityCoordinates CurrentTarget;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How close are we trying to get to the coordinates before being considered in range.
|
/// How close are we trying to get to the coordinates before being considered in range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -36,9 +36,18 @@ public sealed class NPCSteeringComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// How far does the last node in the path need to be before considering re-pathfinding.
|
/// How far does the last node in the path need to be before considering re-pathfinding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite)] public float RepathRange = 1.5f;
|
[ViewVariables(VVAccess.ReadWrite)] public float RepathRange = 1.2f;
|
||||||
|
|
||||||
|
public const int FailedPathLimit = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many times we've failed to pathfind. Once this hits the limit we'll stop steering.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables] public int FailedPathCount;
|
||||||
|
|
||||||
[ViewVariables] public SteeringStatus Status = SteeringStatus.Moving;
|
[ViewVariables] public SteeringStatus Status = SteeringStatus.Moving;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)] public PathFlags Flags = PathFlags.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SteeringStatus : byte
|
public enum SteeringStatus : byte
|
||||||
|
|||||||
@@ -42,4 +42,6 @@ public sealed class HTNComponent : NPCComponent
|
|||||||
/// Is this NPC currently planning?
|
/// Is this NPC currently planning?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables] public bool Planning => PlanningJob != null;
|
[ViewVariables] public bool Planning => PlanningJob != null;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public sealed class HTNPlanJob : Job<HTNPlan>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (valid, effects) = await primitive.Operator.Plan(blackboard);
|
var (valid, effects) = await primitive.Operator.Plan(blackboard, Cancellation);
|
||||||
|
|
||||||
if (!valid)
|
if (!valid)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -151,8 +151,9 @@ public sealed class HTNSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
_sawmill.Fatal($"Received exception on planning job for {comp.Owner}!");
|
_sawmill.Fatal($"Received exception on planning job for {comp.Owner}!");
|
||||||
_npc.SleepNPC(comp.Owner);
|
_npc.SleepNPC(comp.Owner);
|
||||||
|
var exc = comp.PlanningJob.Exception;
|
||||||
RemComp<HTNComponent>(comp.Owner);
|
RemComp<HTNComponent>(comp.Owner);
|
||||||
throw comp.PlanningJob.Exception;
|
throw exc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a new planning job has finished then handle it.
|
// If a new planning job has finished then handle it.
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Concrete code that gets run for an NPC task.
|
/// Concrete code that gets run for an NPC task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ImplicitDataDefinitionForInheritors]
|
[ImplicitDataDefinitionForInheritors, MeansImplicitUse]
|
||||||
public abstract class HTNOperator
|
public abstract class HTNOperator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -20,9 +22,11 @@ public abstract class HTNOperator
|
|||||||
/// Called during planning.
|
/// Called during planning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blackboard">The blackboard for the NPC.</param>
|
/// <param name="blackboard">The blackboard for the NPC.</param>
|
||||||
|
/// <param name="cancelToken"></param>
|
||||||
/// <returns>Whether the plan is still valid and the effects to apply to the blackboard.
|
/// <returns>Whether the plan is still valid and the effects to apply to the blackboard.
|
||||||
/// These get re-applied during execution and are up to the operator to use or discard.</returns>
|
/// These get re-applied during execution and are up to the operator to use or discard.</returns>
|
||||||
public virtual async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public virtual async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.MobState;
|
using Content.Server.MobState;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
@@ -34,7 +35,8 @@ public sealed class MeleeOperator : HTNOperator
|
|||||||
melee.Target = blackboard.GetValue<EntityUid>(TargetKey);
|
melee.Target = blackboard.GetValue<EntityUid>(TargetKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
// Don't attack if they're already as wounded as we want them.
|
// Don't attack if they're already as wounded as we want them.
|
||||||
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
|
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
|
||||||
@@ -62,7 +64,6 @@ public sealed class MeleeOperator : HTNOperator
|
|||||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(blackboard, frameTime);
|
base.Update(blackboard, frameTime);
|
||||||
// TODO:
|
|
||||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||||
var status = HTNOperatorStatus.Continuing;
|
var status = HTNOperatorStatus.Continuing;
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ public sealed class MeleeOperator : HTNOperator
|
|||||||
{
|
{
|
||||||
switch (combat.Status)
|
switch (combat.Status)
|
||||||
{
|
{
|
||||||
|
case CombatStatus.TargetOutOfRange:
|
||||||
case CombatStatus.Normal:
|
case CombatStatus.Normal:
|
||||||
status = HTNOperatorStatus.Continuing;
|
status = HTNOperatorStatus.Continuing;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,32 +1,24 @@
|
|||||||
using Robust.Shared.Map;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects a target for melee.
|
/// Selects a target for melee.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[MeansImplicitUse]
|
||||||
public sealed class PickMeleeTargetOperator : NPCCombatOperator
|
public sealed class PickMeleeTargetOperator : NPCCombatOperator
|
||||||
{
|
{
|
||||||
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
|
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery<TransformComponent> xformQuery)
|
||||||
{
|
{
|
||||||
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
|
|
||||||
|
|
||||||
if (!xformQuery.TryGetComponent(uid, out var targetXform))
|
|
||||||
return -1f;
|
|
||||||
|
|
||||||
var targetCoordinates = targetXform.Coordinates;
|
|
||||||
|
|
||||||
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
|
|
||||||
return -1f;
|
|
||||||
|
|
||||||
var rating = 0f;
|
var rating = 0f;
|
||||||
|
|
||||||
if (existingTarget == uid)
|
if (existingTarget == uid)
|
||||||
{
|
{
|
||||||
rating += 3f;
|
rating += 2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
rating += 1f / distance * 4f;
|
if (distance > 0f)
|
||||||
|
rating += 50f / distance;
|
||||||
|
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
using Content.Server.NPC.Pathfinding;
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Content.Server.NPC.Systems;
|
using Content.Server.NPC.Systems;
|
||||||
|
using Content.Shared.NPC;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
|
using YamlDotNet.Core.Tokens;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||||
|
|
||||||
@@ -58,7 +59,8 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
_steering = sysManager.GetEntitySystem<NPCSteeringSystem>();
|
_steering = sysManager.GetEntitySystem<NPCSteeringSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
if (!blackboard.TryGetValue<EntityCoordinates>(TargetKey, out var targetCoordinates))
|
if (!blackboard.TryGetValue<EntityCoordinates>(TargetKey, out var targetCoordinates))
|
||||||
{
|
{
|
||||||
@@ -72,8 +74,7 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
return (false, null);
|
return (false, null);
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) ||
|
if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) ||
|
||||||
!_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid) ||
|
!_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid))
|
||||||
ownerGrid != targetGrid)
|
|
||||||
{
|
{
|
||||||
return (false, null);
|
return (false, null);
|
||||||
}
|
}
|
||||||
@@ -97,30 +98,25 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var cancelToken = new CancellationTokenSource();
|
var path = await _pathfind.GetPath(
|
||||||
var access = blackboard.GetValueOrDefault<ICollection<string>>(NPCBlackboard.Access) ?? new List<string>();
|
blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
|
||||||
|
xform.Coordinates,
|
||||||
|
targetCoordinates,
|
||||||
|
range,
|
||||||
|
cancelToken,
|
||||||
|
_pathfind.GetFlags(blackboard));
|
||||||
|
|
||||||
var job = _pathfind.RequestPath(
|
if (path.Result != PathResult.Path)
|
||||||
new PathfindingArgs(
|
{
|
||||||
blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
|
|
||||||
access,
|
|
||||||
body.CollisionMask,
|
|
||||||
ownerGrid.GetTileRef(xform.Coordinates),
|
|
||||||
ownerGrid.GetTileRef(targetCoordinates),
|
|
||||||
range), cancelToken.Token);
|
|
||||||
|
|
||||||
job.Run();
|
|
||||||
|
|
||||||
await job.AsTask.WaitAsync(cancelToken.Token);
|
|
||||||
|
|
||||||
if (job.Result == null)
|
|
||||||
return (false, null);
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
{NPCBlackboard.OwnerCoordinates, targetCoordinates},
|
{NPCBlackboard.OwnerCoordinates, targetCoordinates},
|
||||||
{PathfindKey, job.Result}
|
{PathfindKey, path}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator.
|
// Given steering is complicated we'll hand it off to a dedicated system rather than this singleton operator.
|
||||||
@@ -131,23 +127,25 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
|
|
||||||
// Need to remove the planning value for execution.
|
// Need to remove the planning value for execution.
|
||||||
blackboard.Remove<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
|
blackboard.Remove<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
|
||||||
|
var targetCoordinates = blackboard.GetValue<EntityCoordinates>(TargetKey);
|
||||||
|
|
||||||
// Re-use the path we may have if applicable.
|
// Re-use the path we may have if applicable.
|
||||||
var comp = _steering.Register(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), blackboard.GetValue<EntityCoordinates>(TargetKey));
|
var comp = _steering.Register(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner), targetCoordinates);
|
||||||
|
|
||||||
if (blackboard.TryGetValue<float>(RangeKey, out var range))
|
if (blackboard.TryGetValue<float>(RangeKey, out var range))
|
||||||
{
|
{
|
||||||
comp.Range = range;
|
comp.Range = range;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blackboard.TryGetValue<Queue<TileRef>>(PathfindKey, out var path))
|
if (blackboard.TryGetValue<PathResultEvent>(PathfindKey, out var result))
|
||||||
{
|
{
|
||||||
if (blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates))
|
if (blackboard.TryGetValue<EntityCoordinates>(NPCBlackboard.OwnerCoordinates, out var coordinates))
|
||||||
{
|
{
|
||||||
_steering.PrunePath(coordinates, path);
|
var mapCoords = coordinates.ToMap(_entManager);
|
||||||
|
_steering.PrunePath(mapCoords, targetCoordinates.ToMapPos(_entManager) - mapCoords.Position, result.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
comp.CurrentPath = path;
|
comp.CurrentPath = result.Path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +161,7 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OwnerCoordinates is only used in planning so dump it.
|
// OwnerCoordinates is only used in planning so dump it.
|
||||||
blackboard.Remove<Queue<TileRef>>(PathfindKey);
|
blackboard.Remove<PathResultEvent>(PathfindKey);
|
||||||
|
|
||||||
if (RemoveKeyOnFinish)
|
if (RemoveKeyOnFinish)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Interaction;
|
using Content.Server.Interaction;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Content.Server.NPC.Systems;
|
using Content.Server.NPC.Systems;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.MobState;
|
using Content.Shared.MobState;
|
||||||
using Content.Shared.MobState.Components;
|
using Content.Shared.MobState.Components;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -10,8 +14,9 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
|||||||
public abstract class NPCCombatOperator : HTNOperator
|
public abstract class NPCCombatOperator : HTNOperator
|
||||||
{
|
{
|
||||||
[Dependency] protected readonly IEntityManager EntManager = default!;
|
[Dependency] protected readonly IEntityManager EntManager = default!;
|
||||||
private FactionSystem _tags = default!;
|
private FactionSystem _factions = default!;
|
||||||
protected InteractionSystem Interaction = default!;
|
protected InteractionSystem Interaction = default!;
|
||||||
|
private PathfindingSystem _pathfinding = default!;
|
||||||
|
|
||||||
[ViewVariables, DataField("key")] public string Key = "CombatTarget";
|
[ViewVariables, DataField("key")] public string Key = "CombatTarget";
|
||||||
|
|
||||||
@@ -21,16 +26,25 @@ public abstract class NPCCombatOperator : HTNOperator
|
|||||||
[ViewVariables, DataField("keyCoordinates")]
|
[ViewVariables, DataField("keyCoordinates")]
|
||||||
public string KeyCoordinates = "CombatTargetCoordinates";
|
public string KeyCoordinates = "CombatTargetCoordinates";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regardless of pathfinding or LOS these are the max we'll check
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxConsideredTargets = 10;
|
||||||
|
private const int MaxTargetCount = 5;
|
||||||
|
|
||||||
public override void Initialize(IEntitySystemManager sysManager)
|
public override void Initialize(IEntitySystemManager sysManager)
|
||||||
{
|
{
|
||||||
base.Initialize(sysManager);
|
base.Initialize(sysManager);
|
||||||
_tags = sysManager.GetEntitySystem<FactionSystem>();
|
sysManager.GetEntitySystem<ExamineSystemShared>();
|
||||||
|
_factions = sysManager.GetEntitySystem<FactionSystem>();
|
||||||
Interaction = sysManager.GetEntitySystem<InteractionSystem>();
|
Interaction = sysManager.GetEntitySystem<InteractionSystem>();
|
||||||
|
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
var targets = GetTargets(blackboard);
|
var targets = await GetTargets(blackboard);
|
||||||
|
|
||||||
if (targets.Count == 0)
|
if (targets.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -49,34 +63,68 @@ public abstract class NPCCombatOperator : HTNOperator
|
|||||||
return (true, effects);
|
return (true, effects);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<(EntityUid Entity, float Rating)> GetTargets(NPCBlackboard blackboard)
|
private async Task<List<(EntityUid Entity, float Rating, float Distance)>> GetTargets(NPCBlackboard blackboard)
|
||||||
{
|
{
|
||||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||||
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntManager);
|
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntManager);
|
||||||
var targets = new List<(EntityUid Entity, float Rating)>();
|
var targets = new List<(EntityUid Entity, float Rating, float Distance)>();
|
||||||
|
|
||||||
blackboard.TryGetValue<EntityUid>(Key, out var existingTarget);
|
blackboard.TryGetValue<EntityUid>(Key, out var existingTarget);
|
||||||
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
|
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
|
||||||
var mobQuery = EntManager.GetEntityQuery<MobStateComponent>();
|
var mobQuery = EntManager.GetEntityQuery<MobStateComponent>();
|
||||||
var canMove = blackboard.GetValueOrDefault<bool>(NPCBlackboard.CanMove, EntManager);
|
var canMove = blackboard.GetValueOrDefault<bool>(NPCBlackboard.CanMove, EntManager);
|
||||||
|
var cancelToken = new CancellationTokenSource();
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
if (xformQuery.TryGetComponent(existingTarget, out var targetXform))
|
||||||
|
{
|
||||||
|
var distance = await _pathfinding.GetPathDistance(owner, targetXform.Coordinates,
|
||||||
|
SharedInteractionSystem.InteractionRange, cancelToken.Token, _pathfinding.GetFlags(blackboard));
|
||||||
|
|
||||||
|
if (distance != null)
|
||||||
|
{
|
||||||
|
targets.Add((existingTarget, GetRating(blackboard, existingTarget, existingTarget, distance.Value, canMove, xformQuery), distance.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Need a perception system instead
|
// TODO: Need a perception system instead
|
||||||
foreach (var target in _tags
|
// TODO: This will be expensive so will be good to optimise and cut corners.
|
||||||
|
foreach (var target in _factions
|
||||||
.GetNearbyHostiles(owner, radius))
|
.GetNearbyHostiles(owner, radius))
|
||||||
{
|
{
|
||||||
if (mobQuery.TryGetComponent(target, out var mobState) &&
|
if (mobQuery.TryGetComponent(target, out var mobState) &&
|
||||||
mobState.CurrentState > DamageState.Alive)
|
mobState.CurrentState > DamageState.Alive ||
|
||||||
|
!xformQuery.TryGetComponent(target, out targetXform))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
targets.Add((target, GetRating(blackboard, target, existingTarget, canMove, xformQuery)));
|
count++;
|
||||||
|
|
||||||
|
if (count >= MaxConsideredTargets)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!ExamineSystemShared.InRangeUnOccluded(owner, target, radius, null))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var distance = await _pathfinding.GetPathDistance(owner, targetXform.Coordinates,
|
||||||
|
SharedInteractionSystem.InteractionRange, cancelToken.Token, _pathfinding.GetFlags(blackboard));
|
||||||
|
|
||||||
|
if (distance == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
targets.Add((target, GetRating(blackboard, target, existingTarget, distance.Value, canMove, xformQuery), distance.Value));
|
||||||
|
|
||||||
|
if (targets.Count >= MaxTargetCount)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
targets.Sort((x, y) => y.Rating.CompareTo(x.Rating));
|
targets.Sort((x, y) => y.Rating.CompareTo(x.Rating));
|
||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove,
|
protected abstract float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove,
|
||||||
EntityQuery<TransformComponent> xformQuery);
|
EntityQuery<TransformComponent> xformQuery);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.NPC.Pathfinding;
|
using Content.Server.NPC.Pathfinding;
|
||||||
@@ -13,8 +14,7 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
private PathfindingSystem _pathfinding = default!;
|
||||||
private PathfindingSystem _path = default!;
|
|
||||||
private EntityLookupSystem _lookup = default!;
|
private EntityLookupSystem _lookup = default!;
|
||||||
|
|
||||||
[DataField("rangeKey", required: true)]
|
[DataField("rangeKey", required: true)]
|
||||||
@@ -26,15 +26,22 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
|
|||||||
[ViewVariables, DataField("component", required: true)]
|
[ViewVariables, DataField("component", required: true)]
|
||||||
public string Component = string.Empty;
|
public string Component = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("pathfindKey")]
|
||||||
|
public string PathfindKey = "MovementPathfind";
|
||||||
|
|
||||||
public override void Initialize(IEntitySystemManager sysManager)
|
public override void Initialize(IEntitySystemManager sysManager)
|
||||||
{
|
{
|
||||||
base.Initialize(sysManager);
|
base.Initialize(sysManager);
|
||||||
_path = sysManager.GetEntitySystem<PathfindingSystem>();
|
|
||||||
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
|
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
|
||||||
|
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
// Check if the component exists
|
// Check if the component exists
|
||||||
if (!_factory.TryGetRegistration(Component, out var registration))
|
if (!_factory.TryGetRegistration(Component, out var registration))
|
||||||
@@ -56,6 +63,7 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
|
|||||||
|
|
||||||
// TODO: Need to get ones that are accessible.
|
// TODO: Need to get ones that are accessible.
|
||||||
// TODO: Look at unreal HTN to see repeatable ones maybe?
|
// TODO: Look at unreal HTN to see repeatable ones maybe?
|
||||||
|
// TODO: Need type
|
||||||
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, range))
|
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, range))
|
||||||
{
|
{
|
||||||
if (entity == owner || !query.TryGetComponent(entity, out var comp))
|
if (entity == owner || !query.TryGetComponent(entity, out var comp))
|
||||||
@@ -69,27 +77,31 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
|
|||||||
return (false, null);
|
return (false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blackboard.TryGetValue<float>(RangeKey, out var maxRange);
|
||||||
|
|
||||||
|
if (maxRange == 0f)
|
||||||
|
maxRange = 7f;
|
||||||
|
|
||||||
while (targets.Count > 0)
|
while (targets.Count > 0)
|
||||||
{
|
{
|
||||||
// TODO: Get nearest at some stage
|
var path = await _pathfinding.GetRandomPath(
|
||||||
var target = _random.PickAndTake(targets);
|
owner,
|
||||||
|
1.4f,
|
||||||
|
maxRange,
|
||||||
|
cancelToken,
|
||||||
|
flags: _pathfinding.GetFlags(blackboard));
|
||||||
|
|
||||||
// TODO: God the path api sucks PLUS I need some fast way to get this.
|
if (path.Result != PathResult.Path)
|
||||||
var job = _path.RequestPath(owner, target.Owner, CancellationToken.None);
|
|
||||||
|
|
||||||
if (job == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
await job.AsTask;
|
|
||||||
|
|
||||||
if (job.Result == null || !_entManager.TryGetComponent<TransformComponent>(target.Owner, out var targetXform))
|
|
||||||
{
|
{
|
||||||
continue;
|
return (false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var target = path.Path.Last().Coordinates;
|
||||||
|
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
{ TargetKey, targetXform.Coordinates },
|
{ TargetKey, target },
|
||||||
|
{ PathfindKey, path}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.NPC.Pathfinding;
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Content.Server.NPC.Pathfinding.Accessible;
|
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||||
@@ -10,9 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PickAccessibleOperator : HTNOperator
|
public sealed class PickAccessibleOperator : HTNOperator
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
private PathfindingSystem _pathfinding = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
private AiReachableSystem _reachable = default!;
|
|
||||||
|
|
||||||
[DataField("rangeKey", required: true)]
|
[DataField("rangeKey", required: true)]
|
||||||
public string RangeKey = string.Empty;
|
public string RangeKey = string.Empty;
|
||||||
@@ -20,44 +19,48 @@ public sealed class PickAccessibleOperator : HTNOperator
|
|||||||
[ViewVariables, DataField("targetKey", required: true)]
|
[ViewVariables, DataField("targetKey", required: true)]
|
||||||
public string TargetKey = string.Empty;
|
public string TargetKey = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("pathfindKey")]
|
||||||
|
public string PathfindKey = "MovementPathfind";
|
||||||
|
|
||||||
public override void Initialize(IEntitySystemManager sysManager)
|
public override void Initialize(IEntitySystemManager sysManager)
|
||||||
{
|
{
|
||||||
base.Initialize(sysManager);
|
base.Initialize(sysManager);
|
||||||
_reachable = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiReachableSystem>();
|
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
// Very inefficient (should weight each region by its node count) but better than the old system
|
// Very inefficient (should weight each region by its node count) but better than the old system
|
||||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||||
|
|
||||||
if (!_entManager.TryGetComponent(_entManager.GetComponent<TransformComponent>(owner).GridUid, out IMapGridComponent? grid))
|
blackboard.TryGetValue<float>(RangeKey, out var maxRange);
|
||||||
return (false, null);
|
|
||||||
|
|
||||||
var reachableArgs = ReachableArgs.GetArgs(owner, blackboard.GetValueOrDefault<float>(RangeKey));
|
if (maxRange == 0f)
|
||||||
var entityRegion = _reachable.GetRegion(owner);
|
maxRange = 7f;
|
||||||
var reachableRegions = _reachable.GetReachableRegions(reachableArgs, entityRegion);
|
|
||||||
|
|
||||||
if (reachableRegions.Count == 0)
|
var path = await _pathfinding.GetRandomPath(
|
||||||
return (false, null);
|
owner,
|
||||||
|
1.4f,
|
||||||
|
maxRange,
|
||||||
|
cancelToken,
|
||||||
|
flags: _pathfinding.GetFlags(blackboard));
|
||||||
|
|
||||||
var reachableNodes = new List<PathfindingNode>();
|
if (path.Result != PathResult.Path)
|
||||||
|
|
||||||
foreach (var region in reachableRegions)
|
|
||||||
{
|
{
|
||||||
foreach (var node in region.Nodes)
|
return (false, null);
|
||||||
{
|
|
||||||
reachableNodes.Add(node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetNode = _random.Pick(reachableNodes);
|
var target = path.Path.Last().Coordinates;
|
||||||
|
|
||||||
var target = grid.Grid.GridTileToLocal(targetNode.TileRef.GridIndices);
|
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
{ TargetKey, target },
|
{ TargetKey, target },
|
||||||
|
{ PathfindKey, path}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
@@ -10,7 +11,8 @@ public sealed class PickRandomRotationOperator : HTNOperator
|
|||||||
[ViewVariables, DataField("targetKey")]
|
[ViewVariables, DataField("targetKey")]
|
||||||
public string TargetKey = "RotateTarget";
|
public string TargetKey = "RotateTarget";
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
var rotation = _random.NextAngle();
|
var rotation = _random.NextAngle();
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ public sealed class RandomOperator : HTNOperator
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("maxKey", required: true)] public string MaxKey = string.Empty;
|
[DataField("maxKey", required: true)] public string MaxKey = string.Empty;
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
using Robust.Shared.Map;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects a target for ranged combat.
|
/// Selects a target for ranged combat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
public sealed class PickRangedTargetOperator : NPCCombatOperator
|
public sealed class PickRangedTargetOperator : NPCCombatOperator
|
||||||
{
|
{
|
||||||
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery<TransformComponent> xformQuery)
|
protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery<TransformComponent> xformQuery)
|
||||||
{
|
{
|
||||||
var ourCoordinates = blackboard.GetValueOrDefault<EntityCoordinates>(NPCBlackboard.OwnerCoordinates);
|
|
||||||
|
|
||||||
if (!xformQuery.TryGetComponent(uid, out var targetXform))
|
|
||||||
return -1f;
|
|
||||||
|
|
||||||
var targetCoordinates = targetXform.Coordinates;
|
|
||||||
|
|
||||||
if (!ourCoordinates.TryDistance(EntManager, targetCoordinates, out var distance))
|
|
||||||
return -1f;
|
|
||||||
|
|
||||||
// TODO: Uhh make this better with penetration or something.
|
|
||||||
var inLOS = Interaction.InRangeUnobstructed(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
|
|
||||||
uid, distance + 0.1f);
|
|
||||||
|
|
||||||
if (!canMove && !inLOS)
|
|
||||||
return -1f;
|
|
||||||
|
|
||||||
// Yeah look I just came up with values that seemed okay but they will need a lot of tweaking.
|
// Yeah look I just came up with values that seemed okay but they will need a lot of tweaking.
|
||||||
// Having a debug overlay just to project these would be very useful when finetuning in future.
|
// Having a debug overlay just to project these would be very useful when finetuning in future.
|
||||||
var rating = 0f;
|
var rating = 0f;
|
||||||
|
|
||||||
if (inLOS)
|
|
||||||
rating += 4f;
|
|
||||||
|
|
||||||
if (existingTarget == uid)
|
if (existingTarget == uid)
|
||||||
{
|
{
|
||||||
rating += 2f;
|
rating += 2f;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
using Content.Shared.MobState;
|
using Content.Shared.MobState;
|
||||||
@@ -24,7 +25,8 @@ public sealed class RangedOperator : HTNOperator
|
|||||||
|
|
||||||
// Like movement we add a component and pass it off to the dedicated system.
|
// Like movement we add a component and pass it off to the dedicated system.
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
// Don't attack if they're already as wounded as we want them.
|
// Don't attack if they're already as wounded as we want them.
|
||||||
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
|
if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||||
@@ -12,7 +13,8 @@ public sealed class SetFloatOperator : HTNOperator
|
|||||||
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
|
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
|
||||||
public float Amount;
|
public float Amount;
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
return (true, new Dictionary<string, object>()
|
return (true, new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Chemistry.Components.SolutionManager;
|
using Content.Server.Chemistry.Components.SolutionManager;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
@@ -31,7 +32,8 @@ public sealed class PickNearbyInjectableOperator : HTNOperator
|
|||||||
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
|
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
@@ -10,7 +11,8 @@ public sealed class PickPathfindPointOperator : HTNOperator
|
|||||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard)
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
|
CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,16 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
|
|||||||
public const string Owner = "Owner";
|
public const string Owner = "Owner";
|
||||||
public const string OwnerCoordinates = "OwnerCoordinates";
|
public const string OwnerCoordinates = "OwnerCoordinates";
|
||||||
public const string MovementTarget = "MovementTarget";
|
public const string MovementTarget = "MovementTarget";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can the NPC pry open doors for steering.
|
||||||
|
/// </summary>
|
||||||
|
public const string NavPry = "NavPry";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can the NPC smash obstacles for steering.
|
||||||
|
/// </summary>
|
||||||
|
public const string NavSmash = "NavSmash";
|
||||||
public const string RotateSpeed = "RotateSpeed";
|
public const string RotateSpeed = "RotateSpeed";
|
||||||
public const string VisionRadius = "VisionRadius";
|
public const string VisionRadius = "VisionRadius";
|
||||||
public const float MeleeRange = 1f;
|
public const float MeleeRange = 1f;
|
||||||
|
|||||||
@@ -1,789 +0,0 @@
|
|||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Content.Shared.Access.Systems;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using Content.Shared.GameTicking;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether an AI has access to a specific pathfinding node.
|
|
||||||
/// </summary>
|
|
||||||
/// Long-term can be used to do hierarchical pathfinding
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class AiReachableSystem : EntitySystem
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
|
|
||||||
* The goal is so that we can more quickly discern if a specific node is reachable or not rather than
|
|
||||||
* Pathfinding the entire graph.
|
|
||||||
*
|
|
||||||
* There's a lot of different implementations of hierarchical or some variation of it: HPA*, PRA, HAA*, etc.
|
|
||||||
* (HPA* technically caches the edge nodes of each chunk), e.g. Rimworld, Factorio, etc.
|
|
||||||
* so we'll just write one with SS14's requirements in mind.
|
|
||||||
*
|
|
||||||
* There's probably a better data structure to use though you'd need to benchmark multiple ones to compare,
|
|
||||||
* at the very least on the memory side it could definitely be better.
|
|
||||||
*/
|
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
|
|
||||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Queued region updates
|
|
||||||
/// </summary>
|
|
||||||
private readonly HashSet<PathfindingChunk> _queuedUpdates = new();
|
|
||||||
|
|
||||||
// Oh god the nesting. Shouldn't need to go beyond this
|
|
||||||
/// <summary>
|
|
||||||
/// The corresponding regions for each PathfindingChunk.
|
|
||||||
/// Regions are groups of nodes with the same profile (for pathfinding purposes)
|
|
||||||
/// i.e. same collision, not-space, same access, etc.
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<EntityUid, Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>> _regions =
|
|
||||||
new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimum time for the cached reachable regions to be stored
|
|
||||||
/// </summary>
|
|
||||||
private const float MinCacheTime = 1.0f;
|
|
||||||
|
|
||||||
// Cache what regions are accessible from this region. Cached per ReachableArgs
|
|
||||||
// so multiple entities in the same region with the same args should all be able to share their reachable lookup
|
|
||||||
// Also need to store when we cached it to know if it's stale if the chunks have updated
|
|
||||||
|
|
||||||
// TODO: There's probably a more memory-efficient way to cache this
|
|
||||||
// Then again, there's likely also a more memory-efficient way to implement regions.
|
|
||||||
|
|
||||||
// Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
|
|
||||||
// Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
|
|
||||||
private readonly Dictionary<ReachableArgs, Dictionary<PathfindingRegion, (TimeSpan CacheTime, HashSet<PathfindingRegion> Regions)>> _cachedAccessible =
|
|
||||||
new();
|
|
||||||
|
|
||||||
private readonly List<PathfindingRegion> _queuedCacheDeletions = new();
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private HashSet<IPlayerSession> _subscribedSessions = new();
|
|
||||||
private int _runningCacheIdx = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
|
||||||
SubscribeLocalEvent<PathfindingChunkUpdateMessage>(RecalculateNodeRegions);
|
|
||||||
SubscribeLocalEvent<GridRemovalEvent>(GridRemoved);
|
|
||||||
#if DEBUG
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.SubscribeReachableMessage>(HandleSubscription);
|
|
||||||
SubscribeNetworkEvent<SharedAiDebug.UnsubscribeReachableMessage>(HandleUnsubscription);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Shutdown()
|
|
||||||
{
|
|
||||||
base.Shutdown();
|
|
||||||
|
|
||||||
_queuedUpdates.Clear();
|
|
||||||
_regions.Clear();
|
|
||||||
_cachedAccessible.Clear();
|
|
||||||
_queuedCacheDeletions.Clear();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GridRemoved(GridRemovalEvent ev)
|
|
||||||
{
|
|
||||||
_regions.Remove(ev.EntityUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
base.Update(frameTime);
|
|
||||||
foreach (var chunk in _queuedUpdates)
|
|
||||||
{
|
|
||||||
GenerateRegions(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Only send diffs instead
|
|
||||||
#if DEBUG
|
|
||||||
if (_subscribedSessions.Count > 0 && _queuedUpdates.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var (gridId, regs) in _regions)
|
|
||||||
{
|
|
||||||
if (regs.Count > 0)
|
|
||||||
{
|
|
||||||
SendRegionsDebugMessage(gridId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
_queuedUpdates.Clear();
|
|
||||||
|
|
||||||
foreach (var region in _queuedCacheDeletions)
|
|
||||||
{
|
|
||||||
ClearCache(region);
|
|
||||||
}
|
|
||||||
|
|
||||||
_queuedCacheDeletions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private void HandleSubscription(SharedAiDebug.SubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
_subscribedSessions.Add((IPlayerSession) eventArgs.SenderSession);
|
|
||||||
foreach (var (gridId, _) in _regions)
|
|
||||||
{
|
|
||||||
SendRegionsDebugMessage(gridId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleUnsubscription(SharedAiDebug.UnsubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
_subscribedSessions.Remove((IPlayerSession) eventArgs.SenderSession);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private void RecalculateNodeRegions(PathfindingChunkUpdateMessage message)
|
|
||||||
{
|
|
||||||
// TODO: Only need to do changed nodes ideally
|
|
||||||
// For now this is fine but it's a low-hanging fruit optimisation
|
|
||||||
_queuedUpdates.Add(message.Chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can the entity reach the target?
|
|
||||||
/// </summary>
|
|
||||||
/// First it does a quick check to see if there are any traversable nodes in range.
|
|
||||||
/// Then it will go through the regions to try and see if there's a region connection between the target and itself
|
|
||||||
/// Will used a cached region if available
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
/// <param name="target"></param>
|
|
||||||
/// <param name="range"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool CanAccess(EntityUid entity, EntityUid target, float range = 0.0f)
|
|
||||||
{
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(target);
|
|
||||||
// TODO: Handle this gracefully instead of just failing.
|
|
||||||
if (xform.GridUid == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var targetTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
|
||||||
var targetNode = _pathfindingSystem.GetNode(targetTile);
|
|
||||||
|
|
||||||
var collisionMask = 0;
|
|
||||||
if (EntityManager.TryGetComponent(entity, out IPhysBody? physics))
|
|
||||||
{
|
|
||||||
collisionMask = physics.CollisionMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var access = _accessReader.FindAccessTags(entity);
|
|
||||||
|
|
||||||
// We'll do a quick traversable check before going through regions
|
|
||||||
// If we can't access it we'll try to get a valid node in range (this is essentially an early-out)
|
|
||||||
if (!PathfindingHelpers.Traversable(collisionMask, access, targetNode))
|
|
||||||
{
|
|
||||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|
||||||
if (range == 0.0f)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathfindingArgs = new PathfindingArgs(entity, access, collisionMask, default, targetTile, range);
|
|
||||||
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
|
|
||||||
{
|
|
||||||
targetNode = node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CanAccess(entity, targetNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanAccess(EntityUid entity, PathfindingNode targetNode)
|
|
||||||
{
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(entity);
|
|
||||||
|
|
||||||
if (xform.GridUid != targetNode.TileRef.GridUid || xform.GridUid == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var entityTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
|
||||||
var entityNode = _pathfindingSystem.GetNode(entityTile);
|
|
||||||
var entityRegion = GetRegion(entityNode);
|
|
||||||
var targetRegion = GetRegion(targetNode);
|
|
||||||
// TODO: Regional pathfind from target to entity
|
|
||||||
// Early out
|
|
||||||
if (entityRegion == targetRegion)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll go from target's position to us because most of the time it's probably in a locked room rather than vice versa
|
|
||||||
var reachableArgs = ReachableArgs.GetArgs(entity, 7f);
|
|
||||||
var reachableRegions = GetReachableRegions(reachableArgs, targetRegion);
|
|
||||||
|
|
||||||
return entityRegion != null && reachableRegions.Contains(entityRegion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve the reachable regions
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reachableArgs"></param>
|
|
||||||
/// <param name="region"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public HashSet<PathfindingRegion> GetReachableRegions(ReachableArgs reachableArgs, PathfindingRegion? region)
|
|
||||||
{
|
|
||||||
// if we're on a node that's not tracked at all atm then region will be null
|
|
||||||
if (region == null)
|
|
||||||
{
|
|
||||||
return new HashSet<PathfindingRegion>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var cachedArgs = GetCachedArgs(reachableArgs);
|
|
||||||
(TimeSpan CacheTime, HashSet<PathfindingRegion> Regions) cached;
|
|
||||||
|
|
||||||
if (!IsCacheValid(cachedArgs, region))
|
|
||||||
{
|
|
||||||
cached = GetVisionReachable(cachedArgs, region);
|
|
||||||
_cachedAccessible[cachedArgs][region] = cached;
|
|
||||||
#if DEBUG
|
|
||||||
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, false);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cached = _cachedAccessible[cachedArgs][region];
|
|
||||||
#if DEBUG
|
|
||||||
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, true);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
return cached.Regions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get any adequate cached args if possible, otherwise just use ours
|
|
||||||
/// </summary>
|
|
||||||
/// Essentially any args that have the same access AND >= our vision radius can be used
|
|
||||||
/// <param name="accessibleArgs"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private ReachableArgs GetCachedArgs(ReachableArgs accessibleArgs)
|
|
||||||
{
|
|
||||||
ReachableArgs? foundArgs = null;
|
|
||||||
|
|
||||||
foreach (var (cachedAccessible, _) in _cachedAccessible)
|
|
||||||
{
|
|
||||||
if (Equals(cachedAccessible.Access, accessibleArgs.Access) &&
|
|
||||||
cachedAccessible.CollisionMask == accessibleArgs.CollisionMask &&
|
|
||||||
cachedAccessible.VisionRadius <= accessibleArgs.VisionRadius)
|
|
||||||
{
|
|
||||||
foundArgs = cachedAccessible;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundArgs ?? accessibleArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether there's a valid cache for our accessibility args.
|
|
||||||
/// Most regular mobs can share their cached accessibility with each other
|
|
||||||
/// </summary>
|
|
||||||
/// Will also remove it from the cache if it is invalid
|
|
||||||
/// <param name="accessibleArgs"></param>
|
|
||||||
/// <param name="region"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private bool IsCacheValid(ReachableArgs accessibleArgs, PathfindingRegion region)
|
|
||||||
{
|
|
||||||
if (!_cachedAccessible.TryGetValue(accessibleArgs, out var cachedArgs))
|
|
||||||
{
|
|
||||||
_cachedAccessible.Add(accessibleArgs, new Dictionary<PathfindingRegion, (TimeSpan, HashSet<PathfindingRegion>)>());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedArgs.TryGetValue(region, out var regionCache))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just so we don't invalidate the cache every tick we'll store it for a minimum amount of time
|
|
||||||
var currentTime = _gameTiming.CurTime;
|
|
||||||
if ((currentTime - regionCache.CacheTime).TotalSeconds < MinCacheTime)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkedAccess = new HashSet<PathfindingRegion>();
|
|
||||||
// Check if cache is stale
|
|
||||||
foreach (var accessibleRegion in regionCache.Regions)
|
|
||||||
{
|
|
||||||
if (checkedAccess.Contains(accessibleRegion)) continue;
|
|
||||||
|
|
||||||
// Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections)
|
|
||||||
// TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up
|
|
||||||
if (accessibleRegion.ParentChunk.LastUpdate > regionCache.CacheTime)
|
|
||||||
{
|
|
||||||
// Remove the stale cache, to be updated later
|
|
||||||
_cachedAccessible[accessibleArgs].Remove(region);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var neighbor in accessibleRegion.Neighbors)
|
|
||||||
{
|
|
||||||
if (checkedAccess.Contains(neighbor)) continue;
|
|
||||||
if (neighbor.ParentChunk.LastUpdate > regionCache.CacheTime)
|
|
||||||
{
|
|
||||||
_cachedAccessible[accessibleArgs].Remove(region);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
checkedAccess.Add(neighbor);
|
|
||||||
}
|
|
||||||
checkedAccess.Add(accessibleRegion);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Caches the entity's nearby accessible regions in vision radius
|
|
||||||
/// </summary>
|
|
||||||
/// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
|
|
||||||
/// <param name="reachableArgs"></param>
|
|
||||||
/// <param name="entityRegion"></param>
|
|
||||||
private (TimeSpan, HashSet<PathfindingRegion>) GetVisionReachable(ReachableArgs reachableArgs, PathfindingRegion entityRegion)
|
|
||||||
{
|
|
||||||
var openSet = new Queue<PathfindingRegion>();
|
|
||||||
openSet.Enqueue(entityRegion);
|
|
||||||
var closedSet = new HashSet<PathfindingRegion>();
|
|
||||||
var accessible = new HashSet<PathfindingRegion> {entityRegion};
|
|
||||||
|
|
||||||
while (openSet.Count > 0)
|
|
||||||
{
|
|
||||||
var region = openSet.Dequeue();
|
|
||||||
closedSet.Add(region);
|
|
||||||
|
|
||||||
foreach (var neighbor in region.Neighbors)
|
|
||||||
{
|
|
||||||
if (closedSet.Contains(neighbor))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distance is an approximation here so we'll be generous with it
|
|
||||||
// TODO: Could do better; the fewer nodes the better it is.
|
|
||||||
if (!neighbor.RegionTraversable(reachableArgs) ||
|
|
||||||
neighbor.Distance(entityRegion) > reachableArgs.VisionRadius + 1)
|
|
||||||
{
|
|
||||||
closedSet.Add(neighbor);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
openSet.Enqueue(neighbor);
|
|
||||||
accessible.Add(neighbor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (_gameTiming.CurTime, accessible);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
|
|
||||||
/// </summary>
|
|
||||||
/// Implicitly they would've already been merged if possible
|
|
||||||
/// <param name="region"></param>
|
|
||||||
/// <param name="node"></param>
|
|
||||||
private void UpdateRegionEdge(PathfindingRegion region, PathfindingNode node)
|
|
||||||
{
|
|
||||||
DebugTools.Assert(region.Nodes.Contains(node));
|
|
||||||
// Originally I tried just doing bottom and left but that doesn't work as the chunk update order is not guaranteed
|
|
||||||
|
|
||||||
var checkDirections = new[] {Direction.East, Direction.South, Direction.West, Direction.North};
|
|
||||||
foreach (var direction in checkDirections)
|
|
||||||
{
|
|
||||||
var directionNode = node.GetNeighbor(direction);
|
|
||||||
if (directionNode == null) continue;
|
|
||||||
|
|
||||||
var directionRegion = GetRegion(directionNode);
|
|
||||||
if (directionRegion == null || directionRegion == region) continue;
|
|
||||||
|
|
||||||
region.Neighbors.Add(directionRegion);
|
|
||||||
directionRegion.Neighbors.Add(region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the current region for this entity
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PathfindingRegion? GetRegion(EntityUid entity)
|
|
||||||
{
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(entity);
|
|
||||||
|
|
||||||
if (xform.GridUid == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var entityTile = _mapManager.GetGrid(xform.GridUid.Value).GetTileRef(xform.Coordinates);
|
|
||||||
var entityNode = _pathfindingSystem.GetNode(entityTile);
|
|
||||||
return GetRegion(entityNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the current region for this node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="node"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PathfindingRegion? GetRegion(PathfindingNode node)
|
|
||||||
{
|
|
||||||
// Not sure on the best way to optimise this
|
|
||||||
// On the one hand, just storing each node's region is faster buuutttt muh memory
|
|
||||||
// On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes?
|
|
||||||
// Someone smarter than me will know better
|
|
||||||
var parentChunk = node.ParentChunk;
|
|
||||||
|
|
||||||
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
|
|
||||||
if (!_regions.TryGetValue(parentChunk.GridId, out var chunk))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!chunk.TryGetValue(parentChunk, out var regions))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
foreach (var region in regions)
|
|
||||||
{
|
|
||||||
if (region.Nodes.Contains(node))
|
|
||||||
{
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Longer term this will probably be guaranteed a region but for now space etc. are no region
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add this node to the relevant region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="node"></param>
|
|
||||||
/// <param name="existingRegions">The cached region for each node</param>
|
|
||||||
/// <param name="chunkRegions">The existing regions in the chunk</param>
|
|
||||||
/// <param name="x">This is already calculated in advance so may as well re-use it</param>
|
|
||||||
/// <param name="y">This is already calculated in advance so may as well re-use it</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private PathfindingRegion? CalculateNode(
|
|
||||||
PathfindingNode node,
|
|
||||||
Dictionary<PathfindingNode, PathfindingRegion> existingRegions,
|
|
||||||
HashSet<PathfindingRegion> chunkRegions,
|
|
||||||
int x, int y)
|
|
||||||
{
|
|
||||||
DebugTools.Assert(_regions.ContainsKey(node.ParentChunk.GridId));
|
|
||||||
DebugTools.Assert(_regions[node.ParentChunk.GridId].ContainsKey(node.ParentChunk));
|
|
||||||
// TODO For now we don't have these regions but longer-term yeah sure
|
|
||||||
if (node.BlockedCollisionMask != 0x0 || node.TileRef.Tile.IsEmpty)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentChunk = node.ParentChunk;
|
|
||||||
// Doors will be their own separate region
|
|
||||||
// We won't store them in existingRegions so they don't show up and can't be connected to (at least for now)
|
|
||||||
if (node.AccessReaders.Count > 0)
|
|
||||||
{
|
|
||||||
var region = new PathfindingRegion(node, new HashSet<PathfindingNode>(1) {node}, true);
|
|
||||||
_regions[parentChunk.GridId][parentChunk].Add(region);
|
|
||||||
UpdateRegionEdge(region, node);
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relative x and y of the chunk
|
|
||||||
// If one of our bottom / left neighbors are in a region try to join them
|
|
||||||
// Otherwise, make our own region.
|
|
||||||
var leftNeighbor = x > 0 ? parentChunk.Nodes[x - 1, y] : null;
|
|
||||||
var bottomNeighbor = y > 0 ? parentChunk.Nodes[x, y - 1] : null;
|
|
||||||
PathfindingRegion? leftRegion;
|
|
||||||
PathfindingRegion? bottomRegion;
|
|
||||||
|
|
||||||
// We'll check if our left or down neighbors are already in a region and join them
|
|
||||||
|
|
||||||
// Is left node valid to connect to
|
|
||||||
if (leftNeighbor != null &&
|
|
||||||
existingRegions.TryGetValue(leftNeighbor, out leftRegion) &&
|
|
||||||
!leftRegion.IsDoor)
|
|
||||||
{
|
|
||||||
// We'll try and connect the left node's region to the bottom region if they're separate (yay merge)
|
|
||||||
if (bottomNeighbor != null &&
|
|
||||||
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
|
|
||||||
bottomRegion != leftRegion &&
|
|
||||||
!bottomRegion.IsDoor)
|
|
||||||
{
|
|
||||||
bottomRegion.Add(node);
|
|
||||||
existingRegions.Add(node, bottomRegion);
|
|
||||||
MergeInto(leftRegion, bottomRegion, existingRegions);
|
|
||||||
|
|
||||||
// Cleanup leftRegion
|
|
||||||
// MergeInto will remove it from the overall region chunk cache while we need to remove it from
|
|
||||||
// our short-term ones (chunkRegions and existingRegions)
|
|
||||||
chunkRegions.Remove(leftRegion);
|
|
||||||
|
|
||||||
foreach (var leftNode in leftRegion.Nodes)
|
|
||||||
{
|
|
||||||
existingRegions[leftNode] = bottomRegion;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bottomRegion;
|
|
||||||
}
|
|
||||||
|
|
||||||
leftRegion.Add(node);
|
|
||||||
existingRegions.Add(node, leftRegion);
|
|
||||||
UpdateRegionEdge(leftRegion, node);
|
|
||||||
return leftRegion;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Is bottom node valid to connect to
|
|
||||||
if (bottomNeighbor != null &&
|
|
||||||
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
|
|
||||||
!bottomRegion.IsDoor)
|
|
||||||
{
|
|
||||||
bottomRegion.Add(node);
|
|
||||||
existingRegions.Add(node, bottomRegion);
|
|
||||||
UpdateRegionEdge(bottomRegion, node);
|
|
||||||
return bottomRegion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't join an existing region then we'll make our own
|
|
||||||
var newRegion = new PathfindingRegion(node, new HashSet<PathfindingNode> {node}, node.AccessReaders.Count > 0);
|
|
||||||
_regions[parentChunk.GridId][parentChunk].Add(newRegion);
|
|
||||||
existingRegions.Add(node, newRegion);
|
|
||||||
UpdateRegionEdge(newRegion, node);
|
|
||||||
return newRegion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Combines the two regions into one bigger region
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source"></param>
|
|
||||||
/// <param name="target"></param>
|
|
||||||
private void MergeInto(PathfindingRegion source, PathfindingRegion target, Dictionary<PathfindingNode, PathfindingRegion>? existingRegions = null)
|
|
||||||
{
|
|
||||||
DebugTools.AssertNotNull(source);
|
|
||||||
DebugTools.AssertNotNull(target);
|
|
||||||
DebugTools.Assert(source != target);
|
|
||||||
foreach (var node in source.Nodes)
|
|
||||||
{
|
|
||||||
target.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingRegions != null)
|
|
||||||
{
|
|
||||||
foreach (var node in source.Nodes)
|
|
||||||
{
|
|
||||||
existingRegions[node] = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
source.Shutdown();
|
|
||||||
// This doesn't check the cachedaccessible to see if it's reachable but maybe it should?
|
|
||||||
// Although currently merge gets spammed so maybe when some other stuff is improved
|
|
||||||
// MergeInto is also only called by GenerateRegions currently so nothing should hold onto the original region
|
|
||||||
_regions[source.ParentChunk.GridId][source.ParentChunk].Remove(source);
|
|
||||||
|
|
||||||
foreach (var node in target.Nodes)
|
|
||||||
{
|
|
||||||
UpdateRegionEdge(target, node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove the cached accessibility lookup for this region
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="region"></param>
|
|
||||||
private void ClearCache(PathfindingRegion region)
|
|
||||||
{
|
|
||||||
DebugTools.Assert(region.Deleted);
|
|
||||||
|
|
||||||
// Need to forcibly clear cache for ourself and anything that includes us
|
|
||||||
foreach (var (_, cachedRegions) in _cachedAccessible)
|
|
||||||
{
|
|
||||||
if (cachedRegions.ContainsKey(region))
|
|
||||||
{
|
|
||||||
cachedRegions.Remove(region);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seemed like the safest way to remove this
|
|
||||||
// We could just have GetVisionAccessible remove us if it can tell we're deleted but that
|
|
||||||
// seems like it could be unreliable
|
|
||||||
var regionsToClear = new List<PathfindingRegion>();
|
|
||||||
|
|
||||||
foreach (var (otherRegion, cache) in cachedRegions)
|
|
||||||
{
|
|
||||||
if (cache.Regions.Contains(region))
|
|
||||||
{
|
|
||||||
regionsToClear.Add(otherRegion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var otherRegion in regionsToClear)
|
|
||||||
{
|
|
||||||
cachedRegions.Remove(otherRegion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
if (_regions.TryGetValue(region.ParentChunk.GridId, out var chunks) &&
|
|
||||||
chunks.TryGetValue(region.ParentChunk, out var regions))
|
|
||||||
{
|
|
||||||
DebugTools.Assert(!regions.Contains(region));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate all of the regions within a chunk
|
|
||||||
/// </summary>
|
|
||||||
/// These can't across over into another chunk and doors are their own region
|
|
||||||
/// <param name="chunk"></param>
|
|
||||||
private void GenerateRegions(PathfindingChunk chunk)
|
|
||||||
{
|
|
||||||
// Grid deleted while update queued, or invalid grid.
|
|
||||||
if (!_mapManager.TryGetGrid(chunk.GridId, out _))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_regions.ContainsKey(chunk.GridId))
|
|
||||||
{
|
|
||||||
_regions.Add(chunk.GridId, new Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_regions[chunk.GridId].TryGetValue(chunk, out var regions))
|
|
||||||
{
|
|
||||||
foreach (var region in regions)
|
|
||||||
{
|
|
||||||
_queuedCacheDeletions.Add(region);
|
|
||||||
region.Shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
_regions[chunk.GridId].Remove(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily store the corresponding region for each node
|
|
||||||
// Makes merging regions or adding nodes to existing regions neater.
|
|
||||||
var nodeRegions = new Dictionary<PathfindingNode, PathfindingRegion>();
|
|
||||||
var chunkRegions = new HashSet<PathfindingRegion>();
|
|
||||||
_regions[chunk.GridId].Add(chunk, chunkRegions);
|
|
||||||
|
|
||||||
for (var y = 0; y < PathfindingChunk.ChunkSize; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < PathfindingChunk.ChunkSize; x++)
|
|
||||||
{
|
|
||||||
var node = chunk.Nodes[x, y];
|
|
||||||
var region = CalculateNode(node, nodeRegions, chunkRegions, x, y);
|
|
||||||
// Currently we won't store a separate region for each mask / space / whatever because muh effort
|
|
||||||
// Long-term you'll want to account for it probably
|
|
||||||
if (region == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkRegions.Add(region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if DEBUG
|
|
||||||
foreach (var region in chunkRegions)
|
|
||||||
{
|
|
||||||
DebugTools.Assert(!region.Deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.Assert(chunkRegions.Count < Math.Pow(PathfindingChunk.ChunkSize, 2));
|
|
||||||
SendRegionsDebugMessage(chunk.GridId);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset(RoundRestartCleanupEvent ev)
|
|
||||||
{
|
|
||||||
_queuedUpdates.Clear();
|
|
||||||
_regions.Clear();
|
|
||||||
_cachedAccessible.Clear();
|
|
||||||
_queuedCacheDeletions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private void SendRegionsDebugMessage(EntityUid gridId)
|
|
||||||
{
|
|
||||||
if (_subscribedSessions.Count == 0) return;
|
|
||||||
var grid = _mapManager.GetGrid(gridId);
|
|
||||||
// Chunk / Regions / Nodes
|
|
||||||
var debugResult = new Dictionary<int, Dictionary<int, List<Vector2>>>();
|
|
||||||
var chunkIdx = 0;
|
|
||||||
var regionIdx = 0;
|
|
||||||
|
|
||||||
if (!_regions.TryGetValue(gridId, out var dict))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (_, regions) in dict)
|
|
||||||
{
|
|
||||||
var debugRegions = new Dictionary<int, List<Vector2>>();
|
|
||||||
debugResult.Add(chunkIdx, debugRegions);
|
|
||||||
|
|
||||||
foreach (var region in regions)
|
|
||||||
{
|
|
||||||
var debugRegionNodes = new List<Vector2>(region.Nodes.Count);
|
|
||||||
debugResult[chunkIdx].Add(regionIdx, debugRegionNodes);
|
|
||||||
|
|
||||||
foreach (var node in region.Nodes)
|
|
||||||
{
|
|
||||||
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
|
|
||||||
debugRegionNodes.Add(nodeVector);
|
|
||||||
}
|
|
||||||
|
|
||||||
regionIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var session in _subscribedSessions)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new SharedAiDebug.ReachableChunkRegionsDebugMessage(gridId, debugResult), session.ConnectedClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sent whenever the reachable cache for a particular mob is built or retrieved
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="gridId"></param>
|
|
||||||
/// <param name="regions"></param>
|
|
||||||
/// <param name="cached"></param>
|
|
||||||
private void SendRegionCacheMessage(EntityUid gridId, IEnumerable<PathfindingRegion> regions, bool cached)
|
|
||||||
{
|
|
||||||
if (_subscribedSessions.Count == 0) return;
|
|
||||||
|
|
||||||
var grid = _mapManager.GetGrid(gridId);
|
|
||||||
var debugResult = new Dictionary<int, List<Vector2>>();
|
|
||||||
|
|
||||||
foreach (var region in regions)
|
|
||||||
{
|
|
||||||
debugResult.Add(_runningCacheIdx, new List<Vector2>());
|
|
||||||
|
|
||||||
foreach (var node in region.Nodes)
|
|
||||||
{
|
|
||||||
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
|
|
||||||
|
|
||||||
debugResult[_runningCacheIdx].Add(nodeVector);
|
|
||||||
}
|
|
||||||
|
|
||||||
_runningCacheIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var session in _subscribedSessions)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached), session.ConnectedClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The simplest pathfinder
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BFSPathfinder
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all of the tiles in range that can we access
|
|
||||||
/// </summary>
|
|
||||||
/// If you want Dijkstra then add distances.
|
|
||||||
/// Doesn't use the JobQueue as it will generally be encapsulated by other jobs
|
|
||||||
/// <param name="pathfindingArgs"></param>
|
|
||||||
/// <param name="range"></param>
|
|
||||||
/// <param name="fromStart">Whether we traverse from the starting tile or the end tile</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static IEnumerable<PathfindingNode> GetNodesInRange(PathfindingArgs pathfindingArgs, bool fromStart = true)
|
|
||||||
{
|
|
||||||
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
|
|
||||||
// Don't need a priority queue given not looking for shortest path
|
|
||||||
var openTiles = new Queue<PathfindingNode>();
|
|
||||||
var closedTiles = new HashSet<TileRef>();
|
|
||||||
PathfindingNode startNode;
|
|
||||||
|
|
||||||
if (fromStart)
|
|
||||||
{
|
|
||||||
startNode = pathfindingSystem.GetNode(pathfindingArgs.Start);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
startNode = pathfindingSystem.GetNode(pathfindingArgs.End);
|
|
||||||
}
|
|
||||||
|
|
||||||
PathfindingNode currentNode;
|
|
||||||
openTiles.Enqueue(startNode);
|
|
||||||
|
|
||||||
while (openTiles.Count > 0)
|
|
||||||
{
|
|
||||||
currentNode = openTiles.Dequeue();
|
|
||||||
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
// No distances stored so can just check closed tiles here
|
|
||||||
if (closedTiles.Contains(neighbor.TileRef)) continue;
|
|
||||||
closedTiles.Add(currentNode.TileRef);
|
|
||||||
|
|
||||||
// So currently tileCost gets the octile distance between the 2 so we'll also use that for our range check
|
|
||||||
var tileCost = PathfindingHelpers.GetTileCost(pathfindingArgs, startNode, neighbor);
|
|
||||||
var direction = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
|
|
||||||
if (tileCost == null ||
|
|
||||||
tileCost > pathfindingArgs.Proximity ||
|
|
||||||
!PathfindingHelpers.DirectionTraversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, currentNode, direction))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
openTiles.Enqueue(neighbor);
|
|
||||||
yield return neighbor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A group of homogenous PathfindingNodes inside a single chunk
|
|
||||||
/// </summary>
|
|
||||||
/// Makes the graph smaller and quicker to traverse
|
|
||||||
public sealed class PathfindingRegion : IEquatable<PathfindingRegion>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Bottom-left reference node of the region
|
|
||||||
/// </summary>
|
|
||||||
public PathfindingNode OriginNode { get; }
|
|
||||||
|
|
||||||
// The shape may be anything within the bounds of a chunk, this is just a quick way to do a bounds-check
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum width of the nodes
|
|
||||||
/// </summary>
|
|
||||||
public int Height { get; private set; } = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum width of the nodes
|
|
||||||
/// </summary>
|
|
||||||
public int Width { get; private set; } = 1;
|
|
||||||
|
|
||||||
public PathfindingChunk ParentChunk => OriginNode.ParentChunk;
|
|
||||||
public HashSet<PathfindingRegion> Neighbors { get; } = new();
|
|
||||||
|
|
||||||
public bool IsDoor { get; }
|
|
||||||
public HashSet<PathfindingNode> Nodes => _nodes;
|
|
||||||
private readonly HashSet<PathfindingNode> _nodes;
|
|
||||||
|
|
||||||
public bool Deleted { get; private set; }
|
|
||||||
|
|
||||||
public PathfindingRegion(PathfindingNode originNode, HashSet<PathfindingNode> nodes, bool isDoor = false)
|
|
||||||
{
|
|
||||||
OriginNode = originNode;
|
|
||||||
_nodes = nodes;
|
|
||||||
IsDoor = isDoor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Shutdown()
|
|
||||||
{
|
|
||||||
// Tell our neighbors we no longer exist ;-/
|
|
||||||
var neighbors = new List<PathfindingRegion>(Neighbors);
|
|
||||||
|
|
||||||
for (var i = 0; i < neighbors.Count; i++)
|
|
||||||
{
|
|
||||||
var neighbor = neighbors[i];
|
|
||||||
neighbor.Neighbors.Remove(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_nodes.Clear();
|
|
||||||
Neighbors.Clear();
|
|
||||||
|
|
||||||
Deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Roughly how far away another region is by nearest node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="otherRegion"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public float Distance(PathfindingRegion otherRegion)
|
|
||||||
{
|
|
||||||
// JANK
|
|
||||||
var xDistance = otherRegion.OriginNode.TileRef.X - OriginNode.TileRef.X;
|
|
||||||
var yDistance = otherRegion.OriginNode.TileRef.Y - OriginNode.TileRef.Y;
|
|
||||||
|
|
||||||
if (xDistance > 0)
|
|
||||||
{
|
|
||||||
xDistance -= Width;
|
|
||||||
}
|
|
||||||
else if (xDistance < 0)
|
|
||||||
{
|
|
||||||
xDistance = Math.Abs(xDistance + otherRegion.Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yDistance > 0)
|
|
||||||
{
|
|
||||||
yDistance -= Height;
|
|
||||||
}
|
|
||||||
else if (yDistance < 0)
|
|
||||||
{
|
|
||||||
yDistance = Math.Abs(yDistance + otherRegion.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PathfindingHelpers.OctileDistance(xDistance, yDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can the given args can traverse this region?
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reachableArgs"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool RegionTraversable(ReachableArgs reachableArgs)
|
|
||||||
{
|
|
||||||
// The assumption is that all nodes in a region have the same pathfinding traits
|
|
||||||
// As such we can just use the origin node for checking.
|
|
||||||
return PathfindingHelpers.Traversable(reachableArgs.CollisionMask, reachableArgs.Access,
|
|
||||||
OriginNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(PathfindingNode node)
|
|
||||||
{
|
|
||||||
var xWidth = Math.Abs(node.TileRef.X - OriginNode.TileRef.X);
|
|
||||||
var yHeight = Math.Abs(node.TileRef.Y - OriginNode.TileRef.Y);
|
|
||||||
|
|
||||||
if (xWidth > Width)
|
|
||||||
{
|
|
||||||
Width = xWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yHeight > Height)
|
|
||||||
{
|
|
||||||
Height = yHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
_nodes.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashSet wasn't working correctly so uhh we got this.
|
|
||||||
public bool Equals(PathfindingRegion? other)
|
|
||||||
{
|
|
||||||
if (other == null) return false;
|
|
||||||
if (ReferenceEquals(this, other)) return true;
|
|
||||||
if (_nodes.Count != other.Nodes.Count) return false;
|
|
||||||
if (Deleted != other.Deleted) return false;
|
|
||||||
if (OriginNode != other.OriginNode) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return OriginNode.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Content.Shared.Access.Systems;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Accessible
|
|
||||||
{
|
|
||||||
public sealed class ReachableArgs
|
|
||||||
{
|
|
||||||
public float VisionRadius { get; set; }
|
|
||||||
public ICollection<string> Access { get; }
|
|
||||||
public int CollisionMask { get; }
|
|
||||||
|
|
||||||
public ReachableArgs(float visionRadius, ICollection<string> access, int collisionMask)
|
|
||||||
{
|
|
||||||
VisionRadius = visionRadius;
|
|
||||||
Access = access;
|
|
||||||
CollisionMask = collisionMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get appropriate args for a particular entity
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static ReachableArgs GetArgs(EntityUid entity, float radius)
|
|
||||||
{
|
|
||||||
var collisionMask = 0;
|
|
||||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
|
||||||
if (entMan.TryGetComponent(entity, out IPhysBody? physics))
|
|
||||||
{
|
|
||||||
collisionMask = physics.CollisionMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
|
||||||
var access = accessSystem.FindAccessTags(entity);
|
|
||||||
|
|
||||||
return new ReachableArgs(radius, access, collisionMask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs
Normal file
33
Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed class GridPathfindingChunk
|
||||||
|
{
|
||||||
|
// TODO: Make this a 1d array
|
||||||
|
public readonly PathfindingBreadcrumb[,] Points = new PathfindingBreadcrumb[
|
||||||
|
(SharedPathfindingSystem.ChunkSize) * SharedPathfindingSystem.SubStep,
|
||||||
|
(SharedPathfindingSystem.ChunkSize) * SharedPathfindingSystem.SubStep];
|
||||||
|
|
||||||
|
public Vector2i Origin;
|
||||||
|
|
||||||
|
public readonly List<PathPoly>[] Polygons = new List<PathPoly>[SharedPathfindingSystem.ChunkSize * SharedPathfindingSystem.ChunkSize];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The relevant polygon for this chunk's portals
|
||||||
|
/// </summary>
|
||||||
|
public readonly Dictionary<PathPortal, PathPoly> PortalPolys = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This chunk's portals.
|
||||||
|
/// </summary>
|
||||||
|
public readonly List<PathPortal> Portals = new();
|
||||||
|
|
||||||
|
public GridPathfindingChunk()
|
||||||
|
{
|
||||||
|
for (var x = 0; x < Polygons.Length; x++)
|
||||||
|
{
|
||||||
|
Polygons[x] = new List<PathPoly>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
namespace Content.Server.NPC.Pathfinding;
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
[RegisterComponent]
|
/// <summary>
|
||||||
[Access(typeof(PathfindingSystem))]
|
/// Stores the relevant pathfinding data for grids.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Access(typeof(PathfindingSystem))]
|
||||||
public sealed class GridPathfindingComponent : Component
|
public sealed class GridPathfindingComponent : Component
|
||||||
{
|
{
|
||||||
public readonly Dictionary<Vector2i, PathfindingChunk> Graph = new();
|
public readonly HashSet<Vector2i> DirtyChunks = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Next time the graph is allowed to update.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan NextUpdate;
|
||||||
|
|
||||||
|
public readonly Dictionary<Vector2i, GridPathfindingChunk> Chunks = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the chunk where the specified portal is stored on this grid.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Dictionary<PathPortal, Vector2i> PortalLookup = new();
|
||||||
|
|
||||||
|
public readonly List<PathPortal> DirtyPortals = new();
|
||||||
}
|
}
|
||||||
|
|||||||
22
Content.Server/NPC/Pathfinding/PathFlags.cs
Normal file
22
Content.Server/NPC/Pathfinding/PathFlags.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum PathFlags : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do we have any form of access.
|
||||||
|
/// </summary>
|
||||||
|
Access = 1 << 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can we pry airlocks if necessary.
|
||||||
|
/// </summary>
|
||||||
|
Prying = 1 << 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can stuff like walls be broken.
|
||||||
|
/// </summary>
|
||||||
|
Smashing = 1 << 2,
|
||||||
|
}
|
||||||
64
Content.Server/NPC/Pathfinding/PathPoly.cs
Normal file
64
Content.Server/NPC/Pathfinding/PathPoly.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed class PathPoly : IEquatable<PathPoly>
|
||||||
|
{
|
||||||
|
public readonly EntityUid GraphUid;
|
||||||
|
public readonly Vector2i ChunkOrigin;
|
||||||
|
public readonly byte TileIndex;
|
||||||
|
|
||||||
|
public readonly Box2 Box;
|
||||||
|
public PathfindingData Data;
|
||||||
|
|
||||||
|
public readonly HashSet<PathPoly> Neighbors;
|
||||||
|
|
||||||
|
public PathPoly(EntityUid graphUid, Vector2i chunkOrigin, byte tileIndex, Box2 vertices, PathfindingData data, HashSet<PathPoly> neighbors)
|
||||||
|
{
|
||||||
|
GraphUid = graphUid;
|
||||||
|
ChunkOrigin = chunkOrigin;
|
||||||
|
TileIndex = tileIndex;
|
||||||
|
Box = vertices;
|
||||||
|
Data = data;
|
||||||
|
Neighbors = neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid()
|
||||||
|
{
|
||||||
|
return (Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityCoordinates Coordinates => new(GraphUid, Box.Center);
|
||||||
|
|
||||||
|
// Explicitly don't check neighbors.
|
||||||
|
|
||||||
|
public bool IsEquivalent(PathPoly other)
|
||||||
|
{
|
||||||
|
return GraphUid.Equals(other.GraphUid) &&
|
||||||
|
ChunkOrigin.Equals(other.ChunkOrigin) &&
|
||||||
|
TileIndex == other.TileIndex &&
|
||||||
|
Data.IsEquivalent(other.Data) &&
|
||||||
|
Box.Equals(other.Box);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(PathPoly? other)
|
||||||
|
{
|
||||||
|
return other != null &&
|
||||||
|
GraphUid.Equals(other.GraphUid) &&
|
||||||
|
ChunkOrigin.Equals(other.ChunkOrigin) &&
|
||||||
|
TileIndex == other.TileIndex &&
|
||||||
|
Data.Equals(other.Data) &&
|
||||||
|
Box.Equals(other.Box);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return ReferenceEquals(this, obj) || obj is PathPoly other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(GraphUid, ChunkOrigin, TileIndex, Box);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Content.Server/NPC/Pathfinding/PathPortal.cs
Normal file
30
Content.Server/NPC/Pathfinding/PathPortal.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects 2 disparate locations.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For example, 2 docking airlocks connecting 2 graphs, or an actual portal on the same graph.
|
||||||
|
/// </remarks>
|
||||||
|
public struct PathPortal
|
||||||
|
{
|
||||||
|
// Assume for now it's 2-way and code 1-ways later.
|
||||||
|
public readonly int Handle;
|
||||||
|
public readonly EntityCoordinates CoordinatesA;
|
||||||
|
public readonly EntityCoordinates CoordinatesB;
|
||||||
|
|
||||||
|
// TODO: Whenever the chunk rebuilds need to add a neighbor.
|
||||||
|
public PathPortal(int handle, EntityCoordinates coordsA, EntityCoordinates coordsB)
|
||||||
|
{
|
||||||
|
Handle = handle;
|
||||||
|
CoordinatesA = coordsA;
|
||||||
|
CoordinatesB = coordsB;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return Handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
Content.Server/NPC/Pathfinding/PathRequest.cs
Normal file
110
Content.Server/NPC/Pathfinding/PathRequest.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the in-progress data of a pathfinding request.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class PathRequest
|
||||||
|
{
|
||||||
|
public EntityCoordinates Start;
|
||||||
|
|
||||||
|
public Task<PathResult> Task => Tcs.Task;
|
||||||
|
public readonly TaskCompletionSource<PathResult> Tcs;
|
||||||
|
|
||||||
|
public Queue<PathPoly> Polys = new();
|
||||||
|
|
||||||
|
public bool Started = false;
|
||||||
|
|
||||||
|
#region Pathfinding state
|
||||||
|
|
||||||
|
public readonly Stopwatch Stopwatch = new();
|
||||||
|
public PriorityQueue<ValueTuple<float, PathPoly>> Frontier = default!;
|
||||||
|
public readonly Dictionary<PathPoly, float> CostSoFar = new();
|
||||||
|
public readonly Dictionary<PathPoly, PathPoly> CameFrom = new();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Data
|
||||||
|
|
||||||
|
public readonly PathFlags Flags;
|
||||||
|
public readonly float Range;
|
||||||
|
public readonly int CollisionLayer;
|
||||||
|
public readonly int CollisionMask;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public PathRequest(EntityCoordinates start, PathFlags flags, float range, int layer, int mask, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
Start = start;
|
||||||
|
Flags = flags;
|
||||||
|
Range = range;
|
||||||
|
CollisionLayer = layer;
|
||||||
|
CollisionMask = mask;
|
||||||
|
Tcs = new TaskCompletionSource<PathResult>(cancelToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AStarPathRequest : PathRequest
|
||||||
|
{
|
||||||
|
public EntityCoordinates End;
|
||||||
|
|
||||||
|
public AStarPathRequest(
|
||||||
|
EntityCoordinates start,
|
||||||
|
EntityCoordinates end,
|
||||||
|
PathFlags flags,
|
||||||
|
float range,
|
||||||
|
int layer,
|
||||||
|
int mask,
|
||||||
|
CancellationToken cancelToken) : base(start, flags, range, layer, mask, cancelToken)
|
||||||
|
{
|
||||||
|
End = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BFSPathRequest : PathRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How far away we're allowed to expand in distance.
|
||||||
|
/// </summary>
|
||||||
|
public float ExpansionRange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many nodes we're allowed to expand
|
||||||
|
/// </summary>
|
||||||
|
public int ExpansionLimit;
|
||||||
|
|
||||||
|
public BFSPathRequest(
|
||||||
|
float expansionRange,
|
||||||
|
int expansionLimit,
|
||||||
|
EntityCoordinates start,
|
||||||
|
PathFlags flags,
|
||||||
|
float range,
|
||||||
|
int layer,
|
||||||
|
int mask,
|
||||||
|
CancellationToken cancelToken) : base(start, flags, range, layer, mask, cancelToken)
|
||||||
|
{
|
||||||
|
ExpansionRange = expansionRange;
|
||||||
|
ExpansionLimit = expansionLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the final result of a pathfinding request
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PathResultEvent
|
||||||
|
{
|
||||||
|
public PathResult Result;
|
||||||
|
public readonly Queue<PathPoly> Path;
|
||||||
|
|
||||||
|
public PathResultEvent(PathResult result, Queue<PathPoly> path)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Content.Server/NPC/Pathfinding/PathResult.cs
Normal file
9
Content.Server/NPC/Pathfinding/PathResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public enum PathResult : byte
|
||||||
|
{
|
||||||
|
NoPath,
|
||||||
|
PartialPath,
|
||||||
|
Path,
|
||||||
|
Continuing,
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.CPUJob.JobQueues;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
|
||||||
{
|
|
||||||
public sealed class AStarPathfindingJob : Job<Queue<TileRef>>
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
public static event Action<SharedAiDebug.AStarRouteDebug>? DebugRoute;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private readonly PathfindingNode _startNode;
|
|
||||||
private PathfindingNode _endNode;
|
|
||||||
private readonly PathfindingArgs _pathfindingArgs;
|
|
||||||
private readonly IEntityManager _entityManager;
|
|
||||||
|
|
||||||
public AStarPathfindingJob(
|
|
||||||
double maxTime,
|
|
||||||
PathfindingNode startNode,
|
|
||||||
PathfindingNode endNode,
|
|
||||||
PathfindingArgs pathfindingArgs,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
IEntityManager entityManager) : base(maxTime, cancellationToken)
|
|
||||||
{
|
|
||||||
_startNode = startNode;
|
|
||||||
_endNode = endNode;
|
|
||||||
_pathfindingArgs = pathfindingArgs;
|
|
||||||
_entityManager = entityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<Queue<TileRef>?> Process()
|
|
||||||
{
|
|
||||||
if (_startNode.TileRef.Equals(TileRef.Zero) ||
|
|
||||||
_endNode.TileRef.Equals(TileRef.Zero) ||
|
|
||||||
Status == JobStatus.Finished)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we couldn't get a nearby node that's good enough
|
|
||||||
if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entityManager.Deleted(_pathfindingArgs.Start.GridUid))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var frontier = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
|
|
||||||
var costSoFar = new Dictionary<PathfindingNode, float>();
|
|
||||||
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
|
|
||||||
|
|
||||||
PathfindingNode? currentNode = null;
|
|
||||||
frontier.Add((0.0f, _startNode));
|
|
||||||
costSoFar[_startNode] = 0.0f;
|
|
||||||
var routeFound = false;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
while (frontier.Count > 0)
|
|
||||||
{
|
|
||||||
// Handle whether we need to pause if we've taken too long
|
|
||||||
count++;
|
|
||||||
if (count % 20 == 0 && count > 0)
|
|
||||||
{
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
|
|
||||||
if (_startNode == null || _endNode == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actual pathfinding here
|
|
||||||
(_, currentNode) = frontier.Take();
|
|
||||||
if (currentNode.Equals(_endNode))
|
|
||||||
{
|
|
||||||
routeFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var nextNode in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
// If tile is untraversable it'll be null
|
|
||||||
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode);
|
|
||||||
if (tileCost == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// So if we're going NE then that means either N or E needs to be free to actually get there
|
|
||||||
var direction = PathfindingHelpers.RelativeDirection(nextNode, currentNode);
|
|
||||||
if (!PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// f = g + h
|
|
||||||
// gScore is distance to the start node
|
|
||||||
// hScore is distance to the end node
|
|
||||||
var gScore = costSoFar[currentNode] + tileCost.Value;
|
|
||||||
if (costSoFar.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cameFrom[nextNode] = currentNode;
|
|
||||||
costSoFar[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
|
|
||||||
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
|
||||||
// (i.e. somewhere between 1 and infinite)
|
|
||||||
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
|
||||||
var fScore = gScore + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f);
|
|
||||||
frontier.Add((fScore, nextNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!routeFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.AssertNotNull(currentNode);
|
|
||||||
|
|
||||||
var route = PathfindingHelpers.ReconstructPath(cameFrom, currentNode!);
|
|
||||||
|
|
||||||
if (route.Count == 1)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var simplifiedRoute = PathfindingSystem.Simplify(route, 0f);
|
|
||||||
var actualRoute = new Queue<TileRef>(simplifiedRoute);
|
|
||||||
|
|
||||||
#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>(costSoFar.Count);
|
|
||||||
foreach (var (node, parent) in cameFrom)
|
|
||||||
{
|
|
||||||
debugCameFrom.Add(node.TileRef, parent.TileRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (node, score) in costSoFar)
|
|
||||||
{
|
|
||||||
debugGScores.Add(node.TileRef, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
var debugRoute = new SharedAiDebug.AStarRouteDebug(
|
|
||||||
_pathfindingArgs.Uid,
|
|
||||||
actualRoute,
|
|
||||||
debugCameFrom,
|
|
||||||
debugGScores,
|
|
||||||
DebugTime);
|
|
||||||
|
|
||||||
DebugRoute.Invoke(debugRoute);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return actualRoute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.CPUJob.JobQueues;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
|
||||||
{
|
|
||||||
public sealed class JpsPathfindingJob : Job<Queue<TileRef>>
|
|
||||||
{
|
|
||||||
// Some of this is probably fugly due to other structural changes in pathfinding so it could do with optimisation
|
|
||||||
// Realistically it's probably not getting used given it doesn't support tile costs which can be very useful
|
|
||||||
#if DEBUG
|
|
||||||
public static event Action<SharedAiDebug.JpsRouteDebug>? DebugRoute;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private readonly PathfindingNode? _startNode;
|
|
||||||
private PathfindingNode? _endNode;
|
|
||||||
private readonly 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 (!PathfindingHelpers.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 node in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var direction = PathfindingHelpers.RelativeDirection(node, currentNode);
|
|
||||||
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 = PathfindingHelpers.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] + PathfindingHelpers.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f);
|
|
||||||
openTiles.Add((fScore, jumpNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!routeFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.AssertNotNull(currentNode);
|
|
||||||
|
|
||||||
var route = PathfindingHelpers.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++;
|
|
||||||
PathfindingNode? nextNode = null;
|
|
||||||
foreach (var node in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (PathfindingHelpers.RelativeDirection(node, currentNode) == direction)
|
|
||||||
{
|
|
||||||
nextNode = node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll do opposite DirectionTraversable just because of how the method's setup
|
|
||||||
// Nodes should be 2-way anyway.
|
|
||||||
if (nextNode == null ||
|
|
||||||
PathfindingHelpers.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 tried just casting direction ints and offsets to make it smaller but brain no worky.
|
|
||||||
// From NorthEast we check (Closed / Open) S - SE, W - NW
|
|
||||||
|
|
||||||
PathfindingNode? openNeighborOne = null;
|
|
||||||
PathfindingNode? closedNeighborOne = null;
|
|
||||||
PathfindingNode? openNeighborTwo = null;
|
|
||||||
PathfindingNode? closedNeighborTwo = null;
|
|
||||||
|
|
||||||
switch (direction)
|
|
||||||
{
|
|
||||||
case Direction.NorthEast:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.SouthEast:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.South:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.NorthWest:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.West:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.SouthEast:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.NorthEast:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.North:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.West:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.NorthWest:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.North:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.SouthEast:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.East:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.NorthWest:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.SouthWest:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.South:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.NorthEast:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.East:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((closedNeighborOne == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null)
|
|
||||||
&& openNeighborOne != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((closedNeighborTwo == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null)
|
|
||||||
&& openNeighborTwo != null && PathfindingHelpers.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 = null;
|
|
||||||
PathfindingNode? closedNeighborOne = null;
|
|
||||||
PathfindingNode? openNeighborTwo = null;
|
|
||||||
PathfindingNode? closedNeighborTwo = null;
|
|
||||||
|
|
||||||
switch (direction)
|
|
||||||
{
|
|
||||||
case Direction.North:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.NorthEast:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.East:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.NorthWest:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.West:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.East:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.NorthEast:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.North:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.SouthEast:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.South:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.South:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.SouthEast:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.East:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.West:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.West:
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
|
|
||||||
switch (neighborDirection)
|
|
||||||
{
|
|
||||||
case Direction.NorthWest:
|
|
||||||
openNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.North:
|
|
||||||
closedNeighborOne = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
openNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
case Direction.South:
|
|
||||||
closedNeighborTwo = neighbor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((closedNeighborOne == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborOne)) &&
|
|
||||||
openNeighborOne != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborOne))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((closedNeighborTwo == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborTwo)) &&
|
|
||||||
openNeighborTwo != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborTwo))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
|
||||||
{
|
|
||||||
public struct PathfindingArgs
|
|
||||||
{
|
|
||||||
public EntityUid Uid { get; }
|
|
||||||
public ICollection<string> Access { 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,
|
|
||||||
ICollection<string> access,
|
|
||||||
int collisionMask,
|
|
||||||
TileRef start,
|
|
||||||
TileRef end,
|
|
||||||
float proximity = 0.0f,
|
|
||||||
bool allowDiagonals = true,
|
|
||||||
bool noClip = false,
|
|
||||||
bool allowSpace = false)
|
|
||||||
{
|
|
||||||
Uid = entityUid;
|
|
||||||
Access = access;
|
|
||||||
CollisionMask = collisionMask;
|
|
||||||
Start = start;
|
|
||||||
End = end;
|
|
||||||
Proximity = proximity;
|
|
||||||
AllowDiagonals = allowDiagonals;
|
|
||||||
NoClip = noClip;
|
|
||||||
AllowSpace = allowSpace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Content.Server.NPC.Pathfinding.Pathfinders
|
|
||||||
{
|
|
||||||
public sealed class PathfindingComparer : IComparer<ValueTuple<float, PathfindingNode>>
|
|
||||||
{
|
|
||||||
public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y)
|
|
||||||
{
|
|
||||||
return y.Item1.CompareTo(x.Item1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding
|
|
||||||
{
|
|
||||||
public sealed class PathfindingChunkUpdateMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public PathfindingChunk Chunk { get; }
|
|
||||||
|
|
||||||
public PathfindingChunkUpdateMessage(PathfindingChunk chunk)
|
|
||||||
{
|
|
||||||
Chunk = chunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PathfindingChunk
|
|
||||||
{
|
|
||||||
public TimeSpan LastUpdate { get; private set; }
|
|
||||||
public EntityUid GridId { get; }
|
|
||||||
|
|
||||||
public Vector2i Indices => _indices;
|
|
||||||
private readonly Vector2i _indices;
|
|
||||||
|
|
||||||
// Nodes per chunk row
|
|
||||||
public static int ChunkSize => 8;
|
|
||||||
public PathfindingNode[,] Nodes => _nodes;
|
|
||||||
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
|
|
||||||
|
|
||||||
public PathfindingChunk(EntityUid gridId, Vector2i indices)
|
|
||||||
{
|
|
||||||
GridId = gridId;
|
|
||||||
_indices = indices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize(IMapGrid mapGrid)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < ChunkSize; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < ChunkSize; y++)
|
|
||||||
{
|
|
||||||
var tileRef = mapGrid.GetTileRef(new Vector2i(x + _indices.X, y + _indices.Y));
|
|
||||||
CreateNode(tileRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Only called when blockers change (i.e. un-anchored physics objects don't trigger)
|
|
||||||
/// </summary>
|
|
||||||
public void Dirty()
|
|
||||||
{
|
|
||||||
LastUpdate = IoCManager.Resolve<IGameTiming>().CurTime;
|
|
||||||
IoCManager.Resolve<IEntityManager>().EventBus
|
|
||||||
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<PathfindingChunk> GetNeighbors(IEntityManager? entManager = null)
|
|
||||||
{
|
|
||||||
IoCManager.Resolve(ref entManager);
|
|
||||||
var chunkGrid = entManager.GetComponent<GridPathfindingComponent>(GridId).Graph;
|
|
||||||
|
|
||||||
for (var x = -1; x <= 1; x++)
|
|
||||||
{
|
|
||||||
for (var y = -1; y <= 1; y++)
|
|
||||||
{
|
|
||||||
if (x == 0 && y == 0) continue;
|
|
||||||
var (neighborX, neighborY) = (_indices.X + ChunkSize * x, _indices.Y + ChunkSize * y);
|
|
||||||
if (chunkGrid.TryGetValue(new Vector2i(neighborX, neighborY), out var neighbor))
|
|
||||||
{
|
|
||||||
yield return neighbor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool InBounds(Vector2i vector)
|
|
||||||
{
|
|
||||||
if (vector.X < _indices.X || vector.Y < _indices.Y) return false;
|
|
||||||
if (vector.X >= _indices.X + ChunkSize || vector.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets our neighbors that are relevant for the node to retrieve its own neighbors
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="node"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<PathfindingChunk> RelevantChunks(PathfindingNode node)
|
|
||||||
{
|
|
||||||
var relevantDirections = GetEdges(node).ToList();
|
|
||||||
|
|
||||||
foreach (var chunk in GetNeighbors())
|
|
||||||
{
|
|
||||||
var chunkDirection = PathfindingHelpers.RelativeDirection(chunk, this);
|
|
||||||
if (relevantDirections.Contains(chunkDirection))
|
|
||||||
{
|
|
||||||
yield return chunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Direction> GetEdges(PathfindingNode node)
|
|
||||||
{
|
|
||||||
// West Edge
|
|
||||||
if (node.TileRef.X == _indices.X)
|
|
||||||
{
|
|
||||||
yield return Direction.West;
|
|
||||||
if (node.TileRef.Y == _indices.Y)
|
|
||||||
{
|
|
||||||
yield return Direction.SouthWest;
|
|
||||||
yield return Direction.South;
|
|
||||||
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
|
||||||
{
|
|
||||||
yield return Direction.NorthWest;
|
|
||||||
yield return Direction.North;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
// East edge
|
|
||||||
if (node.TileRef.X == _indices.X + ChunkSize - 1)
|
|
||||||
{
|
|
||||||
yield return Direction.East;
|
|
||||||
if (node.TileRef.Y == _indices.Y)
|
|
||||||
{
|
|
||||||
yield return Direction.SouthEast;
|
|
||||||
yield return Direction.South;
|
|
||||||
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
|
||||||
{
|
|
||||||
yield return Direction.NorthEast;
|
|
||||||
yield return Direction.North;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
}
|
|
||||||
// South edge
|
|
||||||
if (node.TileRef.Y == _indices.Y)
|
|
||||||
{
|
|
||||||
yield return Direction.South;
|
|
||||||
// Given we already checked south-west and south-east above shouldn't need any more
|
|
||||||
}
|
|
||||||
// North edge
|
|
||||||
if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
|
|
||||||
{
|
|
||||||
yield return Direction.North;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PathfindingNode GetNode(TileRef tile)
|
|
||||||
{
|
|
||||||
var chunkX = tile.X - _indices.X;
|
|
||||||
var chunkY = tile.Y - _indices.Y;
|
|
||||||
|
|
||||||
return _nodes[chunkX, chunkY];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateNode(TileRef tile, PathfindingChunk? 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
using Content.Server.NPC.Pathfinding.Accessible;
|
|
||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Content.Shared.Access.Systems;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding
|
|
||||||
{
|
|
||||||
public static class PathfindingHelpers
|
|
||||||
{
|
|
||||||
public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs)
|
|
||||||
{
|
|
||||||
if (!Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, endNode))
|
|
||||||
{
|
|
||||||
if (pathfindingArgs.Proximity > 0.0f)
|
|
||||||
{
|
|
||||||
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
|
|
||||||
{
|
|
||||||
endNode = node;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool DirectionTraversable(int collisionMask, ICollection<string> access, PathfindingNode currentNode, Direction direction)
|
|
||||||
{
|
|
||||||
// If it's a diagonal we need to check NSEW to see if we can get to it and stop corner cutting, NE needs N and E etc.
|
|
||||||
// Given there's different collision layers stored for each node in the graph it's probably not worth it to cache this
|
|
||||||
// Also this will help with corner-cutting
|
|
||||||
|
|
||||||
PathfindingNode? northNeighbor = null;
|
|
||||||
PathfindingNode? southNeighbor = null;
|
|
||||||
PathfindingNode? eastNeighbor = null;
|
|
||||||
PathfindingNode? westNeighbor = null;
|
|
||||||
foreach (var neighbor in currentNode.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.TileRef.X == currentNode.TileRef.X &&
|
|
||||||
neighbor.TileRef.Y == currentNode.TileRef.Y + 1)
|
|
||||||
{
|
|
||||||
northNeighbor = neighbor;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (neighbor.TileRef.X == currentNode.TileRef.X + 1 &&
|
|
||||||
neighbor.TileRef.Y == currentNode.TileRef.Y)
|
|
||||||
{
|
|
||||||
eastNeighbor = neighbor;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (neighbor.TileRef.X == currentNode.TileRef.X &&
|
|
||||||
neighbor.TileRef.Y == currentNode.TileRef.Y - 1)
|
|
||||||
{
|
|
||||||
southNeighbor = neighbor;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (neighbor.TileRef.X == currentNode.TileRef.X - 1 &&
|
|
||||||
neighbor.TileRef.Y == currentNode.TileRef.Y)
|
|
||||||
{
|
|
||||||
westNeighbor = neighbor;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (direction)
|
|
||||||
{
|
|
||||||
case Direction.NorthEast:
|
|
||||||
if (northNeighbor == null || eastNeighbor == null) return false;
|
|
||||||
if (!Traversable(collisionMask, access, northNeighbor) ||
|
|
||||||
!Traversable(collisionMask, access, eastNeighbor))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.NorthWest:
|
|
||||||
if (northNeighbor == null || westNeighbor == null) return false;
|
|
||||||
if (!Traversable(collisionMask, access, northNeighbor) ||
|
|
||||||
!Traversable(collisionMask, access, westNeighbor))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
if (southNeighbor == null || westNeighbor == null) return false;
|
|
||||||
if (!Traversable(collisionMask, access, southNeighbor) ||
|
|
||||||
!Traversable(collisionMask, access, westNeighbor))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction.SouthEast:
|
|
||||||
if (southNeighbor == null || eastNeighbor == null) return false;
|
|
||||||
if (!Traversable(collisionMask, access, southNeighbor) ||
|
|
||||||
!Traversable(collisionMask, access, eastNeighbor))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Traversable(int collisionMask, ICollection<string> access, PathfindingNode node)
|
|
||||||
{
|
|
||||||
if ((collisionMask & node.BlockedCollisionMask) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
|
||||||
foreach (var reader in node.AccessReaders)
|
|
||||||
{
|
|
||||||
if (!accessSystem.IsAllowed(access, reader))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<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 List<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.GridUid);
|
|
||||||
|
|
||||||
// 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 Vector2i(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(int dstX, int dstY)
|
|
||||||
{
|
|
||||||
if (dstX > dstY)
|
|
||||||
{
|
|
||||||
return 1.4f * dstY + (dstX - dstY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1.4f * dstX + (dstY - dstX);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 OctileDistance(TileRef endTile, TileRef startTile)
|
|
||||||
{
|
|
||||||
// "Fast Euclidean" / octile.
|
|
||||||
// This implementation is written down in a few sources; it just saves doing sqrt.
|
|
||||||
int dstX = Math.Abs(startTile.X - endTile.X);
|
|
||||||
int dstY = Math.Abs(startTile.Y - endTile.Y);
|
|
||||||
if (dstX > dstY)
|
|
||||||
{
|
|
||||||
return 1.4f * dstY + (dstX - dstY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1.4f * dstX + (dstY - dstX);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode)
|
|
||||||
{
|
|
||||||
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, pathfindingArgs.Access, end))
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Direction RelativeDirection(PathfindingChunk endChunk, PathfindingChunk startChunk)
|
|
||||||
{
|
|
||||||
var xDiff = (endChunk.Indices.X - startChunk.Indices.X) / PathfindingChunk.ChunkSize;
|
|
||||||
var yDiff = (endChunk.Indices.Y - startChunk.Indices.Y) / PathfindingChunk.ChunkSize;
|
|
||||||
|
|
||||||
return RelativeDirection(xDiff, yDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Direction RelativeDirection(PathfindingNode endNode, PathfindingNode startNode)
|
|
||||||
{
|
|
||||||
var xDiff = endNode.TileRef.X - startNode.TileRef.X;
|
|
||||||
var yDiff = endNode.TileRef.Y - startNode.TileRef.Y;
|
|
||||||
|
|
||||||
return RelativeDirection(xDiff, yDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Direction RelativeDirection(int x, int y)
|
|
||||||
{
|
|
||||||
switch (x)
|
|
||||||
{
|
|
||||||
case -1:
|
|
||||||
switch (y)
|
|
||||||
{
|
|
||||||
case -1:
|
|
||||||
return Direction.SouthWest;
|
|
||||||
case 0:
|
|
||||||
return Direction.West;
|
|
||||||
case 1:
|
|
||||||
return Direction.NorthWest;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
case 0:
|
|
||||||
switch (y)
|
|
||||||
{
|
|
||||||
case -1:
|
|
||||||
return Direction.South;
|
|
||||||
case 0:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
case 1:
|
|
||||||
return Direction.North;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
switch (y)
|
|
||||||
{
|
|
||||||
case -1:
|
|
||||||
return Direction.SouthEast;
|
|
||||||
case 0:
|
|
||||||
return Direction.East;
|
|
||||||
case 1:
|
|
||||||
return Direction.NorthEast;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.Doors.Components;
|
|
||||||
using Content.Shared.Access.Components;
|
|
||||||
using Content.Shared.Doors.Components;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding
|
|
||||||
{
|
|
||||||
public sealed class PathfindingNode
|
|
||||||
{
|
|
||||||
public PathfindingChunk ParentChunk => _parentChunk;
|
|
||||||
private readonly PathfindingChunk _parentChunk;
|
|
||||||
|
|
||||||
public TileRef TileRef { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whenever there's a change in the collision layers we update the mask as the graph has more reads than writes
|
|
||||||
/// </summary>
|
|
||||||
public int BlockedCollisionMask { get; private set; }
|
|
||||||
private readonly Dictionary<EntityUid, int> _blockedCollidables = new(0);
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<EntityUid, int> PhysicsLayers => _physicsLayers;
|
|
||||||
private readonly Dictionary<EntityUid, int> _physicsLayers = new(0);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The entities on this tile that require access to traverse
|
|
||||||
/// </summary>
|
|
||||||
/// We don't store the ICollection, at least for now, as we'd need to replicate the access code here
|
|
||||||
public IReadOnlyCollection<AccessReaderComponent> AccessReaders => _accessReaders.Values;
|
|
||||||
private readonly Dictionary<EntityUid, AccessReaderComponent> _accessReaders = new(0);
|
|
||||||
|
|
||||||
public PathfindingNode(PathfindingChunk parent, TileRef tileRef)
|
|
||||||
{
|
|
||||||
_parentChunk = parent;
|
|
||||||
TileRef = tileRef;
|
|
||||||
GenerateMask();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return our neighboring nodes (even across chunks)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<PathfindingNode> GetNeighbors()
|
|
||||||
{
|
|
||||||
List<PathfindingChunk>? neighborChunks = null;
|
|
||||||
if (ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
neighborChunks = ParentChunk.RelevantChunks(this).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var x = -1; x <= 1; x++)
|
|
||||||
{
|
|
||||||
for (var y = -1; y <= 1; y++)
|
|
||||||
{
|
|
||||||
if (x == 0 && y == 0) continue;
|
|
||||||
var indices = new Vector2i(TileRef.X + x, TileRef.Y + y);
|
|
||||||
if (ParentChunk.InBounds(indices))
|
|
||||||
{
|
|
||||||
var (relativeX, relativeY) = (indices.X - ParentChunk.Indices.X,
|
|
||||||
indices.Y - ParentChunk.Indices.Y);
|
|
||||||
yield return ParentChunk.Nodes[relativeX, relativeY];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DebugTools.AssertNotNull(neighborChunks);
|
|
||||||
// Get the relevant chunk and then get the node on it
|
|
||||||
foreach (var neighbor in neighborChunks!)
|
|
||||||
{
|
|
||||||
// A lot of edge transitions are going to have a single neighboring chunk
|
|
||||||
// (given > 1 only affects corners)
|
|
||||||
// So we can just check the count to see if it's inbound
|
|
||||||
if (neighborChunks.Count > 0 && !neighbor.InBounds(indices)) continue;
|
|
||||||
var (relativeX, relativeY) = (indices.X - neighbor.Indices.X,
|
|
||||||
indices.Y - neighbor.Indices.Y);
|
|
||||||
yield return neighbor.Nodes[relativeX, relativeY];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PathfindingNode? GetNeighbor(Direction direction, IEntityManager? entManager = null)
|
|
||||||
{
|
|
||||||
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
|
|
||||||
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
|
|
||||||
Vector2i neighborVector2i;
|
|
||||||
|
|
||||||
switch (direction)
|
|
||||||
{
|
|
||||||
case Direction.East:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.NorthEast:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y + 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.North:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset, chunkYOffset + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y + 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.NorthWest:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y + 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.West:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.SouthWest:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y - 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.South:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset, chunkYOffset - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y - 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case Direction.SouthEast:
|
|
||||||
if (!ParentChunk.OnEdge(this))
|
|
||||||
{
|
|
||||||
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y - 1);
|
|
||||||
foreach (var neighbor in ParentChunk.GetNeighbors())
|
|
||||||
{
|
|
||||||
if (neighbor.InBounds(neighborVector2i))
|
|
||||||
{
|
|
||||||
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
|
|
||||||
neighborVector2i.Y - neighbor.Indices.Y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateTile(TileRef newTile)
|
|
||||||
{
|
|
||||||
TileRef = newTile;
|
|
||||||
ParentChunk.Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Call if this entity is relevant for the pathfinder
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
/// TODO: These 2 methods currently don't account for a bunch of changes (e.g. airlock unpowered, wrenching, etc.)
|
|
||||||
/// TODO: Could probably optimise this slightly more.
|
|
||||||
public void AddEntity(EntityUid entity, IPhysBody physicsComponent, IEntityManager? entMan = null)
|
|
||||||
{
|
|
||||||
IoCManager.Resolve(ref entMan);
|
|
||||||
// If we're a door
|
|
||||||
if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<DoorComponent>(entity))
|
|
||||||
{
|
|
||||||
// If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future)
|
|
||||||
// TODO: Check for powered I think (also need an event for when it's depowered
|
|
||||||
// AccessReader calls this whenever opening / closing but it can seem to get called multiple times
|
|
||||||
// Which may or may not be intended?
|
|
||||||
if (entMan.TryGetComponent(entity, out AccessReaderComponent? accessReader) && !_accessReaders.ContainsKey(entity))
|
|
||||||
{
|
|
||||||
_accessReaders.TryAdd(entity, accessReader);
|
|
||||||
ParentChunk.Dirty();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.Assert((PathfindingSystem.TrackedCollisionLayers & physicsComponent.CollisionLayer) != 0);
|
|
||||||
|
|
||||||
if (physicsComponent.BodyType != BodyType.Static ||
|
|
||||||
!physicsComponent.Hard)
|
|
||||||
{
|
|
||||||
_physicsLayers.TryAdd(entity, physicsComponent.CollisionLayer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_blockedCollidables.TryAdd(entity, physicsComponent.CollisionLayer);
|
|
||||||
GenerateMask();
|
|
||||||
ParentChunk.Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove the entity from this node.
|
|
||||||
/// Will check each category and remove it from the applicable one
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
public void RemoveEntity(EntityUid entity)
|
|
||||||
{
|
|
||||||
// There's no guarantee that the entity isn't deleted
|
|
||||||
// 90% of updates are probably entities moving around
|
|
||||||
// Entity can't be under multiple categories so just checking each once is fine.
|
|
||||||
if (_physicsLayers.Remove(entity))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_accessReaders.Remove(entity))
|
|
||||||
{
|
|
||||||
ParentChunk.Dirty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_blockedCollidables.Remove(entity))
|
|
||||||
{
|
|
||||||
GenerateMask();
|
|
||||||
ParentChunk.Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GenerateMask()
|
|
||||||
{
|
|
||||||
BlockedCollisionMask = 0x0;
|
|
||||||
|
|
||||||
foreach (var layer in _blockedCollidables.Values)
|
|
||||||
{
|
|
||||||
BlockedCollisionMask |= layer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
Content.Server/NPC/Pathfinding/PathfindingRequestEvent.cs
Normal file
11
Content.Server/NPC/Pathfinding/PathfindingRequestEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed class PathfindingRequestEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityCoordinates Start;
|
||||||
|
public EntityCoordinates End;
|
||||||
|
|
||||||
|
// TODO: Need stuff like can we break shit, can we pry, collision mask, etc
|
||||||
|
}
|
||||||
143
Content.Server/NPC/Pathfinding/PathfindingSystem.AStar.cs
Normal file
143
Content.Server/NPC/Pathfinding/PathfindingSystem.AStar.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
private PathResult UpdateAStarPath(AStarPathRequest request)
|
||||||
|
{
|
||||||
|
if (request.Start.Equals(request.End))
|
||||||
|
{
|
||||||
|
return PathResult.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Task.IsCanceled)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need partial planning that uses best node.
|
||||||
|
PathPoly? currentNode = null;
|
||||||
|
|
||||||
|
// First run
|
||||||
|
if (!request.Started)
|
||||||
|
{
|
||||||
|
request.Frontier = new PriorityQueue<(float, PathPoly)>(PathPolyComparer);
|
||||||
|
request.Started = true;
|
||||||
|
}
|
||||||
|
// Re-validate nodes
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(_, currentNode) = request.Frontier.Peek();
|
||||||
|
|
||||||
|
if (!currentNode.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-validate parents too.
|
||||||
|
if (request.CameFrom.TryGetValue(currentNode, out var parentNode) && !parentNode.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTools.Assert(!request.Task.IsCompleted);
|
||||||
|
request.Stopwatch.Restart();
|
||||||
|
|
||||||
|
var startNode = GetPoly(request.Start);
|
||||||
|
var endNode = GetPoly(request.End);
|
||||||
|
|
||||||
|
if (startNode == null || endNode == null)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = startNode;
|
||||||
|
request.Frontier.Add((0.0f, startNode));
|
||||||
|
request.CostSoFar[startNode] = 0.0f;
|
||||||
|
var count = 0;
|
||||||
|
var arrived = false;
|
||||||
|
|
||||||
|
while (request.Frontier.Count > 0 && count < NodeLimit)
|
||||||
|
{
|
||||||
|
// Handle whether we need to pause if we've taken too long
|
||||||
|
if (count % 20 == 0 && count > 0 && request.Stopwatch.Elapsed > PathTime)
|
||||||
|
{
|
||||||
|
// I had this happen once in testing but I don't think it should be possible?
|
||||||
|
DebugTools.Assert(request.Frontier.Count > 0);
|
||||||
|
return PathResult.Continuing;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Actual pathfinding here
|
||||||
|
(_, currentNode) = request.Frontier.Take();
|
||||||
|
|
||||||
|
// TODO: Need this at some stage but need to fix some steering bugs first before it's useable.
|
||||||
|
if (currentNode.Equals(endNode))// ||
|
||||||
|
//(endNode.GraphUid == currentNode.GraphUid &&
|
||||||
|
//(endNode.Box.Center - currentNode.Box.Center).Length < request.Range))
|
||||||
|
{
|
||||||
|
arrived = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var neighbor in currentNode.Neighbors)
|
||||||
|
{
|
||||||
|
var tileCost = GetTileCost(request, currentNode, neighbor);
|
||||||
|
|
||||||
|
if (tileCost.Equals(0f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f = g + h
|
||||||
|
// gScore is distance to the start node
|
||||||
|
// hScore is distance to the end node
|
||||||
|
var gScore = request.CostSoFar[currentNode] + tileCost;
|
||||||
|
if (request.CostSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.CameFrom[neighbor] = currentNode;
|
||||||
|
request.CostSoFar[neighbor] = 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
|
||||||
|
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
||||||
|
// (i.e. somewhere between 1 and infinite)
|
||||||
|
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
||||||
|
var hScore = OctileDistance(endNode, neighbor) * (1.0f + 1.0f / 1000.0f);
|
||||||
|
var fScore = gScore + hScore;
|
||||||
|
request.Frontier.Add((fScore, neighbor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arrived)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var route = ReconstructPath(request.CameFrom, currentNode);
|
||||||
|
var path = new Queue<EntityCoordinates>(route.Count);
|
||||||
|
|
||||||
|
foreach (var node in route)
|
||||||
|
{
|
||||||
|
// Due to partial planning some nodes may have been invalidated.
|
||||||
|
if (!node.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.Enqueue(node.Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTools.Assert(route.Count > 0);
|
||||||
|
request.Polys = route;
|
||||||
|
return PathResult.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Content.Server/NPC/Pathfinding/PathfindingSystem.BFS.cs
Normal file
121
Content.Server/NPC/Pathfinding/PathfindingSystem.BFS.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
private PathResult UpdateBFSPath(IRobustRandom random, BFSPathRequest request)
|
||||||
|
{
|
||||||
|
if (request.Task.IsCanceled)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need partial planning that uses best node.
|
||||||
|
PathPoly? currentNode = null;
|
||||||
|
|
||||||
|
// First run
|
||||||
|
if (!request.Started)
|
||||||
|
{
|
||||||
|
request.Frontier = new PriorityQueue<(float, PathPoly)>(PathPolyComparer);
|
||||||
|
request.Started = true;
|
||||||
|
}
|
||||||
|
// Re-validate nodes
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(_, currentNode) = request.Frontier.Peek();
|
||||||
|
|
||||||
|
if (!currentNode.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-validate parents too.
|
||||||
|
if (request.CameFrom.TryGetValue(currentNode, out var parentNode) && !parentNode.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTools.Assert(!request.Task.IsCompleted);
|
||||||
|
request.Stopwatch.Restart();
|
||||||
|
|
||||||
|
var startNode = GetPoly(request.Start);
|
||||||
|
|
||||||
|
if (startNode == null)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Frontier.Add((0.0f, startNode));
|
||||||
|
request.CostSoFar[startNode] = 0.0f;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (request.Frontier.Count > 0 && count < NodeLimit && count < request.ExpansionLimit)
|
||||||
|
{
|
||||||
|
// Handle whether we need to pause if we've taken too long
|
||||||
|
if (count % 20 == 0 && count > 0 && request.Stopwatch.Elapsed > PathTime)
|
||||||
|
{
|
||||||
|
// I had this happen once in testing but I don't think it should be possible?
|
||||||
|
DebugTools.Assert(request.Frontier.Count > 0);
|
||||||
|
return PathResult.Continuing;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Actual pathfinding here
|
||||||
|
(_, currentNode) = request.Frontier.Take();
|
||||||
|
|
||||||
|
foreach (var neighbor in currentNode.Neighbors)
|
||||||
|
{
|
||||||
|
var tileCost = GetTileCost(request, currentNode, neighbor);
|
||||||
|
|
||||||
|
if (tileCost.Equals(0f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f = g + h
|
||||||
|
// gScore is distance to the start node
|
||||||
|
// hScore is distance to the end node
|
||||||
|
var gScore = request.CostSoFar[currentNode] + tileCost;
|
||||||
|
if (request.CostSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.CameFrom[neighbor] = currentNode;
|
||||||
|
request.CostSoFar[neighbor] = gScore;
|
||||||
|
request.Frontier.Add((gScore, neighbor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.CostSoFar.Count == 0)
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random node to use?
|
||||||
|
(currentNode, _) = random.Pick(request.CostSoFar);
|
||||||
|
|
||||||
|
var route = ReconstructPath(request.CameFrom, currentNode);
|
||||||
|
var path = new Queue<EntityCoordinates>(route.Count);
|
||||||
|
|
||||||
|
foreach (var node in route)
|
||||||
|
{
|
||||||
|
// Due to partial planning some nodes may have been invalidated.
|
||||||
|
if (!node.IsValid())
|
||||||
|
{
|
||||||
|
return PathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.Enqueue(node.Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTools.Assert(route.Count > 0);
|
||||||
|
request.Polys = route;
|
||||||
|
return PathResult.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
Normal file
151
Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Code that is common to all pathfinding methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum amount of nodes we're allowed to expand.
|
||||||
|
/// </summary>
|
||||||
|
private const int NodeLimit = 200;
|
||||||
|
|
||||||
|
private sealed class PathComparer : IComparer<ValueTuple<float, PathPoly>>
|
||||||
|
{
|
||||||
|
public int Compare((float, PathPoly) x, (float, PathPoly) y)
|
||||||
|
{
|
||||||
|
return y.Item1.CompareTo(x.Item1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly PathComparer PathPolyComparer = new();
|
||||||
|
|
||||||
|
private Queue<PathPoly> ReconstructPath(Dictionary<PathPoly, PathPoly> path, PathPoly currentNodeRef)
|
||||||
|
{
|
||||||
|
var running = new List<PathPoly> { currentNodeRef };
|
||||||
|
while (path.ContainsKey(currentNodeRef))
|
||||||
|
{
|
||||||
|
var previousCurrent = currentNodeRef;
|
||||||
|
currentNodeRef = path[currentNodeRef];
|
||||||
|
path.Remove(previousCurrent);
|
||||||
|
running.Add(currentNodeRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
running = Simplify(running);
|
||||||
|
running.Reverse();
|
||||||
|
var result = new Queue<PathPoly>(running);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetTileCost(PathRequest request, PathPoly start, PathPoly end)
|
||||||
|
{
|
||||||
|
var modifier = 1f;
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
if ((end.Data.Flags & PathfindingBreadcrumbFlag.Space) != 0x0)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((request.CollisionLayer & end.Data.CollisionMask) != 0x0 ||
|
||||||
|
(request.CollisionMask & end.Data.CollisionLayer) != 0x0)
|
||||||
|
{
|
||||||
|
var isDoor = (end.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
|
||||||
|
var isAccess = (end.Data.Flags & PathfindingBreadcrumbFlag.Access) != 0x0;
|
||||||
|
|
||||||
|
// TODO: Handling power + door prying
|
||||||
|
// Door we should be able to open
|
||||||
|
if (isDoor && !isAccess)
|
||||||
|
{
|
||||||
|
modifier += 0.5f;
|
||||||
|
}
|
||||||
|
// Door we can force open one way or another
|
||||||
|
else if (isDoor && isAccess && (request.Flags & PathFlags.Prying) != 0x0)
|
||||||
|
{
|
||||||
|
modifier += 10f;
|
||||||
|
}
|
||||||
|
else if ((request.Flags & PathFlags.Smashing) != 0x0 && end.Data.Damage > 0f)
|
||||||
|
{
|
||||||
|
modifier += 10f + end.Data.Damage / 100f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifier * OctileDistance(end, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Simplifier
|
||||||
|
|
||||||
|
public List<PathPoly> Simplify(List<PathPoly> vertices, float tolerance = 0)
|
||||||
|
{
|
||||||
|
// TODO: Needs more work
|
||||||
|
if (vertices.Count <= 3)
|
||||||
|
return vertices;
|
||||||
|
|
||||||
|
var simplified = new List<PathPoly>();
|
||||||
|
|
||||||
|
for (var i = 0; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
// No wraparound for negative sooooo
|
||||||
|
var prev = vertices[i == 0 ? vertices.Count - 1 : i - 1];
|
||||||
|
var current = vertices[i];
|
||||||
|
var next = vertices[(i + 1) % vertices.Count];
|
||||||
|
|
||||||
|
var prevData = prev.Data;
|
||||||
|
var currentData = current.Data;
|
||||||
|
var nextData = next.Data;
|
||||||
|
|
||||||
|
// If they collinear, continue
|
||||||
|
if (i != 0 && i != vertices.Count - 1 &&
|
||||||
|
prevData.Equals(currentData) &&
|
||||||
|
currentData.Equals(nextData) &&
|
||||||
|
IsCollinear(prev, current, next, tolerance))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
simplified.Add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farseer didn't seem to handle straight lines and nuked all points
|
||||||
|
if (simplified.Count == 0)
|
||||||
|
{
|
||||||
|
simplified.Add(vertices[0]);
|
||||||
|
simplified.Add(vertices[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check LOS and cut out more nodes
|
||||||
|
// TODO: Grid cast
|
||||||
|
// https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/Detour/Source/DetourNavMeshQuery.cpp#L2455
|
||||||
|
// Essentially you just do a raycast but a specialised version.
|
||||||
|
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCollinear(PathPoly prev, PathPoly current, PathPoly next, float tolerance)
|
||||||
|
{
|
||||||
|
return FloatInRange(Area(prev, current, next), -tolerance, tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float Area(PathPoly a, PathPoly b, PathPoly c)
|
||||||
|
{
|
||||||
|
var (ax, ay) = a.Box.Center;
|
||||||
|
var (bx, by) = b.Box.Center;
|
||||||
|
var (cx, cy) = c.Box.Center;
|
||||||
|
|
||||||
|
return ax * (by - cy) + bx * (cy - ay) + cx * (ay - by);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FloatInRange(float value, float min, float max)
|
||||||
|
{
|
||||||
|
return (value >= min && value <= max);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
46
Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs
Normal file
46
Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
public float EuclideanDistance(PathPoly start, PathPoly end)
|
||||||
|
{
|
||||||
|
var (dx, dy) = GetDiff(start, end);
|
||||||
|
return MathF.Sqrt((dx * dx + dy * dy));
|
||||||
|
}
|
||||||
|
|
||||||
|
public float ManhattanDistance(PathPoly start, PathPoly end)
|
||||||
|
{
|
||||||
|
var (dx, dy) = GetDiff(start, end);
|
||||||
|
return dx + dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float OctileDistance(PathPoly start, PathPoly end)
|
||||||
|
{
|
||||||
|
var (dx, dy) = GetDiff(start, end);
|
||||||
|
return dx + dy + (1.41f - 2) * Math.Min(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GetDiff(PathPoly start, PathPoly end)
|
||||||
|
{
|
||||||
|
var startPos = start.Box.Center;
|
||||||
|
var endPos = end.Box.Center;
|
||||||
|
|
||||||
|
if (end.GraphUid != start.GraphUid)
|
||||||
|
{
|
||||||
|
if (!TryComp<TransformComponent>(start.GraphUid, out var startXform) ||
|
||||||
|
!TryComp<TransformComponent>(end.GraphUid, out var endXform))
|
||||||
|
{
|
||||||
|
return Vector2.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
endPos = startXform.InvWorldMatrix.Transform(endXform.WorldMatrix.Transform(endPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Numerics when we changeover.
|
||||||
|
var diff = startPos - endPos;
|
||||||
|
var ab = Vector2.Abs(diff);
|
||||||
|
return ab;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,305 +1,761 @@
|
|||||||
using Content.Server.Access;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Content.Shared.Access.Systems;
|
using System.Linq;
|
||||||
using Content.Shared.GameTicking;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Destructible;
|
||||||
|
using Content.Shared.Access.Components;
|
||||||
|
using Content.Shared.Doors.Components;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Content.Shared.Physics;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Physics;
|
using Robust.Shared.Physics;
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Physics.Events;
|
using Robust.Shared.Physics.Events;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding;
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
public sealed partial class PathfindingSystem
|
public sealed partial class PathfindingSystem
|
||||||
{
|
{
|
||||||
/*
|
private static readonly TimeSpan UpdateCooldown = TimeSpan.FromSeconds(0.45);
|
||||||
* Handles pathfinding while on a grid.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
// What relevant collision groups we track for pathfinding.
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
// Stuff like chairs have collision but aren't relevant for mobs.
|
||||||
|
public const int PathfindingCollisionMask = (int) CollisionGroup.MobMask;
|
||||||
|
public const int PathfindingCollisionLayer = (int) CollisionGroup.MobLayer;
|
||||||
|
|
||||||
// Queued pathfinding graph updates
|
private readonly Stopwatch _stopwatch = new();
|
||||||
private readonly Queue<MoveEvent> _moveUpdateQueue = new();
|
|
||||||
private readonly Queue<AccessReaderChangeEvent> _accessReaderUpdateQueue = new();
|
|
||||||
private readonly Queue<TileRef> _tileUpdateQueue = new();
|
|
||||||
|
|
||||||
public override void Initialize()
|
// Probably can't pool polys as there might be old pathfinding refs to them.
|
||||||
|
|
||||||
|
private readonly ObjectPool<HashSet<PathPoly>> _neighborPolyPool =
|
||||||
|
new DefaultObjectPool<HashSet<PathPoly>>(new DefaultPooledObjectPolicy<HashSet<PathPoly>>(), MaxPoolSize);
|
||||||
|
|
||||||
|
private static int MaxPoolSize = Environment.ProcessorCount * 2 * ChunkSize * ChunkSize;
|
||||||
|
|
||||||
|
private void InitializeGrid()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
|
||||||
|
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||||
|
SubscribeLocalEvent<GridPathfindingComponent, EntityPausedEvent>(OnGridPathPause);
|
||||||
|
SubscribeLocalEvent<GridPathfindingComponent, ComponentShutdown>(OnGridPathShutdown);
|
||||||
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
|
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
|
||||||
SubscribeLocalEvent<MoveEvent>(OnMoveEvent);
|
|
||||||
SubscribeLocalEvent<AccessReaderChangeEvent>(OnAccessChange);
|
|
||||||
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
|
|
||||||
SubscribeLocalEvent<TileChangedEvent>(OnTileChange);
|
|
||||||
SubscribeLocalEvent<PhysicsBodyTypeChangedEvent>(OnBodyTypeChange);
|
SubscribeLocalEvent<PhysicsBodyTypeChangedEvent>(OnBodyTypeChange);
|
||||||
|
SubscribeLocalEvent<MoveEvent>(OnMoveEvent);
|
||||||
// Handle all the base grid changes
|
|
||||||
// Anything that affects traversal (i.e. collision layer) is handled separately.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBodyTypeChange(ref PhysicsBodyTypeChangedEvent ev)
|
private void OnGridPathPause(EntityUid uid, GridPathfindingComponent component, EntityPausedEvent args)
|
||||||
{
|
{
|
||||||
var xform = Transform(ev.Entity);
|
// TODO: Need the offsets + time serializer. Mainly just need this here to ensure it gets update after load
|
||||||
|
if (!args.Paused && component.NextUpdate < _timing.CurTime)
|
||||||
if (!IsRelevant(xform, ev.Component)) return;
|
component.NextUpdate = _timing.CurTime;
|
||||||
|
|
||||||
var node = GetNode(xform);
|
|
||||||
node?.RemoveEntity(ev.Entity);
|
|
||||||
node?.AddEntity(ev.Entity, ev.Component, EntityManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGridAdd(GridAddEvent ev)
|
private void OnGridPathShutdown(EntityUid uid, GridPathfindingComponent component, ComponentShutdown args)
|
||||||
{
|
{
|
||||||
EnsureComp<GridPathfindingComponent>(ev.EntityUid);
|
foreach (var chunk in component.Chunks)
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCollisionChange(ref CollisionChangeEvent collisionEvent)
|
|
||||||
{
|
|
||||||
if (collisionEvent.CanCollide)
|
|
||||||
{
|
{
|
||||||
OnEntityAdd(collisionEvent.Body.Owner);
|
// Invalidate all polygons in case there's portals or the likes.
|
||||||
}
|
foreach (var poly in chunk.Value.Polygons)
|
||||||
else
|
{
|
||||||
{
|
ClearTilePolys(poly);
|
||||||
OnEntityRemove(collisionEvent.Body.Owner);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMoveEvent(ref MoveEvent moveEvent)
|
|
||||||
{
|
|
||||||
_moveUpdateQueue.Enqueue(moveEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTileChange(TileChangedEvent ev)
|
|
||||||
{
|
|
||||||
_tileUpdateQueue.Enqueue(ev.NewTile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAccessChange(AccessReaderChangeEvent message)
|
|
||||||
{
|
|
||||||
_accessReaderUpdateQueue.Enqueue(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PathfindingChunk GetOrCreateChunk(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 vector2i = new Vector2i(chunkX, chunkY);
|
|
||||||
var comp = Comp<GridPathfindingComponent>(tile.GridUid);
|
|
||||||
var chunks = comp.Graph;
|
|
||||||
|
|
||||||
if (!chunks.TryGetValue(vector2i, out var chunk))
|
|
||||||
{
|
|
||||||
chunk = CreateChunk(comp, vector2i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chunk;
|
component.DirtyChunks.Clear();
|
||||||
|
component.Chunks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathfindingChunk CreateChunk(GridPathfindingComponent comp, Vector2i indices)
|
private void UpdateGrid()
|
||||||
{
|
{
|
||||||
var grid = _mapManager.GetGrid(comp.Owner);
|
var curTime = _timing.CurTime;
|
||||||
var newChunk = new PathfindingChunk(grid.GridEntityId, indices);
|
#if DEBUG
|
||||||
comp.Graph.Add(indices, newChunk);
|
var updateCount = 0;
|
||||||
newChunk.Initialize(grid);
|
#endif
|
||||||
|
_stopwatch.Restart();
|
||||||
|
|
||||||
return newChunk;
|
// We defer chunk updates because rebuilding a navmesh is hella costly
|
||||||
}
|
// If we're paused then NPCs can't run anyway.
|
||||||
|
foreach (var comp in EntityQuery<GridPathfindingComponent>())
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return the corresponding PathfindingNode for this tile
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="tile"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public PathfindingNode GetNode(TileRef tile)
|
|
||||||
{
|
|
||||||
var chunk = GetOrCreateChunk(tile);
|
|
||||||
var node = chunk.GetNode(tile);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTileUpdate(TileRef tile)
|
|
||||||
{
|
|
||||||
if (!_mapManager.GridExists(tile.GridUid)) return;
|
|
||||||
|
|
||||||
var node = GetNode(tile);
|
|
||||||
node.UpdateTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsRelevant(TransformComponent xform, PhysicsComponent physics)
|
|
||||||
{
|
|
||||||
return xform.GridUid != null && (TrackedCollisionLayers & physics.CollisionLayer) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to add the entity to the relevant pathfinding node
|
|
||||||
/// </summary>
|
|
||||||
/// The node will filter it to the correct category (if possible)
|
|
||||||
/// <param name="entity"></param>
|
|
||||||
private void OnEntityAdd(EntityUid entity, TransformComponent? xform = null, PhysicsComponent? physics = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(entity, ref xform, false) ||
|
|
||||||
!Resolve(entity, ref physics, false)) return;
|
|
||||||
|
|
||||||
if (!IsRelevant(xform, physics) ||
|
|
||||||
!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
|
||||||
{
|
{
|
||||||
return;
|
if (comp.DirtyChunks.Count == 0 ||
|
||||||
|
comp.NextUpdate < curTime ||
|
||||||
|
!TryComp<IMapGridComponent>(comp.Owner, out var mapGridComp))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirtyPortals = comp.DirtyPortals;
|
||||||
|
dirtyPortals.Clear();
|
||||||
|
|
||||||
|
// TODO: Often we invalidate the entire chunk when it might be something as simple as an airlock change
|
||||||
|
// Would be better to handle that though this was safer and max it's taking is like 1-2ms every half-second.
|
||||||
|
var dirt = new GridPathfindingChunk[comp.DirtyChunks.Count];
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
foreach (var origin in comp.DirtyChunks)
|
||||||
|
{
|
||||||
|
var chunk = GetChunk(origin, comp.Owner, comp);
|
||||||
|
dirt[i] = chunk;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We force clear portals in a single-threaded context to be safe
|
||||||
|
// as they may not be thread-safe to touch.
|
||||||
|
foreach (var chunk in dirt)
|
||||||
|
{
|
||||||
|
foreach (var (_, poly) in chunk.PortalPolys)
|
||||||
|
{
|
||||||
|
ClearPoly(poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.PortalPolys.Clear();
|
||||||
|
|
||||||
|
foreach (var portal in chunk.Portals)
|
||||||
|
{
|
||||||
|
dirtyPortals.Add(portal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Inflate grid bounds slightly and get chunks.
|
||||||
|
// This is for map <> grid pathfinding
|
||||||
|
|
||||||
|
// Without parallel this is roughly 3x slower on my desktop.
|
||||||
|
Parallel.For(0, dirt.Length, i =>
|
||||||
|
{
|
||||||
|
// Doing the queries per task seems faster.
|
||||||
|
var accessQuery = GetEntityQuery<AccessReaderComponent>();
|
||||||
|
var destructibleQuery = GetEntityQuery<DestructibleComponent>();
|
||||||
|
var doorQuery = GetEntityQuery<DoorComponent>();
|
||||||
|
var fixturesQuery = GetEntityQuery<FixturesComponent>();
|
||||||
|
var physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||||
|
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||||
|
BuildBreadcrumbs(dirt[i], mapGridComp.Grid, accessQuery, destructibleQuery, doorQuery, fixturesQuery, physicsQuery, xformQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
const int Division = 4;
|
||||||
|
|
||||||
|
// You can safely do this in parallel as long as no neighbor chunks are being touched in the same iteration.
|
||||||
|
// You essentially do bottom left, bottom right, top left, top right in quadrants.
|
||||||
|
// For each 4x4 block of chunks.
|
||||||
|
|
||||||
|
// i.e. first iteration: 0,0; 2,0; 0,2
|
||||||
|
// second iteration: 1,0; 3,0; 1;2
|
||||||
|
// third iteration: 0,1; 2,1; 0,3 etc
|
||||||
|
|
||||||
|
// TODO: You can probably skimp on some neighbor chunk caches
|
||||||
|
for (var it = 0; it < Division; it++)
|
||||||
|
{
|
||||||
|
var it1 = it;
|
||||||
|
|
||||||
|
Parallel.For(0, dirt.Length, j =>
|
||||||
|
{
|
||||||
|
var chunk = dirt[j];
|
||||||
|
// Check if the chunk is safe on this iteration.
|
||||||
|
var x = Math.Abs(chunk.Origin.X % 2);
|
||||||
|
var y = Math.Abs(chunk.Origin.Y % 2);
|
||||||
|
var index = x * 2 + y;
|
||||||
|
|
||||||
|
if (index != it1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BuildNavmesh(chunk, comp);
|
||||||
|
#if DEBUG
|
||||||
|
Interlocked.Increment(ref updateCount);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle portals at the end after having cleared their neighbors above.
|
||||||
|
// We do this because there's no guarantee of where these are for chunks.
|
||||||
|
foreach (var portal in dirtyPortals)
|
||||||
|
{
|
||||||
|
var polyA = GetPoly(portal.CoordinatesA);
|
||||||
|
var polyB = GetPoly(portal.CoordinatesB);
|
||||||
|
|
||||||
|
if (polyA == null || polyB == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
|
||||||
|
DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
|
||||||
|
var chunkA = GetChunk(polyA.ChunkOrigin, polyA.GraphUid);
|
||||||
|
var chunkB = GetChunk(polyB.ChunkOrigin, polyB.GraphUid);
|
||||||
|
|
||||||
|
chunkA.PortalPolys.TryAdd(portal, polyA);
|
||||||
|
chunkB.PortalPolys.TryAdd(portal, polyB);
|
||||||
|
AddNeighbors(polyA, polyB);
|
||||||
|
}
|
||||||
|
|
||||||
|
comp.DirtyChunks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
var tileRef = grid.GetTileRef(xform.Coordinates);
|
#if DEBUG
|
||||||
|
if (updateCount > 0)
|
||||||
var chunk = GetOrCreateChunk(tileRef);
|
_sawmill.Debug($"Updated {updateCount} nav chunks in {_stopwatch.Elapsed.TotalMilliseconds:0.000}ms");
|
||||||
var node = chunk.GetNode(tileRef);
|
#endif
|
||||||
node.AddEntity(entity, physics, EntityManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEntityRemove(EntityUid entity, TransformComponent? xform = null)
|
private bool IsBodyRelevant(PhysicsComponent body)
|
||||||
{
|
{
|
||||||
if (!Resolve(entity, ref xform, false) ||
|
if (!body.Hard || !body.CanCollide || body.BodyType != BodyType.Static)
|
||||||
!_mapManager.TryGetGrid(xform.GridUid, out var grid)) return;
|
|
||||||
|
|
||||||
var node = GetNode(grid.GetTileRef(xform.Coordinates));
|
|
||||||
node.RemoveEntity(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEntityRemove(EntityUid entity, EntityCoordinates coordinates)
|
|
||||||
{
|
|
||||||
var gridId = coordinates.GetGridUid(EntityManager);
|
|
||||||
if (!_mapManager.TryGetGrid(gridId, out var grid)) return;
|
|
||||||
|
|
||||||
var node = GetNode(grid.GetTileRef(coordinates));
|
|
||||||
node.RemoveEntity(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PathfindingNode? GetNode(TransformComponent xform)
|
|
||||||
{
|
|
||||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid)) return null;
|
|
||||||
return GetNode(grid.GetTileRef(xform.Coordinates));
|
|
||||||
}
|
|
||||||
|
|
||||||
private PathfindingNode? GetNode(EntityCoordinates coordinates)
|
|
||||||
{
|
|
||||||
if (!_mapManager.TryGetGrid(coordinates.GetGridUid(EntityManager), out var grid)) return null;
|
|
||||||
return GetNode(grid.GetTileRef(coordinates));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When an entity moves around we'll remove it from its old node and add it to its new node (if applicable)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="moveEvent"></param>
|
|
||||||
private void OnEntityMove(MoveEvent moveEvent)
|
|
||||||
{
|
|
||||||
if (!TryComp<TransformComponent>(moveEvent.Sender, out var xform)) return;
|
|
||||||
|
|
||||||
// If we've moved to space or the likes then remove us.
|
|
||||||
if (!TryComp<PhysicsComponent>(moveEvent.Sender, out var physics) ||
|
|
||||||
!IsRelevant(xform, physics))
|
|
||||||
{
|
|
||||||
OnEntityRemove(moveEvent.Sender, moveEvent.OldPosition);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldNode = GetNode(moveEvent.OldPosition);
|
|
||||||
var newNode = GetNode(moveEvent.NewPosition);
|
|
||||||
|
|
||||||
if (oldNode?.Equals(newNode) == true) return;
|
|
||||||
|
|
||||||
oldNode?.RemoveEntity(moveEvent.Sender);
|
|
||||||
newNode?.AddEntity(moveEvent.Sender, physics, EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Need to rethink the pathfinder utils (traversable etc.). Maybe just chuck them all in PathfindingSystem
|
|
||||||
// Otherwise you get the steerer using this and the pathfinders using a different traversable.
|
|
||||||
// Also look at increasing tile cost the more physics entities are on it
|
|
||||||
public bool CanTraverse(EntityUid entity, EntityCoordinates coordinates)
|
|
||||||
{
|
|
||||||
var gridId = coordinates.GetGridUid(EntityManager);
|
|
||||||
if (gridId == null)
|
|
||||||
return false;
|
|
||||||
var tile = _mapManager.GetGrid(gridId.Value).GetTileRef(coordinates);
|
|
||||||
var node = GetNode(tile);
|
|
||||||
return CanTraverse(entity, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanTraverse(EntityUid entity, PathfindingNode node)
|
|
||||||
{
|
|
||||||
if (EntityManager.TryGetComponent(entity, out IPhysBody? physics) &&
|
|
||||||
(physics.CollisionMask & node.BlockedCollisionMask) != 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var access = _accessReader.FindAccessTags(entity);
|
if ((body.CollisionMask & PathfindingCollisionLayer) != 0x0 ||
|
||||||
foreach (var reader in node.AccessReaders)
|
(body.CollisionLayer & PathfindingCollisionMask) != 0x0)
|
||||||
{
|
{
|
||||||
if (!_accessReader.IsAllowed(access, reader))
|
return true;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnRoundRestart(RoundRestartCleanupEvent ev)
|
private void OnCollisionChange(ref CollisionChangeEvent ev)
|
||||||
{
|
{
|
||||||
_moveUpdateQueue.Clear();
|
if (!IsBodyRelevant(ev.Body))
|
||||||
_accessReaderUpdateQueue.Clear();
|
return;
|
||||||
_tileUpdateQueue.Clear();
|
|
||||||
|
var xform = Transform(ev.Body.Owner);
|
||||||
|
|
||||||
|
if (xform.GridUid == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// This will also rebuild on door open / closes which I think is good?
|
||||||
|
DirtyChunk(xform.GridUid.Value, xform.Coordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessGridUpdates()
|
private void OnBodyTypeChange(ref PhysicsBodyTypeChangedEvent ev)
|
||||||
{
|
{
|
||||||
var totalUpdates = 0;
|
if (IsBodyRelevant(ev.Component) &&
|
||||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
TryComp<TransformComponent>(ev.Entity, out var xform) &&
|
||||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
xform.GridUid != null)
|
||||||
|
|
||||||
foreach (var update in _accessReaderUpdateQueue)
|
|
||||||
{
|
{
|
||||||
if (!xformQuery.TryGetComponent(update.Sender, out var xform) ||
|
DirtyChunk(xform.GridUid.Value, xform.Coordinates);
|
||||||
!bodyQuery.TryGetComponent(update.Sender, out var body)) continue;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (update.Enabled)
|
private void OnMoveEvent(ref MoveEvent ev)
|
||||||
|
{
|
||||||
|
if (!TryComp<PhysicsComponent>(ev.Sender, out var body) ||
|
||||||
|
body.BodyType != BodyType.Static ||
|
||||||
|
HasComp<IMapGridComponent>(ev.Sender) ||
|
||||||
|
ev.OldPosition.Equals(ev.NewPosition))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldGridUid = ev.OldPosition.GetGridUid(EntityManager);
|
||||||
|
var gridUid = ev.NewPosition.GetGridUid(EntityManager);
|
||||||
|
|
||||||
|
// Not on a grid at all so just ignore.
|
||||||
|
if (oldGridUid == gridUid && oldGridUid == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldGridUid != null && gridUid != null)
|
||||||
|
{
|
||||||
|
// If the chunk hasn't changed then just dirty that one.
|
||||||
|
var oldOrigin = GetOrigin(ev.OldPosition, oldGridUid.Value);
|
||||||
|
var origin = GetOrigin(ev.NewPosition, gridUid.Value);
|
||||||
|
|
||||||
|
if (oldOrigin == origin)
|
||||||
{
|
{
|
||||||
OnEntityAdd(update.Sender, xform, body);
|
// TODO: Don't need to transform again numpty.
|
||||||
|
DirtyChunk(oldGridUid.Value, ev.NewPosition);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
if (oldGridUid != null)
|
||||||
|
{
|
||||||
|
DirtyChunk(oldGridUid.Value, ev.OldPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridUid != null)
|
||||||
|
{
|
||||||
|
DirtyChunk(gridUid.Value, ev.NewPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGridInit(GridInitializeEvent ev)
|
||||||
|
{
|
||||||
|
EnsureComp<GridPathfindingComponent>(ev.EntityUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGridRemoved(GridRemovalEvent ev)
|
||||||
|
{
|
||||||
|
RemComp<GridPathfindingComponent>(ev.EntityUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queues the entire relevant chunk to be re-built in the next update.
|
||||||
|
/// </summary>
|
||||||
|
private void DirtyChunk(EntityUid gridUid, EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
if (!TryComp<GridPathfindingComponent>(gridUid, out var comp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentTime = _timing.CurTime;
|
||||||
|
|
||||||
|
if (comp.NextUpdate < currentTime)
|
||||||
|
comp.NextUpdate = currentTime + UpdateCooldown;
|
||||||
|
|
||||||
|
var chunks = comp.DirtyChunks;
|
||||||
|
// TODO: Change these args around.
|
||||||
|
chunks.Add(GetOrigin(coordinates, gridUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GridPathfindingChunk GetChunk(Vector2i origin, EntityUid uid, GridPathfindingComponent? component = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Chunks.TryGetValue(origin, out var chunk))
|
||||||
|
return chunk;
|
||||||
|
|
||||||
|
chunk = new GridPathfindingChunk()
|
||||||
|
{
|
||||||
|
Origin = origin,
|
||||||
|
};
|
||||||
|
|
||||||
|
component.Chunks[origin] = chunk;
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetChunk(Vector2i origin, GridPathfindingComponent component, [NotNullWhen(true)] out GridPathfindingChunk? chunk)
|
||||||
|
{
|
||||||
|
return component.Chunks.TryGetValue(origin, out chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte GetIndex(int x, int y)
|
||||||
|
{
|
||||||
|
return (byte) (x * ChunkSize + y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2i GetOrigin(Vector2 localPos)
|
||||||
|
{
|
||||||
|
return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2i GetOrigin(EntityCoordinates coordinates, EntityUid gridUid)
|
||||||
|
{
|
||||||
|
var gridXform = Transform(gridUid);
|
||||||
|
var localPos = gridXform.InvWorldMatrix.Transform(coordinates.ToMapPos(EntityManager));
|
||||||
|
return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildBreadcrumbs(GridPathfindingChunk chunk,
|
||||||
|
IMapGrid grid,
|
||||||
|
EntityQuery<AccessReaderComponent> accessQuery,
|
||||||
|
EntityQuery<DestructibleComponent> destructibleQuery,
|
||||||
|
EntityQuery<DoorComponent> doorQuery,
|
||||||
|
EntityQuery<FixturesComponent> fixturesQuery,
|
||||||
|
EntityQuery<PhysicsComponent> physicsQuery,
|
||||||
|
EntityQuery<TransformComponent> xformQuery)
|
||||||
|
{
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
var points = chunk.Points;
|
||||||
|
var gridOrigin = chunk.Origin * ChunkSize;
|
||||||
|
var tileEntities = new ValueList<EntityUid>();
|
||||||
|
|
||||||
|
// TODO: Pool this or something
|
||||||
|
var chunkPolys = new List<PathPoly>[ChunkSize * ChunkSize];
|
||||||
|
|
||||||
|
for (var i = 0; i < chunkPolys.Length; i++)
|
||||||
|
{
|
||||||
|
chunkPolys[i] = new List<PathPoly>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tilePolys = new ValueList<Box2i>(SubStep);
|
||||||
|
|
||||||
|
// Need to get the relevant polygons in each tile.
|
||||||
|
// If we wanted to create a larger navmesh we could triangulate these points but in our case we're just going
|
||||||
|
// to treat them as tile-based.
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
{
|
{
|
||||||
OnEntityRemove(update.Sender, xform);
|
// Tile
|
||||||
|
var tilePos = new Vector2i(x, y) + gridOrigin;
|
||||||
|
tilePolys.Clear();
|
||||||
|
|
||||||
|
var tile = grid.GetTileRef(tilePos);
|
||||||
|
var flags = tile.Tile.IsEmpty ? PathfindingBreadcrumbFlag.Space : PathfindingBreadcrumbFlag.None;
|
||||||
|
// var isBorder = x < 0 || y < 0 || x == ChunkSize - 1 || y == ChunkSize - 1;
|
||||||
|
|
||||||
|
tileEntities.Clear();
|
||||||
|
var anchored = grid.GetAnchoredEntitiesEnumerator(tilePos);
|
||||||
|
|
||||||
|
while (anchored.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
// Irrelevant for pathfinding
|
||||||
|
if (!physicsQuery.TryGetComponent(ent, out var body) ||
|
||||||
|
!IsBodyRelevant(body))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tileEntities.Add(ent.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var subX = 0; subX < SubStep; subX++)
|
||||||
|
{
|
||||||
|
for (var subY = 0; subY < SubStep; subY++)
|
||||||
|
{
|
||||||
|
var xOffset = x * SubStep + subX;
|
||||||
|
var yOffset = y * SubStep + subY;
|
||||||
|
|
||||||
|
// Subtile
|
||||||
|
var localPos = new Vector2(StepOffset + gridOrigin.X + x + (float) subX / SubStep, StepOffset + gridOrigin.Y + y + (float) subY / SubStep);
|
||||||
|
var collisionMask = 0x0;
|
||||||
|
var collisionLayer = 0x0;
|
||||||
|
var damage = 0f;
|
||||||
|
|
||||||
|
foreach (var ent in tileEntities)
|
||||||
|
{
|
||||||
|
if (!fixturesQuery.TryGetComponent(ent, out var fixtures))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// TODO: Inefficient af
|
||||||
|
foreach (var (_, fixture) in fixtures.Fixtures)
|
||||||
|
{
|
||||||
|
// Don't need to re-do it.
|
||||||
|
if ((collisionMask & fixture.CollisionMask) == fixture.CollisionMask &&
|
||||||
|
(collisionLayer & fixture.CollisionLayer) == fixture.CollisionLayer)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Do an AABB check first as it's probably faster, then do an actual point check.
|
||||||
|
var intersects = false;
|
||||||
|
|
||||||
|
foreach (var proxy in fixture.Proxies)
|
||||||
|
{
|
||||||
|
if (!proxy.AABB.Contains(localPos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
intersects = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intersects ||
|
||||||
|
!xformQuery.TryGetComponent(ent, out var xform))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_fixtures.TestPoint(fixture.Shape, new Transform(xform.LocalPosition, xform.LocalRotation), localPos))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
collisionLayer |= fixture.CollisionLayer;
|
||||||
|
collisionMask |= fixture.CollisionMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessQuery.HasComponent(ent))
|
||||||
|
{
|
||||||
|
flags |= PathfindingBreadcrumbFlag.Access;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doorQuery.HasComponent(ent))
|
||||||
|
{
|
||||||
|
flags |= PathfindingBreadcrumbFlag.Door;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destructibleQuery.TryGetComponent(ent, out var damageable))
|
||||||
|
{
|
||||||
|
damage += _destructible.DestroyedAt(ent, damageable).Float();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & PathfindingBreadcrumbFlag.Space) != 0x0)
|
||||||
|
{
|
||||||
|
DebugTools.Assert(tileEntities.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var crumb = new PathfindingBreadcrumb()
|
||||||
|
{
|
||||||
|
Coordinates = new Vector2i(xOffset, yOffset),
|
||||||
|
Data = new PathfindingData(flags, collisionLayer, collisionMask, damage),
|
||||||
|
};
|
||||||
|
|
||||||
|
points[xOffset, yOffset] = crumb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we got tile data and we can get the polys
|
||||||
|
var data = points[x * SubStep, y * SubStep].Data;
|
||||||
|
var start = Vector2i.Zero;
|
||||||
|
|
||||||
|
for (var i = 0; i < SubStep * SubStep; i++)
|
||||||
|
{
|
||||||
|
var ix = i / SubStep;
|
||||||
|
var iy = i % SubStep;
|
||||||
|
|
||||||
|
var nextX = (i + 1) / SubStep;
|
||||||
|
var nextY = (i + 1) % SubStep;
|
||||||
|
|
||||||
|
// End point
|
||||||
|
if (iy == SubStep - 1 ||
|
||||||
|
!points[x * SubStep + nextX, y * SubStep + nextY].Data.Equals(data))
|
||||||
|
{
|
||||||
|
tilePolys.Add(new Box2i(start, new Vector2i(ix, iy)));
|
||||||
|
|
||||||
|
if (i < (SubStep * SubStep) - 1)
|
||||||
|
{
|
||||||
|
start = new Vector2i(nextX, nextY);
|
||||||
|
data = points[x * SubStep + nextX, y * SubStep + nextY].Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now combine the lines
|
||||||
|
var anyCombined = true;
|
||||||
|
|
||||||
|
while (anyCombined)
|
||||||
|
{
|
||||||
|
anyCombined = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < tilePolys.Count; i++)
|
||||||
|
{
|
||||||
|
var poly = tilePolys[i];
|
||||||
|
data = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
|
||||||
|
|
||||||
|
for (var j = i + 1; j < tilePolys.Count; j++)
|
||||||
|
{
|
||||||
|
var nextPoly = tilePolys[j];
|
||||||
|
var nextData = points[x * SubStep + nextPoly.Left, y * SubStep + nextPoly.Bottom].Data;
|
||||||
|
|
||||||
|
// Oh no, Combine
|
||||||
|
if (poly.Bottom == nextPoly.Bottom &&
|
||||||
|
poly.Top == nextPoly.Top &&
|
||||||
|
poly.Right + 1 == nextPoly.Left &&
|
||||||
|
data.Equals(nextData))
|
||||||
|
{
|
||||||
|
tilePolys.RemoveAt(j);
|
||||||
|
j--;
|
||||||
|
poly = new Box2i(poly.Left, poly.Bottom, poly.Right + 1, poly.Top);
|
||||||
|
anyCombined = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tilePolys[i] = poly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Can store a hash for each tile and check if the breadcrumbs match and avoid allocating these at all.
|
||||||
|
var tilePoly = chunkPolys[x * ChunkSize + y];
|
||||||
|
var polyOffset = gridOrigin + new Vector2(x, y);
|
||||||
|
|
||||||
|
foreach (var poly in tilePolys)
|
||||||
|
{
|
||||||
|
var box = new Box2((Vector2) poly.BottomLeft / SubStep + polyOffset,
|
||||||
|
(Vector2) (poly.TopRight + Vector2i.One) / SubStep + polyOffset);
|
||||||
|
var polyData = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
|
||||||
|
|
||||||
|
var neighbors = _neighborPolyPool.Get();
|
||||||
|
tilePoly.Add(new PathPoly(grid.GridEntityId, chunk.Origin, GetIndex(x, y), box, polyData, neighbors));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalUpdates++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_accessReaderUpdateQueue.Clear();
|
// Check if the tiles match
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
foreach (var tile in _tileUpdateQueue)
|
|
||||||
{
|
{
|
||||||
OnTileUpdate(tile);
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
totalUpdates++;
|
{
|
||||||
|
var index = x * ChunkSize + y;
|
||||||
|
var polys = chunkPolys[index];
|
||||||
|
ref var existing = ref chunk.Polygons[index];
|
||||||
|
|
||||||
|
var isEquivalent = true;
|
||||||
|
|
||||||
|
if (polys.Count == existing.Count)
|
||||||
|
{
|
||||||
|
// May want to update damage or the likes if it's different but not invalidate the ref.
|
||||||
|
for (var i = 0; i < existing.Count; i++)
|
||||||
|
{
|
||||||
|
var ePoly = existing[i];
|
||||||
|
var poly = polys[i];
|
||||||
|
|
||||||
|
if (!ePoly.IsEquivalent(poly))
|
||||||
|
{
|
||||||
|
isEquivalent = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ePoly.Data.Damage = poly.Data.Damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEquivalent)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearTilePolys(existing);
|
||||||
|
existing.AddRange(polys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_tileUpdateQueue.Clear();
|
// _sawmill.Debug($"Built breadcrumbs in {sw.Elapsed.TotalMilliseconds}ms");
|
||||||
var moveUpdateCount = Math.Max(50 - totalUpdates, 0);
|
SendBreadcrumbs(chunk, grid.GridEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
// Other updates are high priority so for this we'll just defer it if there's a spike (explosion, etc.)
|
/// <summary>
|
||||||
// If the move updates grow too large then we'll just do it
|
/// Clears all of the polygons on a tile.
|
||||||
if (_moveUpdateQueue.Count > 100)
|
/// </summary>
|
||||||
|
private void ClearTilePolys(List<PathPoly> polys)
|
||||||
|
{
|
||||||
|
foreach (var poly in polys)
|
||||||
{
|
{
|
||||||
moveUpdateCount = _moveUpdateQueue.Count - 100;
|
ClearPoly(poly);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveUpdateCount = Math.Min(moveUpdateCount, _moveUpdateQueue.Count);
|
polys.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < moveUpdateCount; i++)
|
/// <summary>
|
||||||
|
/// Clears a polygon and invalidates its flags if anyone still has a reference to it.
|
||||||
|
/// </summary>
|
||||||
|
private void ClearPoly(PathPoly poly)
|
||||||
|
{
|
||||||
|
foreach (var neighbor in poly.Neighbors)
|
||||||
{
|
{
|
||||||
OnEntityMove(_moveUpdateQueue.Dequeue());
|
neighbor.Neighbors.Remove(poly);
|
||||||
|
poly.Neighbors.Remove(neighbor);
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugTools.Assert(_moveUpdateQueue.Count < 1000);
|
// If any paths have a ref to it let them know that the class is no longer a valid node.
|
||||||
|
poly.Data.Flags = PathfindingBreadcrumbFlag.Invalid;
|
||||||
|
poly.Neighbors.Clear();
|
||||||
|
_neighborPolyPool.Return(poly.Neighbors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildNavmesh(GridPathfindingChunk chunk, GridPathfindingComponent component)
|
||||||
|
{
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
var chunkPolys = chunk.Polygons;
|
||||||
|
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(-1, 0), out var leftChunk);
|
||||||
|
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, -1), out var bottomChunk);
|
||||||
|
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(1, 0), out var rightChunk);
|
||||||
|
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, 1), out var topChunk);
|
||||||
|
|
||||||
|
// Now we can get the neighbors for our tile polys
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var index = GetIndex(x, y);
|
||||||
|
var tile = chunkPolys[index];
|
||||||
|
|
||||||
|
for (byte i = 0; i < tile.Count; i++)
|
||||||
|
{
|
||||||
|
var poly = tile[i];
|
||||||
|
var enlarged = poly.Box.Enlarged(StepOffset);
|
||||||
|
|
||||||
|
// Shouldn't need to wraparound as previous neighbors would've handled us.
|
||||||
|
for (var j = (byte) (i + 1); j < tile.Count; j++)
|
||||||
|
{
|
||||||
|
var neighbor = tile[j];
|
||||||
|
var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
|
||||||
|
var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
|
||||||
|
|
||||||
|
// Need to ensure they intersect by at least 2 tiles.
|
||||||
|
if (overlap <= 0.5f / SubStep)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AddNeighbors(poly, neighbor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Get neighbor tile polys
|
||||||
|
for (var ix = -1; ix <= 1; ix++)
|
||||||
|
{
|
||||||
|
for (var iy = -1; iy <= 1; iy++)
|
||||||
|
{
|
||||||
|
if (ix != 0 && iy != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighborX = x + ix;
|
||||||
|
var neighborY = y + iy;
|
||||||
|
var neighborIndex = GetIndex(neighborX, neighborY);
|
||||||
|
List<PathPoly> neighborTile;
|
||||||
|
|
||||||
|
if (neighborX < 0)
|
||||||
|
{
|
||||||
|
if (leftChunk == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighborX = ChunkSize - 1;
|
||||||
|
neighborIndex = GetIndex(neighborX, neighborY);
|
||||||
|
neighborTile = leftChunk.Polygons[neighborIndex];
|
||||||
|
}
|
||||||
|
else if (neighborY < 0)
|
||||||
|
{
|
||||||
|
if (bottomChunk == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighborY = ChunkSize - 1;
|
||||||
|
neighborIndex = GetIndex(neighborX, neighborY);
|
||||||
|
neighborTile = bottomChunk.Polygons[neighborIndex];
|
||||||
|
}
|
||||||
|
else if (neighborX >= ChunkSize)
|
||||||
|
{
|
||||||
|
if (rightChunk == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighborX = 0;
|
||||||
|
neighborIndex = GetIndex(neighborX, neighborY);
|
||||||
|
neighborTile = rightChunk.Polygons[neighborIndex];
|
||||||
|
}
|
||||||
|
else if (neighborY >= ChunkSize)
|
||||||
|
{
|
||||||
|
if (topChunk == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
neighborY = 0;
|
||||||
|
neighborIndex = GetIndex(neighborX, neighborY);
|
||||||
|
neighborTile = topChunk.Polygons[neighborIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
neighborTile = chunkPolys[neighborIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (byte j = 0; j < neighborTile.Count; j++)
|
||||||
|
{
|
||||||
|
var neighbor = neighborTile[j];
|
||||||
|
var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
|
||||||
|
var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
|
||||||
|
|
||||||
|
// Need to ensure they intersect by at least 2 tiles.
|
||||||
|
if (overlap <= 0.5f / SubStep)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AddNeighbors(poly, neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _sawmill.Debug($"Built navmesh in {sw.Elapsed.TotalMilliseconds}ms");
|
||||||
|
SendPolys(chunk, component.Owner, chunkPolys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddNeighbors(PathPoly polyA, PathPoly polyB)
|
||||||
|
{
|
||||||
|
DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
|
||||||
|
DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
|
||||||
|
polyA.Neighbors.Add(polyB);
|
||||||
|
polyB.Neighbors.Add(polyA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding;
|
|
||||||
|
|
||||||
public sealed partial class PathfindingSystem
|
|
||||||
{
|
|
||||||
// TODO: Re-use the existing simplifier. Because the pathfinding API sucks I just copy-pasted for now.
|
|
||||||
public static List<TileRef> Simplify(List<TileRef> vertices, float tolerance = 0)
|
|
||||||
{
|
|
||||||
if (vertices.Count <= 3)
|
|
||||||
return vertices;
|
|
||||||
|
|
||||||
var simplified = new List<TileRef>();
|
|
||||||
|
|
||||||
for (var i = 0; i < vertices.Count; i++)
|
|
||||||
{
|
|
||||||
// No wraparound for negative sooooo
|
|
||||||
var prev = vertices[i == 0 ? vertices.Count - 1 : i - 1];
|
|
||||||
var current = vertices[i];
|
|
||||||
var next = vertices[(i + 1) % vertices.Count];
|
|
||||||
|
|
||||||
// If they collinear, continue
|
|
||||||
if (IsCollinear(in prev, in current, in next, tolerance))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
simplified.Add(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Farseer didn't seem to handle straight lines and nuked all points
|
|
||||||
if (simplified.Count == 0)
|
|
||||||
{
|
|
||||||
simplified.Add(vertices[0]);
|
|
||||||
simplified.Add(vertices[^1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return simplified;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsCollinear(in TileRef prev, in TileRef current, in TileRef next, float tolerance)
|
|
||||||
{
|
|
||||||
return FloatInRange(Area(in prev, in current, in next), -tolerance, tolerance);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float Area(in TileRef a, in TileRef b, in TileRef c)
|
|
||||||
{
|
|
||||||
return a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool FloatInRange(float value, float min, float max)
|
|
||||||
{
|
|
||||||
return (value >= min && value <= max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Content.Server.CPUJob.JobQueues;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.CPUJob.JobQueues.Queues;
|
using Content.Server.Administration.Managers;
|
||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
using Content.Server.Destructible;
|
||||||
using Content.Shared.Access.Systems;
|
using Content.Server.NPC.Components;
|
||||||
using Content.Shared.Physics;
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Physics.Systems;
|
||||||
|
using Robust.Shared.Players;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding
|
namespace Content.Server.NPC.Pathfinding
|
||||||
{
|
{
|
||||||
@@ -13,66 +24,651 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
|
/// 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)
|
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class PathfindingSystem : EntitySystem
|
public sealed partial class PathfindingSystem : SharedPathfindingSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly AccessReaderSystem _access = default!;
|
/*
|
||||||
|
* I have spent many hours looking at what pathfinding to use
|
||||||
|
* Ideally we would be able to use something grid based with hierarchy, but the problem is
|
||||||
|
* we also have triangular / diagonal walls and thindows which makes that not exactly feasible
|
||||||
|
* Recast is also overkill for our usecase, plus another lib, hence you get this.
|
||||||
|
*
|
||||||
|
* See PathfindingSystem.Grid for a description of the grid implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
private readonly PathfindingJobQueue _pathfindingQueue = new();
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly DestructibleSystem _destructible = default!;
|
||||||
|
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||||
|
|
||||||
public const int TrackedCollisionLayers = (int)
|
private ISawmill _sawmill = default!;
|
||||||
(CollisionGroup.Impassable |
|
|
||||||
CollisionGroup.MidImpassable |
|
private readonly Dictionary<ICommonSession, PathfindingDebugMode> _subscribedSessions = new();
|
||||||
CollisionGroup.LowImpassable |
|
|
||||||
CollisionGroup.HighImpassable);
|
private readonly List<PathRequest> _pathRequests = new(PathTickLimit);
|
||||||
|
|
||||||
|
private static readonly TimeSpan PathTime = TimeSpan.FromMilliseconds(3);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ask for the pathfinder to gimme somethin
|
/// How many paths we can process in a single tick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Job<Queue<TileRef>> RequestPath(PathfindingArgs pathfindingArgs, CancellationToken cancellationToken)
|
private const int PathTickLimit = 256;
|
||||||
{
|
|
||||||
var startNode = GetNode(pathfindingArgs.Start);
|
|
||||||
var endNode = GetNode(pathfindingArgs.End);
|
|
||||||
var job = new AStarPathfindingJob(0.001, startNode, endNode, pathfindingArgs, cancellationToken, EntityManager);
|
|
||||||
_pathfindingQueue.EnqueueJob(job);
|
|
||||||
|
|
||||||
return job;
|
private int _portalIndex;
|
||||||
|
private Dictionary<int, PathPortal> _portals = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
_sawmill = Logger.GetSawmill("nav");
|
||||||
|
_playerManager.PlayerStatusChanged += OnPlayerChange;
|
||||||
|
InitializeGrid();
|
||||||
|
SubscribeNetworkEvent<RequestPathfindingDebugMessage>(OnBreadcrumbs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Job<Queue<TileRef>>? RequestPath(EntityUid source, EntityUid target, CancellationToken cancellationToken)
|
public override void Shutdown()
|
||||||
{
|
{
|
||||||
var collisionMask = 0;
|
base.Shutdown();
|
||||||
|
_subscribedSessions.Clear();
|
||||||
if (TryComp<PhysicsComponent>(source, out var body))
|
_playerManager.PlayerStatusChanged -= OnPlayerChange;
|
||||||
{
|
|
||||||
collisionMask = body.CollisionMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryComp<TransformComponent>(source, out var xform) ||
|
|
||||||
!_mapManager.TryGetGrid(xform.GridUid, out var grid) ||
|
|
||||||
!TryComp<TransformComponent>(target, out var targetXform) ||
|
|
||||||
!_mapManager.TryGetGrid(targetXform.GridUid, out var targetGrid))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = grid.GetTileRef(xform.Coordinates);
|
|
||||||
var end = targetGrid.GetTileRef(targetXform.Coordinates);
|
|
||||||
|
|
||||||
var args = new PathfindingArgs(source, _access.FindAccessTags(source), collisionMask, start, end);
|
|
||||||
|
|
||||||
var startNode = GetNode(start);
|
|
||||||
var endNode = GetNode(end);
|
|
||||||
var job = new AStarPathfindingJob(0.001, startNode, endNode, args, cancellationToken, EntityManager);
|
|
||||||
_pathfindingQueue.EnqueueJob(job);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
ProcessGridUpdates();
|
UpdateGrid();
|
||||||
_pathfindingQueue.Process();
|
_stopwatch.Restart();
|
||||||
|
var amount = Math.Min(PathTickLimit, _pathRequests.Count);
|
||||||
|
var results = ArrayPool<PathResult>.Shared.Rent(amount);
|
||||||
|
|
||||||
|
Parallel.For(0, amount, i =>
|
||||||
|
{
|
||||||
|
// If we're over the limit (either time-sliced or hard cap).
|
||||||
|
if (_stopwatch.Elapsed >= PathTime)
|
||||||
|
{
|
||||||
|
results[i] = PathResult.Continuing;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = _pathRequests[i];
|
||||||
|
|
||||||
|
switch (request)
|
||||||
|
{
|
||||||
|
case AStarPathRequest astar:
|
||||||
|
results[i] = UpdateAStarPath(astar);
|
||||||
|
break;
|
||||||
|
case BFSPathRequest bfs:
|
||||||
|
results[i] = UpdateBFSPath(_random, bfs);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
// then, single-threaded cleanup.
|
||||||
|
for (var i = 0; i < amount; i++)
|
||||||
|
{
|
||||||
|
var resultIndex = i + offset;
|
||||||
|
var path = _pathRequests[resultIndex];
|
||||||
|
var result = results[i];
|
||||||
|
|
||||||
|
if (path.Task.Exception != null)
|
||||||
|
{
|
||||||
|
throw path.Task.Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case PathResult.Continuing:
|
||||||
|
break;
|
||||||
|
case PathResult.PartialPath:
|
||||||
|
case PathResult.Path:
|
||||||
|
case PathResult.NoPath:
|
||||||
|
SendDebug(path);
|
||||||
|
// Don't use RemoveSwap because we still want to try and process them in order.
|
||||||
|
_pathRequests.RemoveAt(resultIndex);
|
||||||
|
offset--;
|
||||||
|
path.Tcs.SetResult(result);
|
||||||
|
SendRoute(path);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayPool<PathResult>.Shared.Return(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates neighbouring edges at both locations, each leading to the other.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryCreatePortal(EntityCoordinates coordsA, EntityCoordinates coordsB, out int handle)
|
||||||
|
{
|
||||||
|
var mapUidA = coordsA.GetMapUid(EntityManager);
|
||||||
|
var mapUidB = coordsB.GetMapUid(EntityManager);
|
||||||
|
handle = -1;
|
||||||
|
|
||||||
|
if (mapUidA != mapUidB || mapUidA == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gridUidA = coordsA.GetGridUid(EntityManager);
|
||||||
|
var gridUidB = coordsB.GetGridUid(EntityManager);
|
||||||
|
|
||||||
|
if (!TryComp<GridPathfindingComponent>(gridUidA, out var gridA) ||
|
||||||
|
!TryComp<GridPathfindingComponent>(gridUidB, out var gridB))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = _portalIndex++;
|
||||||
|
var portal = new PathPortal(handle, coordsA, coordsB);
|
||||||
|
_portals[handle] = portal;
|
||||||
|
var originA = GetOrigin(coordsA, gridUidA.Value);
|
||||||
|
var originB = GetOrigin(coordsB, gridUidB.Value);
|
||||||
|
|
||||||
|
gridA.PortalLookup.Add(portal, originA);
|
||||||
|
gridB.PortalLookup.Add(portal, originB);
|
||||||
|
|
||||||
|
var chunkA = GetChunk(originA, gridUidA.Value);
|
||||||
|
var chunkB = GetChunk(originB, gridUidB.Value);
|
||||||
|
chunkA.Portals.Add(portal);
|
||||||
|
chunkB.Portals.Add(portal);
|
||||||
|
|
||||||
|
// TODO: You already have the chunks
|
||||||
|
DirtyChunk(gridUidA.Value, coordsA);
|
||||||
|
DirtyChunk(gridUidB.Value, coordsB);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemovePortal(int handle)
|
||||||
|
{
|
||||||
|
if (!_portals.TryGetValue(handle, out var portal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_portals.Remove(handle);
|
||||||
|
|
||||||
|
var gridUidA = portal.CoordinatesA.GetGridUid(EntityManager);
|
||||||
|
var gridUidB = portal.CoordinatesB.GetGridUid(EntityManager);
|
||||||
|
|
||||||
|
if (!TryComp<GridPathfindingComponent>(gridUidA, out var gridA) ||
|
||||||
|
!TryComp<GridPathfindingComponent>(gridUidB, out var gridB))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridA.PortalLookup.Remove(portal);
|
||||||
|
gridB.PortalLookup.Remove(portal);
|
||||||
|
var chunkA = GetChunk(GetOrigin(portal.CoordinatesA, gridUidA.Value), gridUidA.Value, gridA);
|
||||||
|
var chunkB = GetChunk(GetOrigin(portal.CoordinatesB, gridUidB.Value), gridUidB.Value, gridB);
|
||||||
|
chunkA.Portals.Remove(portal);
|
||||||
|
chunkB.Portals.Remove(portal);
|
||||||
|
DirtyChunk(gridUidA.Value, portal.CoordinatesA);
|
||||||
|
DirtyChunk(gridUidB.Value, portal.CoordinatesB);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PathResultEvent> GetRandomPath(
|
||||||
|
EntityUid entity,
|
||||||
|
float range,
|
||||||
|
float maxRange,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
int limit = 40,
|
||||||
|
PathFlags flags = PathFlags.None)
|
||||||
|
{
|
||||||
|
if (!TryComp<TransformComponent>(entity, out var start))
|
||||||
|
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
||||||
|
|
||||||
|
var layer = 0;
|
||||||
|
var mask = 0;
|
||||||
|
|
||||||
|
if (TryComp<PhysicsComponent>(entity, out var body))
|
||||||
|
{
|
||||||
|
layer = body.CollisionLayer;
|
||||||
|
mask = body.CollisionMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new BFSPathRequest(maxRange, limit, start.Coordinates, flags, range, layer, mask, cancelToken);
|
||||||
|
var path = await GetPath(request);
|
||||||
|
|
||||||
|
if (path.Result != PathResult.Path)
|
||||||
|
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
||||||
|
|
||||||
|
return new PathResultEvent(PathResult.Path, path.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the estimated distance from the entity to the target node.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<float?> GetPathDistance(
|
||||||
|
EntityUid entity,
|
||||||
|
EntityCoordinates end,
|
||||||
|
float range,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
PathFlags flags = PathFlags.None)
|
||||||
|
{
|
||||||
|
if (!TryComp<TransformComponent>(entity, out var start))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var request = GetRequest(entity, start.Coordinates, end, range, cancelToken, flags);
|
||||||
|
var path = await GetPath(request);
|
||||||
|
|
||||||
|
if (path.Result != PathResult.Path)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (path.Path.Count == 0)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
var distance = 0f;
|
||||||
|
var node = path.Path.Dequeue();
|
||||||
|
var lastNode = node;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
distance += GetTileCost(request, lastNode, node);
|
||||||
|
lastNode = node;
|
||||||
|
} while (path.Path.TryDequeue(out node));
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PathResultEvent> GetPath(
|
||||||
|
EntityUid entity,
|
||||||
|
EntityCoordinates start,
|
||||||
|
EntityCoordinates end,
|
||||||
|
float range,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
PathFlags flags = PathFlags.None)
|
||||||
|
{
|
||||||
|
var request = GetRequest(entity, start, end, range, cancelToken, flags);
|
||||||
|
return await GetPath(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously gets a path.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PathResultEvent> GetPath(
|
||||||
|
EntityCoordinates start,
|
||||||
|
EntityCoordinates end,
|
||||||
|
float range,
|
||||||
|
int layer,
|
||||||
|
int mask,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
PathFlags flags = PathFlags.None)
|
||||||
|
{
|
||||||
|
// Don't allow the caller to pass in the request in case they try to do something with its data.
|
||||||
|
var request = new AStarPathRequest(start, end, flags, range, layer, mask, cancelToken);
|
||||||
|
return await GetPath(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raises the pathfinding result event on the entity when finished.
|
||||||
|
/// </summary>
|
||||||
|
public async void GetPathEvent(
|
||||||
|
EntityUid uid,
|
||||||
|
EntityCoordinates start,
|
||||||
|
EntityCoordinates end,
|
||||||
|
float range,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
PathFlags flags = PathFlags.None)
|
||||||
|
{
|
||||||
|
var path = await GetPath(uid, start, end, range, cancelToken);
|
||||||
|
RaiseLocalEvent(uid, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relevant poly for the specified coordinates if it exists.
|
||||||
|
/// </summary>
|
||||||
|
public PathPoly? GetPoly(EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
var gridUid = coordinates.GetGridUid(EntityManager);
|
||||||
|
|
||||||
|
if (!TryComp<GridPathfindingComponent>(gridUid, out var comp) ||
|
||||||
|
!TryComp<TransformComponent>(gridUid, out var xform))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localPos = xform.InvWorldMatrix.Transform(coordinates.ToMapPos(EntityManager));
|
||||||
|
var origin = GetOrigin(localPos);
|
||||||
|
|
||||||
|
if (!TryGetChunk(origin, comp, out var chunk))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var chunkPos = new Vector2(MathHelper.Mod(localPos.X, ChunkSize), MathHelper.Mod(localPos.Y, ChunkSize));
|
||||||
|
var polys = chunk.Polygons[(int) chunkPos.X * ChunkSize + (int) chunkPos.Y];
|
||||||
|
|
||||||
|
foreach (var poly in polys)
|
||||||
|
{
|
||||||
|
if (!poly.Box.Contains(localPos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathRequest GetRequest(EntityUid entity, EntityCoordinates start, EntityCoordinates end, float range, CancellationToken cancelToken, PathFlags flags)
|
||||||
|
{
|
||||||
|
var layer = 0;
|
||||||
|
var mask = 0;
|
||||||
|
|
||||||
|
if (TryComp<PhysicsComponent>(entity, out var body))
|
||||||
|
{
|
||||||
|
layer = body.CollisionLayer;
|
||||||
|
mask = body.CollisionMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AStarPathRequest(start, end, flags, range, layer, mask, cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PathFlags GetFlags(EntityUid uid)
|
||||||
|
{
|
||||||
|
if (!TryComp<NPCComponent>(uid, out var npc))
|
||||||
|
{
|
||||||
|
return PathFlags.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetFlags(npc.Blackboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PathFlags GetFlags(NPCBlackboard blackboard)
|
||||||
|
{
|
||||||
|
var flags = PathFlags.None;
|
||||||
|
|
||||||
|
if (blackboard.TryGetValue<bool>(NPCBlackboard.NavPry, out var pry))
|
||||||
|
{
|
||||||
|
flags |= PathFlags.Prying;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blackboard.TryGetValue<bool>(NPCBlackboard.NavSmash, out var smash))
|
||||||
|
{
|
||||||
|
flags |= PathFlags.Smashing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PathResultEvent> GetPath(
|
||||||
|
PathRequest request)
|
||||||
|
{
|
||||||
|
// We could maybe try an initial quick run to avoid forcing time-slicing over ticks.
|
||||||
|
// For now it seems okay and it shouldn't block on 1 NPC anyway.
|
||||||
|
|
||||||
|
_pathRequests.Add(request);
|
||||||
|
|
||||||
|
await request.Task;
|
||||||
|
|
||||||
|
if (request.Task.Exception != null)
|
||||||
|
{
|
||||||
|
throw request.Task.Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.Task.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same context as do_after and not synchronously blocking soooo
|
||||||
|
#pragma warning disable RA0004
|
||||||
|
var ev = new PathResultEvent(request.Task.Result, request.Polys);
|
||||||
|
#pragma warning restore RA0004
|
||||||
|
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Debug handlers
|
||||||
|
|
||||||
|
private DebugPathPoly GetDebugPoly(PathPoly poly)
|
||||||
|
{
|
||||||
|
// Create fake neighbors for it
|
||||||
|
var neighbors = new List<EntityCoordinates>(poly.Neighbors.Count);
|
||||||
|
|
||||||
|
foreach (var neighbor in poly.Neighbors)
|
||||||
|
{
|
||||||
|
neighbors.Add(neighbor.Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DebugPathPoly()
|
||||||
|
{
|
||||||
|
GraphUid = poly.GraphUid,
|
||||||
|
ChunkOrigin = poly.ChunkOrigin,
|
||||||
|
TileIndex = poly.TileIndex,
|
||||||
|
Box = poly.Box,
|
||||||
|
Data = poly.Data,
|
||||||
|
Neighbors = neighbors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendDebug(PathRequest request)
|
||||||
|
{
|
||||||
|
if (_subscribedSessions.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var session in _subscribedSessions)
|
||||||
|
{
|
||||||
|
if ((session.Value & PathfindingDebugMode.Routes) == 0x0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(new PathRouteMessage(request.Polys.Select(GetDebugPoly).ToList(), new Dictionary<DebugPathPoly, float>()), session.Key.ConnectedClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBreadcrumbs(RequestPathfindingDebugMessage msg, EntitySessionEventArgs args)
|
||||||
|
{
|
||||||
|
var pSession = (IPlayerSession) args.SenderSession;
|
||||||
|
|
||||||
|
if (!_adminManager.HasAdminFlag(pSession, AdminFlags.Debug))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessions = _subscribedSessions.GetOrNew(args.SenderSession);
|
||||||
|
|
||||||
|
if (msg.Mode == PathfindingDebugMode.None)
|
||||||
|
{
|
||||||
|
_subscribedSessions.Remove(args.SenderSession);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions = msg.Mode;
|
||||||
|
_subscribedSessions[args.SenderSession] = sessions;
|
||||||
|
|
||||||
|
if (IsCrumb(sessions))
|
||||||
|
{
|
||||||
|
SendBreadcrumbs(pSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPoly(sessions))
|
||||||
|
{
|
||||||
|
SendPolys(pSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCrumb(PathfindingDebugMode mode)
|
||||||
|
{
|
||||||
|
return (mode & (PathfindingDebugMode.Breadcrumbs | PathfindingDebugMode.Crumb)) != 0x0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPoly(PathfindingDebugMode mode)
|
||||||
|
{
|
||||||
|
return (mode & (PathfindingDebugMode.Chunks | PathfindingDebugMode.Polys | PathfindingDebugMode.Poly | PathfindingDebugMode.PolyNeighbors)) != 0x0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsRoute(PathfindingDebugMode mode)
|
||||||
|
{
|
||||||
|
return (mode & (PathfindingDebugMode.Routes | PathfindingDebugMode.RouteCosts)) != 0x0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendBreadcrumbs(ICommonSession pSession)
|
||||||
|
{
|
||||||
|
var msg = new PathBreadcrumbsMessage();
|
||||||
|
|
||||||
|
foreach (var comp in EntityQuery<GridPathfindingComponent>(true))
|
||||||
|
{
|
||||||
|
msg.Breadcrumbs.Add(comp.Owner, new Dictionary<Vector2i, List<PathfindingBreadcrumb>>(comp.Chunks.Count));
|
||||||
|
|
||||||
|
foreach (var chunk in comp.Chunks)
|
||||||
|
{
|
||||||
|
var data = GetCrumbs(chunk.Value);
|
||||||
|
msg.Breadcrumbs[comp.Owner].Add(chunk.Key, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseNetworkEvent(msg, pSession.ConnectedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendRoute(PathRequest request)
|
||||||
|
{
|
||||||
|
if (_subscribedSessions.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polys = new List<DebugPathPoly>();
|
||||||
|
var costs = new Dictionary<DebugPathPoly, float>();
|
||||||
|
|
||||||
|
foreach (var poly in request.Polys)
|
||||||
|
{
|
||||||
|
polys.Add(GetDebugPoly(poly));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (poly, value) in request.CostSoFar)
|
||||||
|
{
|
||||||
|
costs.Add(GetDebugPoly(poly), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = new PathRouteMessage(polys, costs);
|
||||||
|
|
||||||
|
foreach (var session in _subscribedSessions)
|
||||||
|
{
|
||||||
|
if (!IsRoute(session.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(msg, session.Key.ConnectedClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendPolys(ICommonSession pSession)
|
||||||
|
{
|
||||||
|
var msg = new PathPolysMessage();
|
||||||
|
|
||||||
|
foreach (var comp in EntityQuery<GridPathfindingComponent>(true))
|
||||||
|
{
|
||||||
|
msg.Polys.Add(comp.Owner, new Dictionary<Vector2i, Dictionary<Vector2i, List<DebugPathPoly>>>(comp.Chunks.Count));
|
||||||
|
|
||||||
|
foreach (var chunk in comp.Chunks)
|
||||||
|
{
|
||||||
|
var data = GetPolys(chunk.Value);
|
||||||
|
msg.Polys[comp.Owner].Add(chunk.Key, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseNetworkEvent(msg, pSession.ConnectedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendBreadcrumbs(GridPathfindingChunk chunk, EntityUid gridUid)
|
||||||
|
{
|
||||||
|
if (_subscribedSessions.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = new PathBreadcrumbsRefreshMessage()
|
||||||
|
{
|
||||||
|
Origin = chunk.Origin,
|
||||||
|
GridUid = gridUid,
|
||||||
|
Data = GetCrumbs(chunk),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var session in _subscribedSessions)
|
||||||
|
{
|
||||||
|
if (!IsCrumb(session.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(msg, session.Key.ConnectedClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendPolys(GridPathfindingChunk chunk, EntityUid gridUid,
|
||||||
|
List<PathPoly>[] tilePolys)
|
||||||
|
{
|
||||||
|
if (_subscribedSessions.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var data = new Dictionary<Vector2i, List<DebugPathPoly>>(tilePolys.Length);
|
||||||
|
var extent = Math.Sqrt(tilePolys.Length);
|
||||||
|
|
||||||
|
for (var x = 0; x < extent; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < extent; y++)
|
||||||
|
{
|
||||||
|
var index = GetIndex(x, y);
|
||||||
|
data[new Vector2i(x, y)] = tilePolys[index].Select(GetDebugPoly).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = new PathPolysRefreshMessage()
|
||||||
|
{
|
||||||
|
Origin = chunk.Origin,
|
||||||
|
GridUid = gridUid,
|
||||||
|
Polys = data,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var session in _subscribedSessions)
|
||||||
|
{
|
||||||
|
if (!IsPoly(session.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RaiseNetworkEvent(msg, session.Key.ConnectedClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PathfindingBreadcrumb> GetCrumbs(GridPathfindingChunk chunk)
|
||||||
|
{
|
||||||
|
var crumbs = new List<PathfindingBreadcrumb>(chunk.Points.Length);
|
||||||
|
const int extent = ChunkSize * SubStep;
|
||||||
|
|
||||||
|
for (var x = 0; x < extent; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < extent; y++)
|
||||||
|
{
|
||||||
|
crumbs.Add(chunk.Points[x, y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<Vector2i, List<DebugPathPoly>> GetPolys(GridPathfindingChunk chunk)
|
||||||
|
{
|
||||||
|
var polys = new Dictionary<Vector2i, List<DebugPathPoly>>(chunk.Polygons.Length);
|
||||||
|
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var index = GetIndex(x, y);
|
||||||
|
polys[new Vector2i(x, y)] = chunk.Polygons[index].Select(GetDebugPoly).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerChange(object? sender, SessionStatusEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.NewStatus == SessionStatus.Connected || !_subscribedSessions.ContainsKey(e.Session))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_subscribedSessions.Remove(e.Session);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Content.Shared.AI;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Pathfinding
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class ServerPathfindingDebugSystem : EntitySystem
|
|
||||||
{
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
|
|
||||||
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.GridUid).GridTileToLocal(tile.GridIndices);
|
|
||||||
route.Add(tileGrid.ToMapPos(EntityManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
var cameFrom = new Dictionary<Vector2, Vector2>();
|
|
||||||
foreach (var (from, to) in routeDebug.CameFrom)
|
|
||||||
{
|
|
||||||
var tileOneGrid = mapManager.GetGrid(from.GridUid).GridTileToLocal(from.GridIndices);
|
|
||||||
var tileOneWorld = tileOneGrid.ToMapPos(EntityManager);
|
|
||||||
var tileTwoGrid = mapManager.GetGrid(to.GridUid).GridTileToLocal(to.GridIndices);
|
|
||||||
var tileTwoWorld = tileTwoGrid.ToMapPos(EntityManager);
|
|
||||||
cameFrom[tileOneWorld] = tileTwoWorld;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gScores = new Dictionary<Vector2, float>();
|
|
||||||
foreach (var (tile, score) in routeDebug.GScores)
|
|
||||||
{
|
|
||||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
|
||||||
gScores[tileGrid.ToMapPos(EntityManager)] = score;
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemMessage = new SharedAiDebug.AStarRouteMessage(
|
|
||||||
routeDebug.EntityUid,
|
|
||||||
route,
|
|
||||||
cameFrom,
|
|
||||||
gScores,
|
|
||||||
routeDebug.TimeTaken
|
|
||||||
);
|
|
||||||
|
|
||||||
RaiseNetworkEvent(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.GridUid).GridTileToLocal(tile.GridIndices);
|
|
||||||
route.Add(tileGrid.ToMapPos(EntityManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
var jumpNodes = new List<Vector2>();
|
|
||||||
foreach (var tile in routeDebug.JumpNodes)
|
|
||||||
{
|
|
||||||
var tileGrid = mapManager.GetGrid(tile.GridUid).GridTileToLocal(tile.GridIndices);
|
|
||||||
jumpNodes.Add(tileGrid.ToMapPos(EntityManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemMessage = new SharedAiDebug.JpsRouteMessage(
|
|
||||||
routeDebug.EntityUid,
|
|
||||||
route,
|
|
||||||
jumpNodes,
|
|
||||||
routeDebug.TimeTaken
|
|
||||||
);
|
|
||||||
|
|
||||||
RaiseNetworkEvent(systemMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,14 @@ using Content.Server.NPC.Components;
|
|||||||
using Content.Shared.MobState;
|
using Content.Shared.MobState;
|
||||||
using Content.Shared.MobState.Components;
|
using Content.Shared.MobState.Components;
|
||||||
using Content.Shared.Weapons.Melee;
|
using Content.Shared.Weapons.Melee;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
namespace Content.Server.NPC.Systems;
|
namespace Content.Server.NPC.Systems;
|
||||||
|
|
||||||
public sealed partial class NPCCombatSystem
|
public sealed partial class NPCCombatSystem
|
||||||
{
|
{
|
||||||
|
private const float TargetMeleeLostRange = 14f;
|
||||||
|
|
||||||
private void InitializeMelee()
|
private void InitializeMelee()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentStartup>(OnMeleeStartup);
|
SubscribeLocalEvent<NPCMeleeCombatComponent, ComponentStartup>(OnMeleeStartup);
|
||||||
@@ -20,6 +23,8 @@ public sealed partial class NPCCombatSystem
|
|||||||
{
|
{
|
||||||
combatMode.IsInCombatMode = false;
|
combatMode.IsInCombatMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_steering.Unregister(component.Owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMeleeStartup(EntityUid uid, NPCMeleeCombatComponent component, ComponentStartup args)
|
private void OnMeleeStartup(EntityUid uid, NPCMeleeCombatComponent component, ComponentStartup args)
|
||||||
@@ -54,8 +59,7 @@ public sealed partial class NPCCombatSystem
|
|||||||
{
|
{
|
||||||
component.Status = CombatStatus.Normal;
|
component.Status = CombatStatus.Normal;
|
||||||
|
|
||||||
// TODO: Also need to co-ordinate with steering to keep in range.
|
// TODO:
|
||||||
// For now I've just moved the utlity version over.
|
|
||||||
// Also need some blackboard data for stuff like juke frequency, assigning target slots (to surround targets), etc.
|
// Also need some blackboard data for stuff like juke frequency, assigning target slots (to surround targets), etc.
|
||||||
// miss %
|
// miss %
|
||||||
if (!TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
|
if (!TryComp<MeleeWeaponComponent>(component.Weapon, out var weapon))
|
||||||
@@ -64,11 +68,6 @@ public sealed partial class NPCCombatSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (weapon.NextAttack > _timing.CurTime)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!xformQuery.TryGetComponent(component.Owner, out var xform) ||
|
if (!xformQuery.TryGetComponent(component.Owner, out var xform) ||
|
||||||
!xformQuery.TryGetComponent(component.Target, out var targetXform))
|
!xformQuery.TryGetComponent(component.Target, out var targetXform))
|
||||||
{
|
{
|
||||||
@@ -76,14 +75,26 @@ public sealed partial class NPCCombatSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!xform.Coordinates.TryDistance(EntityManager, targetXform.Coordinates, out var distance) ||
|
if (!xform.Coordinates.TryDistance(EntityManager, targetXform.Coordinates, out var distance))
|
||||||
distance > weapon.Range)
|
|
||||||
{
|
{
|
||||||
// TODO: Steering in combat.
|
|
||||||
component.Status = CombatStatus.TargetUnreachable;
|
component.Status = CombatStatus.TargetUnreachable;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (distance > TargetMeleeLostRange)
|
||||||
|
{
|
||||||
|
component.Status = CombatStatus.TargetUnreachable;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance > weapon.Range)
|
||||||
|
{
|
||||||
|
component.Status = CombatStatus.TargetOutOfRange;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets unregistered on component shutdown.
|
||||||
|
_steering.TryRegister(component.Owner, new EntityCoordinates(component.Target, Vector2.Zero));
|
||||||
_melee.AttemptLightAttack(component.Owner, weapon, component.Target);
|
_melee.AttemptLightAttack(component.Owner, weapon, component.Target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ namespace Content.Server.NPC.Systems;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class NPCCombatSystem : EntitySystem
|
public sealed partial class NPCCombatSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
[Dependency] private readonly GunSystem _gun = default!;
|
[Dependency] private readonly GunSystem _gun = default!;
|
||||||
[Dependency] private readonly InteractionSystem _interaction = default!;
|
[Dependency] private readonly InteractionSystem _interaction = default!;
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly NPCSteeringSystem _steering = default!;
|
||||||
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
|
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
|
||||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.NPC.Components;
|
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Movement.Components;
|
|
||||||
using Robust.Shared.Collections;
|
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Physics.Collision.Shapes;
|
|
||||||
|
|
||||||
namespace Content.Server.NPC.Systems;
|
namespace Content.Server.NPC.Systems;
|
||||||
|
|
||||||
|
|||||||
157
Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
Normal file
157
Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using Content.Server.Destructible;
|
||||||
|
using Content.Server.NPC.Components;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
|
using Content.Shared.Doors.Components;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Content.Shared.Weapons.Melee;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Systems;
|
||||||
|
|
||||||
|
public sealed partial class NPCSteeringSystem
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* For any custom path handlers, e.g. destroying walls, opening airlocks, etc.
|
||||||
|
* Putting it onto steering seemed easier than trying to make a custom compound task for it.
|
||||||
|
* I also considered task interrupts although the problem is handling stuff like pathfinding overlaps
|
||||||
|
* Ideally we could do interrupts but that's TODO.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO:
|
||||||
|
* - Add path cap
|
||||||
|
* - Circle cast BFS in LOS to determine targets.
|
||||||
|
* - Store last known coordinates of X targets.
|
||||||
|
* - Require line of sight for melee
|
||||||
|
* - Add new behavior where they move to melee target's last known position (diffing theirs and current)
|
||||||
|
* then do the thing like from dishonored where it gets passed to a search system that opens random stuff.
|
||||||
|
*
|
||||||
|
* Also need to make sure it picks nearest obstacle path so it starts smashing in front of it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
private SteeringObstacleStatus TryHandleFlags(NPCSteeringComponent component, PathPoly poly, EntityQuery<PhysicsComponent> bodyQuery)
|
||||||
|
{
|
||||||
|
if (poly.Data.IsFreeSpace)
|
||||||
|
return SteeringObstacleStatus.Completed;
|
||||||
|
|
||||||
|
if (!bodyQuery.TryGetComponent(component.Owner, out var body))
|
||||||
|
return SteeringObstacleStatus.Failed;
|
||||||
|
|
||||||
|
// TODO: Store PathFlags on the steering comp
|
||||||
|
// and be able to re-check it.
|
||||||
|
|
||||||
|
// TODO: Should cache the fact we're doing this somewhere.
|
||||||
|
// See https://github.com/space-wizards/space-station-14/issues/11475
|
||||||
|
if ((poly.Data.CollisionLayer & body.CollisionMask) != 0x0 ||
|
||||||
|
(poly.Data.CollisionMask & body.CollisionLayer) != 0x0)
|
||||||
|
{
|
||||||
|
var obstacleEnts = new List<EntityUid>();
|
||||||
|
|
||||||
|
GetObstacleEntities(poly, body.CollisionMask, body.CollisionLayer, bodyQuery, obstacleEnts);
|
||||||
|
var isDoor = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
|
||||||
|
var isAccess = (poly.Data.Flags & PathfindingBreadcrumbFlag.Access) != 0x0;
|
||||||
|
|
||||||
|
// Just walk into it stupid
|
||||||
|
if (isDoor && !isAccess)
|
||||||
|
{
|
||||||
|
var doorQuery = GetEntityQuery<DoorComponent>();
|
||||||
|
|
||||||
|
// ... At least if it's not a bump open.
|
||||||
|
foreach (var ent in obstacleEnts)
|
||||||
|
{
|
||||||
|
if (!doorQuery.TryGetComponent(ent, out var door))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!door.BumpOpen)
|
||||||
|
{
|
||||||
|
if (door.State != DoorState.Opening)
|
||||||
|
{
|
||||||
|
_interaction.InteractionActivate(component.Owner, ent);
|
||||||
|
return SteeringObstacleStatus.Continuing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SteeringObstacleStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((component.Flags & PathFlags.Prying) != 0x0 && isAccess && isDoor)
|
||||||
|
{
|
||||||
|
var doorQuery = GetEntityQuery<DoorComponent>();
|
||||||
|
|
||||||
|
// Get the relevant obstacle
|
||||||
|
foreach (var ent in obstacleEnts)
|
||||||
|
{
|
||||||
|
if (doorQuery.TryGetComponent(ent, out var door) && door.State != DoorState.Open)
|
||||||
|
{
|
||||||
|
// TODO: Use the verb.
|
||||||
|
if (door.State != DoorState.Opening && !door.BeingPried)
|
||||||
|
_doors.TryPryDoor(ent, component.Owner, component.Owner, door, true);
|
||||||
|
|
||||||
|
return SteeringObstacleStatus.Continuing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obstacleEnts.Count == 0)
|
||||||
|
return SteeringObstacleStatus.Completed;
|
||||||
|
}
|
||||||
|
// Try smashing obstacles.
|
||||||
|
else if ((component.Flags & PathFlags.Smashing) != 0x0 && TryComp<NPCMeleeCombatComponent>(component.Owner, out var melee) &&
|
||||||
|
TryComp<MeleeWeaponComponent>(melee.Weapon, out var meleeWeapon))
|
||||||
|
{
|
||||||
|
var destructibleQuery = GetEntityQuery<DestructibleComponent>();
|
||||||
|
|
||||||
|
// TODO: This is a hack around grilles and windows.
|
||||||
|
_random.Shuffle(obstacleEnts);
|
||||||
|
|
||||||
|
foreach (var ent in obstacleEnts)
|
||||||
|
{
|
||||||
|
// TODO: Validate we can damage it
|
||||||
|
if (destructibleQuery.HasComponent(ent))
|
||||||
|
{
|
||||||
|
_melee.AttemptLightAttack(component.Owner, meleeWeapon, ent);
|
||||||
|
return SteeringObstacleStatus.Continuing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obstacleEnts.Count == 0)
|
||||||
|
return SteeringObstacleStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SteeringObstacleStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SteeringObstacleStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetObstacleEntities(PathPoly poly, int mask, int layer, EntityQuery<PhysicsComponent> bodyQuery,
|
||||||
|
List<EntityUid> ents)
|
||||||
|
{
|
||||||
|
// TODO: Can probably re-use this from pathfinding or something
|
||||||
|
if (!_mapManager.TryGetGrid(poly.GraphUid, out var grid))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ent in grid.GetLocalAnchoredEntities(poly.Box))
|
||||||
|
{
|
||||||
|
if (!bodyQuery.TryGetComponent(ent, out var body) ||
|
||||||
|
!body.Hard ||
|
||||||
|
!body.CanCollide ||
|
||||||
|
(body.CollisionMask & layer) == 0x0 && (body.CollisionLayer & mask) == 0x0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ents.Add(ent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SteeringObstacleStatus : byte
|
||||||
|
{
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Continuing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Content.Server.CPUJob.JobQueues;
|
using Content.Server.Doors.Systems;
|
||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
using Content.Server.NPC.Pathfinding;
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Content.Server.NPC.Pathfinding.Pathfinders;
|
|
||||||
using Content.Shared.Access.Systems;
|
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Movement.Components;
|
using Content.Shared.Movement.Components;
|
||||||
|
using Content.Shared.Movement.Systems;
|
||||||
|
using Content.Shared.NPC;
|
||||||
|
using Content.Shared.Weapons.Melee;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Physics.Systems;
|
using Robust.Shared.Physics.Systems;
|
||||||
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.NPC.Systems
|
namespace Content.Server.NPC.Systems
|
||||||
@@ -22,18 +24,22 @@ namespace Content.Server.NPC.Systems
|
|||||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly DoorSystem _doors = default!;
|
||||||
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
|
[Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
|
||||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||||
|
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
|
||||||
|
[Dependency] private readonly SharedMoverController _mover = default!;
|
||||||
|
|
||||||
// This will likely get moved onto an abstract pathfinding node that specifies the max distance allowed from the coordinate.
|
// This will likely get moved onto an abstract pathfinding node that specifies the max distance allowed from the coordinate.
|
||||||
private const float TileTolerance = 0.4f;
|
private const float TileTolerance = 0.40f;
|
||||||
|
|
||||||
private bool _enabled;
|
private bool _enabled;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
|
||||||
InitializeAvoidance();
|
InitializeAvoidance();
|
||||||
_configManager.OnValueChanged(CCVars.NPCEnabled, SetNPCEnabled, true);
|
_configManager.OnValueChanged(CCVars.NPCEnabled, SetNPCEnabled, true);
|
||||||
|
|
||||||
@@ -68,22 +74,37 @@ namespace Content.Server.NPC.Systems
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the AI to the steering system to move towards a specific target
|
/// Adds the AI to the steering system to move towards a specific target
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates)
|
public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
|
||||||
{
|
{
|
||||||
if (TryComp<NPCSteeringComponent>(uid, out var comp))
|
if (Resolve(uid, ref component, false))
|
||||||
{
|
{
|
||||||
comp.PathfindToken?.Cancel();
|
component.PathfindToken?.Cancel();
|
||||||
comp.PathfindToken = null;
|
component.PathfindToken = null;
|
||||||
comp.CurrentPath.Clear();
|
component.CurrentPath.Clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
comp = AddComp<NPCSteeringComponent>(uid);
|
component = AddComp<NPCSteeringComponent>(uid);
|
||||||
|
component.Flags = _pathfindingSystem.GetFlags(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureComp<NPCRVOComponent>(uid);
|
EnsureComp<NPCRVOComponent>(uid);
|
||||||
comp.Coordinates = coordinates;
|
component.Coordinates = coordinates;
|
||||||
return comp;
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to register the entity. Does nothing if the coordinates already registered.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryRegister(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
|
||||||
|
{
|
||||||
|
if (Resolve(uid, ref component, false) && component.Coordinates.Equals(coordinates))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Register(uid, coordinates, component);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -101,7 +122,6 @@ namespace Content.Server.NPC.Systems
|
|||||||
|
|
||||||
component.PathfindToken?.Cancel();
|
component.PathfindToken?.Cancel();
|
||||||
component.PathfindToken = null;
|
component.PathfindToken = null;
|
||||||
component.Pathfind = null;
|
|
||||||
RemComp<NPCRVOComponent>(uid);
|
RemComp<NPCRVOComponent>(uid);
|
||||||
RemComp<NPCSteeringComponent>(uid);
|
RemComp<NPCSteeringComponent>(uid);
|
||||||
}
|
}
|
||||||
@@ -120,15 +140,21 @@ namespace Content.Server.NPC.Systems
|
|||||||
var npcs = EntityQuery<NPCSteeringComponent, ActiveNPCComponent, InputMoverComponent, TransformComponent>()
|
var npcs = EntityQuery<NPCSteeringComponent, ActiveNPCComponent, InputMoverComponent, TransformComponent>()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// TODO: Do this in parallel. This will require pathfinder refactor to not use jobqueue.
|
// TODO: Do this in parallel.
|
||||||
|
// Main obstacle is requesting a new path needs to be done synchronously
|
||||||
foreach (var (steering, _, mover, xform) in npcs)
|
foreach (var (steering, _, mover, xform) in npcs)
|
||||||
{
|
{
|
||||||
Steer(steering, mover, xform, modifierQuery, bodyQuery, frameTime);
|
Steer(steering, mover, xform, modifierQuery, bodyQuery, frameTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetDirection(InputMoverComponent component, Vector2 value)
|
private void SetDirection(InputMoverComponent component, NPCSteeringComponent steering, Vector2 value, bool clear = true)
|
||||||
{
|
{
|
||||||
|
if (clear && value.Equals(Vector2.Zero))
|
||||||
|
{
|
||||||
|
steering.CurrentPath.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
component.CurTickSprintMovement = value;
|
component.CurTickSprintMovement = value;
|
||||||
component.LastInputTick = _timing.CurTick;
|
component.LastInputTick = _timing.CurTick;
|
||||||
component.LastInputSubTick = ushort.MaxValue;
|
component.LastInputSubTick = ushort.MaxValue;
|
||||||
@@ -145,6 +171,13 @@ namespace Content.Server.NPC.Systems
|
|||||||
EntityQuery<PhysicsComponent> bodyQuery,
|
EntityQuery<PhysicsComponent> bodyQuery,
|
||||||
float frameTime)
|
float frameTime)
|
||||||
{
|
{
|
||||||
|
if (Deleted(steering.Coordinates.EntityId))
|
||||||
|
{
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var ourCoordinates = xform.Coordinates;
|
var ourCoordinates = xform.Coordinates;
|
||||||
var destinationCoordinates = steering.Coordinates;
|
var destinationCoordinates = steering.Coordinates;
|
||||||
|
|
||||||
@@ -152,54 +185,46 @@ namespace Content.Server.NPC.Systems
|
|||||||
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var distance) &&
|
if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var distance) &&
|
||||||
distance <= steering.Range)
|
distance <= steering.Range)
|
||||||
{
|
{
|
||||||
SetDirection(mover, Vector2.Zero);
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
steering.Status = SteeringStatus.InRange;
|
steering.Status = SteeringStatus.InRange;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No path set from pathfinding or the likes.
|
||||||
|
if (steering.Status == SteeringStatus.NoPath)
|
||||||
|
{
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Can't move at all, just noop input.
|
// Can't move at all, just noop input.
|
||||||
if (!mover.CanMove)
|
if (!mover.CanMove)
|
||||||
{
|
{
|
||||||
SetDirection(mover, Vector2.Zero);
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
steering.Status = SteeringStatus.Moving;
|
steering.Status = SteeringStatus.Moving;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we were pathfinding then try to update our path.
|
// Grab the target position, either the next path node or our end goal.
|
||||||
if (steering.Pathfind != null)
|
// TODO: Some situations we may not want to move at our target without a path.
|
||||||
|
var targetCoordinates = GetTargetCoordinates(steering);
|
||||||
|
var needsPath = false;
|
||||||
|
|
||||||
|
// If the next node is invalid then get new ones
|
||||||
|
if (!targetCoordinates.IsValid(EntityManager))
|
||||||
{
|
{
|
||||||
switch (steering.Pathfind.Status)
|
if (steering.CurrentPath.TryPeek(out var poly) &&
|
||||||
|
(poly.Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0)
|
||||||
{
|
{
|
||||||
case JobStatus.Waiting:
|
steering.CurrentPath.Dequeue();
|
||||||
case JobStatus.Running:
|
// Try to get the next node temporarily.
|
||||||
case JobStatus.Pending:
|
targetCoordinates = GetTargetCoordinates(steering);
|
||||||
case JobStatus.Paused:
|
needsPath = true;
|
||||||
break;
|
|
||||||
case JobStatus.Finished:
|
|
||||||
steering.CurrentPath.Clear();
|
|
||||||
|
|
||||||
if (steering.Pathfind.Result != null)
|
|
||||||
{
|
|
||||||
PrunePath(ourCoordinates, steering.Pathfind.Result);
|
|
||||||
|
|
||||||
foreach (var node in steering.Pathfind.Result)
|
|
||||||
{
|
|
||||||
steering.CurrentPath.Enqueue(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
steering.Pathfind = null;
|
|
||||||
steering.PathfindToken = null;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the target position, either the path or our end goal.
|
// Need to be pretty close if it's just a node to make sure LOS for door bashes or the likes.
|
||||||
// TODO: Some situations we may not want to move at our target without a path.
|
float arrivalDistance;
|
||||||
var targetCoordinates = GetTargetCoordinates(steering);
|
|
||||||
var arrivalDistance = TileTolerance;
|
|
||||||
|
|
||||||
if (targetCoordinates.Equals(steering.Coordinates))
|
if (targetCoordinates.Equals(steering.Coordinates))
|
||||||
{
|
{
|
||||||
@@ -207,6 +232,10 @@ namespace Content.Server.NPC.Systems
|
|||||||
// If it's a pathfinding node it might be different to the destination.
|
// If it's a pathfinding node it might be different to the destination.
|
||||||
arrivalDistance = steering.Range;
|
arrivalDistance = steering.Range;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arrivalDistance = SharedInteractionSystem.InteractionRange - 0.8f;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if mapids match.
|
// Check if mapids match.
|
||||||
var targetMap = targetCoordinates.ToMap(EntityManager);
|
var targetMap = targetCoordinates.ToMap(EntityManager);
|
||||||
@@ -214,73 +243,87 @@ namespace Content.Server.NPC.Systems
|
|||||||
|
|
||||||
if (targetMap.MapId != ourMap.MapId)
|
if (targetMap.MapId != ourMap.MapId)
|
||||||
{
|
{
|
||||||
SetDirection(mover, Vector2.Zero);
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
steering.Status = SteeringStatus.NoPath;
|
steering.Status = SteeringStatus.NoPath;
|
||||||
steering.CurrentTarget = targetCoordinates;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var direction = targetMap.Position - ourMap.Position;
|
var direction = targetMap.Position - ourMap.Position;
|
||||||
|
|
||||||
|
if (steering.Owner == new EntityUid(15315))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Are we in range
|
// Are we in range
|
||||||
if (direction.Length <= arrivalDistance)
|
if (direction.Length <= arrivalDistance)
|
||||||
{
|
{
|
||||||
// It was just a node, not the target, so grab the next destination (either the target or next node).
|
// Node needs some kind of special handling like access or smashing.
|
||||||
if (steering.CurrentPath.Count > 0)
|
if (steering.CurrentPath.TryPeek(out var node))
|
||||||
{
|
{
|
||||||
steering.CurrentPath.Dequeue();
|
var status = TryHandleFlags(steering, node, bodyQuery);
|
||||||
|
|
||||||
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
|
// TODO: Need to handle re-pathing in case the target moves around.
|
||||||
// TODO: If it's the last node just grab the target instead.
|
switch (status)
|
||||||
targetCoordinates = GetTargetCoordinates(steering);
|
|
||||||
targetMap = targetCoordinates.ToMap(EntityManager);
|
|
||||||
|
|
||||||
// Can't make it again.
|
|
||||||
if (ourMap.MapId != targetMap.MapId)
|
|
||||||
{
|
{
|
||||||
SetDirection(mover, Vector2.Zero);
|
case SteeringObstacleStatus.Completed:
|
||||||
steering.Status = SteeringStatus.NoPath;
|
break;
|
||||||
steering.CurrentTarget = targetCoordinates;
|
case SteeringObstacleStatus.Failed:
|
||||||
|
// TODO: Blacklist the poly for next query
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
return;
|
||||||
|
case SteeringObstacleStatus.Continuing:
|
||||||
|
SetDirection(mover, steering, Vector2.Zero, false);
|
||||||
|
CheckPath(steering, xform, needsPath, distance);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it's probably regular pathing so just keep going a bit more to get to tile centre
|
||||||
|
if (direction.Length <= TileTolerance)
|
||||||
|
{
|
||||||
|
// It was just a node, not the target, so grab the next destination (either the target or next node).
|
||||||
|
if (steering.CurrentPath.Count > 0)
|
||||||
|
{
|
||||||
|
steering.CurrentPath.Dequeue();
|
||||||
|
|
||||||
|
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
|
||||||
|
// TODO: If it's the last node just grab the target instead.
|
||||||
|
targetCoordinates = GetTargetCoordinates(steering);
|
||||||
|
targetMap = targetCoordinates.ToMap(EntityManager);
|
||||||
|
|
||||||
|
// Can't make it again.
|
||||||
|
if (ourMap.MapId != targetMap.MapId)
|
||||||
|
{
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gonna resume now business as usual
|
||||||
|
direction = targetMap.Position - ourMap.Position;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This probably shouldn't happen as we check above but eh.
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
steering.Status = SteeringStatus.InRange;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gonna resume now business as usual
|
|
||||||
direction = targetMap.Position - ourMap.Position;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This probably shouldn't happen as we check above but eh.
|
|
||||||
SetDirection(mover, Vector2.Zero);
|
|
||||||
steering.Status = SteeringStatus.InRange;
|
|
||||||
steering.CurrentTarget = targetCoordinates;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we have no more nodes to follow OR has the target moved sufficiently? If so then re-path.
|
// Do we have no more nodes to follow OR has the target moved sufficiently? If so then re-path.
|
||||||
var needsPath = steering.CurrentPath.Count == 0;
|
|
||||||
|
|
||||||
// TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
|
|
||||||
|
|
||||||
if (!needsPath)
|
if (!needsPath)
|
||||||
{
|
{
|
||||||
var lastNode = steering.CurrentPath.Last();
|
needsPath = steering.CurrentPath.Count == 0 || (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
|
||||||
// I know this is bad and doesn't account for tile size
|
|
||||||
// However with the path I'm going to change it to return pathfinding nodes which include coordinates instead.
|
|
||||||
var lastCoordinate = new EntityCoordinates(lastNode.GridUid, (Vector2) lastNode.GridIndices + 0.5f);
|
|
||||||
|
|
||||||
if (lastCoordinate.TryDistance(EntityManager, steering.Coordinates, out var lastDistance) &&
|
|
||||||
lastDistance > steering.RepathRange)
|
|
||||||
{
|
|
||||||
needsPath = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request the new path.
|
// TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
|
||||||
if (needsPath && bodyQuery.TryGetComponent(steering.Owner, out var body))
|
CheckPath(steering, xform, needsPath, distance);
|
||||||
{
|
|
||||||
RequestPath(steering, xform, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
modifierQuery.TryGetComponent(steering.Owner, out var modifier);
|
modifierQuery.TryGetComponent(steering.Owner, out var modifier);
|
||||||
var moveSpeed = GetSprintSpeed(steering.Owner, modifier);
|
var moveSpeed = GetSprintSpeed(steering.Owner, modifier);
|
||||||
@@ -293,9 +336,8 @@ namespace Content.Server.NPC.Systems
|
|||||||
|
|
||||||
if (tickMovement.Equals(0f))
|
if (tickMovement.Equals(0f))
|
||||||
{
|
{
|
||||||
SetDirection(mover, Vector2.Zero);
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
steering.Status = SteeringStatus.NoPath;
|
steering.Status = SteeringStatus.NoPath;
|
||||||
steering.CurrentTarget = targetCoordinates;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,45 +349,59 @@ namespace Content.Server.NPC.Systems
|
|||||||
input *= maxDistance / tickMovement;
|
input *= maxDistance / tickMovement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This isn't going to work for space.
|
// We have the input in world terms but need to convert it back to what movercontroller is doing.
|
||||||
if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
input = (-_mover.GetParentGridAngle(mover)).RotateVec(input);
|
||||||
|
SetDirection(mover, steering, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckPath(NPCSteeringComponent steering, TransformComponent xform, bool needsPath, float targetDistance)
|
||||||
|
{
|
||||||
|
if (!needsPath)
|
||||||
{
|
{
|
||||||
input = (-grid.WorldRotation).RotateVec(input);
|
// If the target has sufficiently moved.
|
||||||
|
var lastNode = GetCoordinates(steering.CurrentPath.Last());
|
||||||
|
|
||||||
|
if (lastNode.TryDistance(EntityManager, steering.Coordinates, out var lastDistance) &&
|
||||||
|
lastDistance > steering.RepathRange)
|
||||||
|
{
|
||||||
|
needsPath = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetDirection(mover, input);
|
// Request the new path.
|
||||||
steering.CurrentTarget = targetCoordinates;
|
if (needsPath)
|
||||||
|
{
|
||||||
|
RequestPath(steering, xform, targetDistance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
|
/// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="coordinates">Our coordinates we are pruning from</param>
|
public void PrunePath(MapCoordinates mapCoordinates, Vector2 direction, Queue<PathPoly> nodes)
|
||||||
/// <param name="nodes">Path we're pruning</param>
|
|
||||||
public void PrunePath(EntityCoordinates coordinates, Queue<TileRef> nodes)
|
|
||||||
{
|
{
|
||||||
if (nodes.Count == 0)
|
if (nodes.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Right now the pathfinder gives EVERY TILE back but ideally it won't someday, it'll just give straightline ones.
|
// Prune the first node as it's irrelevant.
|
||||||
// For now, we just prune up until the closest node + 1 extra.
|
nodes.Dequeue();
|
||||||
var closest = ((Vector2) nodes.Peek().GridIndices + 0.5f - coordinates.Position).Length;
|
|
||||||
// TODO: Need to handle multi-grid and stuff.
|
|
||||||
|
|
||||||
while (nodes.TryPeek(out var node))
|
while (nodes.TryPeek(out var node))
|
||||||
{
|
{
|
||||||
// TODO: Tile size
|
if (!node.Data.IsFreeSpace)
|
||||||
var nodePosition = (Vector2) node.GridIndices + 0.5f;
|
break;
|
||||||
var length = (coordinates.Position - nodePosition).Length;
|
|
||||||
|
|
||||||
if (length < closest)
|
var nodeMap = node.Coordinates.ToMap(EntityManager);
|
||||||
|
|
||||||
|
// If any nodes are 'behind us' relative to the target we'll prune them.
|
||||||
|
// This isn't perfect but should fix most cases of stutter stepping.
|
||||||
|
if (nodeMap.MapId == mapCoordinates.MapId &&
|
||||||
|
Vector2.Dot(direction, nodeMap.Position - mapCoordinates.Position) < 0f)
|
||||||
{
|
{
|
||||||
closest = length;
|
|
||||||
nodes.Dequeue();
|
nodes.Dequeue();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.Dequeue();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,44 +416,62 @@ namespace Content.Server.NPC.Systems
|
|||||||
// Even if we're at the last node may not be able to head to target in case we get stuck on a corner or the likes.
|
// Even if we're at the last node may not be able to head to target in case we get stuck on a corner or the likes.
|
||||||
if (steering.CurrentPath.Count >= 1 && steering.CurrentPath.TryPeek(out var nextTarget))
|
if (steering.CurrentPath.Count >= 1 && steering.CurrentPath.TryPeek(out var nextTarget))
|
||||||
{
|
{
|
||||||
return new EntityCoordinates(nextTarget.GridUid, (Vector2) nextTarget.GridIndices + 0.5f);
|
return GetCoordinates(nextTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return steering.Coordinates;
|
return steering.Coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EntityCoordinates GetCoordinates(PathPoly poly)
|
||||||
|
{
|
||||||
|
if (!poly.IsValid())
|
||||||
|
return EntityCoordinates.Invalid;
|
||||||
|
|
||||||
|
return new EntityCoordinates(poly.GraphUid, poly.Box.Center);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a new job from the pathfindingsystem
|
/// Get a new job from the pathfindingsystem
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RequestPath(NPCSteeringComponent steering, TransformComponent xform, PhysicsComponent? body)
|
private async void RequestPath(NPCSteeringComponent steering, TransformComponent xform, float targetDistance)
|
||||||
{
|
{
|
||||||
// If we already have a pathfinding request then don't grab another.
|
// If we already have a pathfinding request then don't grab another.
|
||||||
if (steering.Pathfind != null)
|
// If we're in range then just beeline them; this can avoid stutter stepping and is an easy way to look nicer.
|
||||||
return;
|
if (steering.Pathfind || targetDistance < steering.RepathRange)
|
||||||
|
|
||||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
steering.PathfindToken = new CancellationTokenSource();
|
steering.PathfindToken = new CancellationTokenSource();
|
||||||
var startTile = grid.GetTileRef(xform.Coordinates);
|
|
||||||
var endTile = grid.GetTileRef(steering.Coordinates);
|
|
||||||
var collisionMask = 0;
|
|
||||||
|
|
||||||
if (body != null)
|
var flags = _pathfindingSystem.GetFlags(steering.Owner);
|
||||||
|
|
||||||
|
var result = await _pathfindingSystem.GetPath(
|
||||||
|
steering.Owner,
|
||||||
|
xform.Coordinates,
|
||||||
|
steering.Coordinates,
|
||||||
|
steering.Range,
|
||||||
|
steering.PathfindToken.Token,
|
||||||
|
flags);
|
||||||
|
|
||||||
|
if (result.Result == PathResult.NoPath)
|
||||||
{
|
{
|
||||||
collisionMask = body.CollisionMask;
|
steering.CurrentPath.Clear();
|
||||||
|
steering.PathfindToken = null;
|
||||||
|
steering.FailedPathCount++;
|
||||||
|
|
||||||
|
if (steering.FailedPathCount >= NPCSteeringComponent.FailedPathLimit)
|
||||||
|
{
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var access = _accessReader.FindAccessTags(steering.Owner);
|
var targetPos = steering.Coordinates.ToMap(EntityManager);
|
||||||
|
var ourPos = xform.MapPosition;
|
||||||
|
|
||||||
steering.Pathfind = _pathfindingSystem.RequestPath(new PathfindingArgs(
|
PrunePath(ourPos, targetPos.Position - ourPos.Position, result.Path);
|
||||||
steering.Owner,
|
steering.CurrentPath = result.Path;
|
||||||
access,
|
steering.PathfindToken = null;
|
||||||
collisionMask,
|
|
||||||
startTile,
|
|
||||||
endTile,
|
|
||||||
steering.Range
|
|
||||||
), steering.PathfindToken.Token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move these to movercontroller
|
// TODO: Move these to movercontroller
|
||||||
|
|||||||
@@ -30,5 +30,8 @@ namespace Content.Server.Shuttles.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField("highlightedRadarColor")]
|
[ViewVariables(VVAccess.ReadWrite), DataField("highlightedRadarColor")]
|
||||||
public Color HighlightedRadarColor = Color.Magenta;
|
public Color HighlightedRadarColor = Color.Magenta;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public int PathfindHandle = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Content.Server.Doors.Components;
|
using Content.Server.Doors.Components;
|
||||||
using Content.Server.Doors.Systems;
|
using Content.Server.Doors.Systems;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
using Content.Server.Shuttles.Components;
|
using Content.Server.Shuttles.Components;
|
||||||
using Content.Server.Shuttles.Events;
|
using Content.Server.Shuttles.Events;
|
||||||
using Content.Shared.Doors;
|
using Content.Shared.Doors;
|
||||||
@@ -24,6 +25,7 @@ namespace Content.Server.Shuttles.Systems
|
|||||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||||
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
|
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
|
||||||
[Dependency] private readonly DoorSystem _doorSystem = default!;
|
[Dependency] private readonly DoorSystem _doorSystem = default!;
|
||||||
|
[Dependency] private readonly PathfindingSystem _pathfinding = default!;
|
||||||
|
|
||||||
private ISawmill _sawmill = default!;
|
private ISawmill _sawmill = default!;
|
||||||
private const string DockingFixture = "docking";
|
private const string DockingFixture = "docking";
|
||||||
@@ -136,6 +138,7 @@ namespace Content.Server.Shuttles.Systems
|
|||||||
|
|
||||||
private void Cleanup(DockingComponent dockA)
|
private void Cleanup(DockingComponent dockA)
|
||||||
{
|
{
|
||||||
|
_pathfinding.RemovePortal(dockA.PathfindHandle);
|
||||||
_jointSystem.RemoveJoint(dockA.DockJoint!);
|
_jointSystem.RemoveJoint(dockA.DockJoint!);
|
||||||
|
|
||||||
var dockBUid = dockA.DockedWith;
|
var dockBUid = dockA.DockedWith;
|
||||||
@@ -369,6 +372,12 @@ namespace Content.Server.Shuttles.Systems
|
|||||||
_doorSystem.StartOpening(doorB.Owner, doorB);
|
_doorSystem.StartOpening(doorB.Owner, doorB);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pathfinding.TryCreatePortal(dockAXform.Coordinates, dockBXform.Coordinates, out var handle))
|
||||||
|
{
|
||||||
|
dockA.PathfindHandle = handle;
|
||||||
|
dockB.PathfindHandle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
var msg = new DockEvent
|
var msg = new DockEvent
|
||||||
{
|
{
|
||||||
DockA = dockA,
|
DockA = dockA,
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.AI
|
|
||||||
{
|
|
||||||
public static class SharedAiDebug
|
|
||||||
{
|
|
||||||
#region Mob Debug
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class UtilityAiDebugMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public EntityUid EntityUid { get; }
|
|
||||||
public double PlanningTime { get; }
|
|
||||||
public float ActionScore { get; }
|
|
||||||
public string FoundTask { get; }
|
|
||||||
public int ConsideredTaskCount { get; }
|
|
||||||
|
|
||||||
public UtilityAiDebugMessage(
|
|
||||||
EntityUid entityUid,
|
|
||||||
double planningTime,
|
|
||||||
float actionScore,
|
|
||||||
string foundTask,
|
|
||||||
int consideredTaskCount)
|
|
||||||
{
|
|
||||||
EntityUid = entityUid;
|
|
||||||
PlanningTime = planningTime;
|
|
||||||
ActionScore = actionScore;
|
|
||||||
FoundTask = foundTask;
|
|
||||||
ConsideredTaskCount = consideredTaskCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#region Pathfinder Debug
|
|
||||||
/// <summary>
|
|
||||||
/// Client asks the server for the pathfinding graph details
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class RequestPathfindingGraphMessage : EntityEventArgs {}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class PathfindingGraphMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public Dictionary<int, List<Vector2>> Graph { get; }
|
|
||||||
|
|
||||||
public PathfindingGraphMessage(Dictionary<int, List<Vector2>> graph)
|
|
||||||
{
|
|
||||||
Graph = graph;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class AStarRouteDebug
|
|
||||||
{
|
|
||||||
public EntityUid EntityUid { get; }
|
|
||||||
public Queue<TileRef> Route { get; }
|
|
||||||
public Dictionary<TileRef, TileRef> CameFrom { get; }
|
|
||||||
public Dictionary<TileRef, float> GScores { get; }
|
|
||||||
public double TimeTaken { get; }
|
|
||||||
|
|
||||||
public AStarRouteDebug(
|
|
||||||
EntityUid uid,
|
|
||||||
Queue<TileRef> route,
|
|
||||||
Dictionary<TileRef, TileRef> cameFrom,
|
|
||||||
Dictionary<TileRef, float> gScores,
|
|
||||||
double timeTaken)
|
|
||||||
{
|
|
||||||
EntityUid = uid;
|
|
||||||
Route = route;
|
|
||||||
CameFrom = cameFrom;
|
|
||||||
GScores = gScores;
|
|
||||||
TimeTaken = timeTaken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class JpsRouteDebug
|
|
||||||
{
|
|
||||||
public EntityUid EntityUid { get; }
|
|
||||||
public Queue<TileRef> Route { get; }
|
|
||||||
public HashSet<TileRef> JumpNodes { get; }
|
|
||||||
public double TimeTaken { get; }
|
|
||||||
|
|
||||||
public JpsRouteDebug(
|
|
||||||
EntityUid uid,
|
|
||||||
Queue<TileRef> route,
|
|
||||||
HashSet<TileRef> jumpNodes,
|
|
||||||
double timeTaken)
|
|
||||||
{
|
|
||||||
EntityUid = uid;
|
|
||||||
Route = route;
|
|
||||||
JumpNodes = jumpNodes;
|
|
||||||
TimeTaken = timeTaken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class AStarRouteMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public readonly EntityUid EntityUid;
|
|
||||||
public readonly IEnumerable<Vector2> Route;
|
|
||||||
public readonly Dictionary<Vector2, Vector2> CameFrom;
|
|
||||||
public readonly Dictionary<Vector2, float> GScores;
|
|
||||||
public double TimeTaken;
|
|
||||||
|
|
||||||
public AStarRouteMessage(
|
|
||||||
EntityUid uid,
|
|
||||||
IEnumerable<Vector2> route,
|
|
||||||
Dictionary<Vector2, Vector2> cameFrom,
|
|
||||||
Dictionary<Vector2, float> gScores,
|
|
||||||
double timeTaken)
|
|
||||||
{
|
|
||||||
EntityUid = uid;
|
|
||||||
Route = route;
|
|
||||||
CameFrom = cameFrom;
|
|
||||||
GScores = gScores;
|
|
||||||
TimeTaken = timeTaken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class JpsRouteMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public readonly EntityUid EntityUid;
|
|
||||||
public readonly IEnumerable<Vector2> Route;
|
|
||||||
public readonly List<Vector2> JumpNodes;
|
|
||||||
public double TimeTaken;
|
|
||||||
|
|
||||||
public JpsRouteMessage(
|
|
||||||
EntityUid uid,
|
|
||||||
IEnumerable<Vector2> route,
|
|
||||||
List<Vector2> jumpNodes,
|
|
||||||
double timeTaken)
|
|
||||||
{
|
|
||||||
EntityUid = uid;
|
|
||||||
Route = route;
|
|
||||||
JumpNodes = jumpNodes;
|
|
||||||
TimeTaken = timeTaken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#region Reachable Debug
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class ReachableChunkRegionsDebugMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public EntityUid GridId { get; }
|
|
||||||
public Dictionary<int, Dictionary<int, List<Vector2>>> Regions { get; }
|
|
||||||
|
|
||||||
public ReachableChunkRegionsDebugMessage(EntityUid gridId, Dictionary<int, Dictionary<int, List<Vector2>>> regions)
|
|
||||||
{
|
|
||||||
GridId = gridId;
|
|
||||||
Regions = regions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class ReachableCacheDebugMessage : EntityEventArgs
|
|
||||||
{
|
|
||||||
public EntityUid GridId { get; }
|
|
||||||
public Dictionary<int, List<Vector2>> Regions { get; }
|
|
||||||
public bool Cached { get; }
|
|
||||||
|
|
||||||
public ReachableCacheDebugMessage(EntityUid gridId, Dictionary<int, List<Vector2>> regions, bool cached)
|
|
||||||
{
|
|
||||||
GridId = gridId;
|
|
||||||
Regions = regions;
|
|
||||||
Cached = cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send if someone is subscribing to reachable regions for NPCs.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class SubscribeReachableMessage : EntityEventArgs {}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send if someone is unsubscribing to reachable regions for NPCs.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class UnsubscribeReachableMessage : EntityEventArgs {}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -494,7 +494,7 @@ namespace Content.Shared.CCVar
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public static readonly CVarDef<int> NPCMaxUpdates =
|
public static readonly CVarDef<int> NPCMaxUpdates =
|
||||||
CVarDef.Create("npc.max_updates", 64);
|
CVarDef.Create("npc.max_updates", 128);
|
||||||
|
|
||||||
public static readonly CVarDef<bool> NPCEnabled = CVarDef.Create("npc.enabled", true);
|
public static readonly CVarDef<bool> NPCEnabled = CVarDef.Create("npc.enabled", true);
|
||||||
|
|
||||||
|
|||||||
23
Content.Shared/NPC/Events/PathBreadcrumbsMessage.cs
Normal file
23
Content.Shared/NPC/Events/PathBreadcrumbsMessage.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PathBreadcrumbsMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public Dictionary<EntityUid, Dictionary<Vector2i, List<PathfindingBreadcrumb>>> Breadcrumbs = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PathBreadcrumbsRefreshMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid GridUid;
|
||||||
|
public Vector2i Origin;
|
||||||
|
public List<PathfindingBreadcrumb> Data = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PathPolysMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public Dictionary<EntityUid, Dictionary<Vector2i, Dictionary<Vector2i, List<DebugPathPoly>>>> Polys = new();
|
||||||
|
}
|
||||||
15
Content.Shared/NPC/Events/PathPolysRefreshMessage.cs
Normal file
15
Content.Shared/NPC/Events/PathPolysRefreshMessage.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PathPolysRefreshMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid GridUid;
|
||||||
|
public Vector2i Origin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multi-dimension arrays aren't supported so
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<Vector2i, List<DebugPathPoly>> Polys = new();
|
||||||
|
}
|
||||||
19
Content.Shared/NPC/Events/PathRouteMessage.cs
Normal file
19
Content.Shared/NPC/Events/PathRouteMessage.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debug message containing a pathfinding route.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PathRouteMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public List<DebugPathPoly> Path;
|
||||||
|
public Dictionary<DebugPathPoly, float> Costs;
|
||||||
|
|
||||||
|
public PathRouteMessage(List<DebugPathPoly> path, Dictionary<DebugPathPoly, float> costs)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Costs = costs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class RequestPathfindingDebugMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public PathfindingDebugMode Mode;
|
||||||
|
}
|
||||||
32
Content.Shared/NPC/PathPoly.cs
Normal file
32
Content.Shared/NPC/PathPoly.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* I bikeshedded a lot on how to do this and I'm still not entirely happy.
|
||||||
|
* The main thing is you need a weak ref to the poly because it may be invalidated due to graph updates.
|
||||||
|
* I had a struct version but you still need to store the neighbors somewhere, maybe on the chunk itself?
|
||||||
|
* Future dev work required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A path poly to be used for networked debug purposes.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class DebugPathPoly
|
||||||
|
{
|
||||||
|
public EntityUid GraphUid;
|
||||||
|
public Vector2i ChunkOrigin;
|
||||||
|
public byte TileIndex;
|
||||||
|
|
||||||
|
public Box2 Box;
|
||||||
|
public PathfindingData Data;
|
||||||
|
public List<EntityCoordinates> Neighbors = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class DebugPathPolyNeighbor
|
||||||
|
{
|
||||||
|
public EntityCoordinates Coordinates;
|
||||||
|
}
|
||||||
23
Content.Shared/NPC/PathfindingBoundary.cs
Normal file
23
Content.Shared/NPC/PathfindingBoundary.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Boundary around a navigation region.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public struct PathfindingBoundary
|
||||||
|
{
|
||||||
|
public List<PathfindingBreadcrumb> Breadcrumbs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is it a closed loop or is it a special-case chain (e.g. thindows).
|
||||||
|
/// </summary>
|
||||||
|
public bool Closed;
|
||||||
|
|
||||||
|
public PathfindingBoundary(bool closed, List<PathfindingBreadcrumb> crumbs)
|
||||||
|
{
|
||||||
|
Closed = closed;
|
||||||
|
Breadcrumbs = crumbs;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
Content.Shared/NPC/PathfindingBreadcrumb.cs
Normal file
118
Content.Shared/NPC/PathfindingBreadcrumb.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public struct PathfindingBreadcrumb : IEquatable<PathfindingBreadcrumb>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The X and Y index in the point grid.
|
||||||
|
/// The actual coordinates require using <see cref="SharedPathfindingSystem.ChunkSize"/> and <see cref="SharedPathfindingSystem.SubStep"/>
|
||||||
|
/// </summary>
|
||||||
|
public Vector2i Coordinates;
|
||||||
|
|
||||||
|
public PathfindingData Data;
|
||||||
|
|
||||||
|
public static readonly PathfindingBreadcrumb Invalid = new()
|
||||||
|
{
|
||||||
|
Data = new PathfindingData(PathfindingBreadcrumbFlag.None, -1, -1, 0f),
|
||||||
|
};
|
||||||
|
|
||||||
|
public PathfindingBreadcrumb(Vector2i coordinates, int layer, int mask, float damage, PathfindingBreadcrumbFlag flags = PathfindingBreadcrumbFlag.None)
|
||||||
|
{
|
||||||
|
Coordinates = coordinates;
|
||||||
|
Data = new PathfindingData(flags, layer, mask, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this crumb equal for pathfinding region purposes.
|
||||||
|
/// </summary>
|
||||||
|
public bool Equivalent(PathfindingBreadcrumb other)
|
||||||
|
{
|
||||||
|
return Data.Equals(other.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(PathfindingBreadcrumb other)
|
||||||
|
{
|
||||||
|
return Coordinates.Equals(other.Coordinates) && Data.Equals(other.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return obj is PathfindingBreadcrumb other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(Coordinates, Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data relevant for pathfinding.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public struct PathfindingData : IEquatable<PathfindingData>
|
||||||
|
{
|
||||||
|
public PathfindingBreadcrumbFlag Flags;
|
||||||
|
public int CollisionLayer;
|
||||||
|
public int CollisionMask;
|
||||||
|
public float Damage;
|
||||||
|
|
||||||
|
public bool IsFreeSpace => (Flags == PathfindingBreadcrumbFlag.None && Damage.Equals(0f));
|
||||||
|
|
||||||
|
public PathfindingData(PathfindingBreadcrumbFlag flag, int layer, int mask, float damage)
|
||||||
|
{
|
||||||
|
Flags = flag;
|
||||||
|
CollisionLayer = layer;
|
||||||
|
CollisionMask = mask;
|
||||||
|
Damage = damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEquivalent(PathfindingData other)
|
||||||
|
{
|
||||||
|
return CollisionLayer.Equals(other.CollisionLayer) &&
|
||||||
|
CollisionMask.Equals(other.CollisionMask) &&
|
||||||
|
Flags.Equals(other.Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(PathfindingData other)
|
||||||
|
{
|
||||||
|
return CollisionLayer.Equals(other.CollisionLayer) &&
|
||||||
|
CollisionMask.Equals(other.CollisionMask) &&
|
||||||
|
Flags.Equals(other.Flags) &&
|
||||||
|
Damage.Equals(other.Damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return obj is PathfindingData other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine((int) Flags, CollisionLayer, CollisionMask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum PathfindingBreadcrumbFlag : ushort
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has this poly been replaced and is it no longer valid.
|
||||||
|
/// </summary>
|
||||||
|
Invalid = 1 << 0,
|
||||||
|
Space = 1 << 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is there a door that is potentially pryable
|
||||||
|
/// </summary>
|
||||||
|
Door = 1 << 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is there access required
|
||||||
|
/// </summary>
|
||||||
|
Access = 1 << 3,
|
||||||
|
}
|
||||||
46
Content.Shared/NPC/PathfindingDebugMode.cs
Normal file
46
Content.Shared/NPC/PathfindingDebugMode.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum PathfindingDebugMode : ushort
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show the individual pathfinding breadcrumbs.
|
||||||
|
/// </summary>
|
||||||
|
Breadcrumbs = 1 << 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show the pathfinding chunk edges.
|
||||||
|
/// </summary>
|
||||||
|
Chunks = 1 << 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the stats nearest crumb to the mouse cursor.
|
||||||
|
/// </summary>
|
||||||
|
Crumb = 1 << 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows all of the pathfinding polys.
|
||||||
|
/// </summary>
|
||||||
|
Polys = 1 << 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the edges between pathfinding polys.
|
||||||
|
/// </summary>
|
||||||
|
PolyNeighbors = 1 << 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the nearest poly to the mouse cursor.
|
||||||
|
/// </summary>
|
||||||
|
Poly = 1 << 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a path from the current attached entity to the mouse cursor.
|
||||||
|
/// </summary>
|
||||||
|
Path = 1 << 9,
|
||||||
|
|
||||||
|
Routes = 1 << 10,
|
||||||
|
|
||||||
|
RouteCosts = 1 << 11,
|
||||||
|
}
|
||||||
22
Content.Shared/NPC/SharedPathfindingSystem.cs
Normal file
22
Content.Shared/NPC/SharedPathfindingSystem.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Content.Shared.NPC;
|
||||||
|
|
||||||
|
public abstract class SharedPathfindingSystem : EntitySystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is equivalent to agent radii for navmeshes. In our case it's preferable that things are cleanly
|
||||||
|
/// divisible per tile so we'll make sure it works as a discrete number.
|
||||||
|
/// </summary>
|
||||||
|
public const byte SubStep = 4;
|
||||||
|
|
||||||
|
public const byte ChunkSize = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We won't do points on edges so we'll offset them slightly.
|
||||||
|
/// </summary>
|
||||||
|
protected const float StepOffset = 1f / SubStep / 2f;
|
||||||
|
|
||||||
|
public Vector2 GetCoordinate(Vector2i chunk, Vector2i index)
|
||||||
|
{
|
||||||
|
return new Vector2(index.X, index.Y) / SubStep+ (chunk) * ChunkSize + StepOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Content.Shared.CombatMode;
|
|||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Weapons.Melee.Events;
|
using Content.Shared.Weapons.Melee.Events;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|||||||
1
Resources/Locale/en-US/doors/door.ftl
Normal file
1
Resources/Locale/en-US/doors/door.ftl
Normal file
@@ -0,0 +1 @@
|
|||||||
|
door-pry = Pry door
|
||||||
@@ -14,8 +14,13 @@
|
|||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: HTN
|
- type: HTN
|
||||||
rootTask: XenoCompound
|
rootTask: XenoCompound
|
||||||
|
blackboard:
|
||||||
|
NavPry: !type:Bool
|
||||||
|
true
|
||||||
|
NavSmash: !type:Bool
|
||||||
|
true
|
||||||
- type: Tool
|
- type: Tool
|
||||||
speed: 0.3
|
speed: 1.5
|
||||||
qualities:
|
qualities:
|
||||||
- Prying
|
- Prying
|
||||||
useSound:
|
useSound:
|
||||||
@@ -69,7 +74,7 @@
|
|||||||
animation: WeaponArcClaw
|
animation: WeaponArcClaw
|
||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Brute: 20
|
Brute: 12
|
||||||
- type: Appearance
|
- type: Appearance
|
||||||
- type: DamageStateVisuals
|
- type: DamageStateVisuals
|
||||||
rotate: true
|
rotate: true
|
||||||
@@ -232,7 +237,7 @@
|
|||||||
hidden: true
|
hidden: true
|
||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Brute: 35
|
Brute: 20
|
||||||
- type: SlowOnDamage
|
- type: SlowOnDamage
|
||||||
speedModifierThresholds:
|
speedModifierThresholds:
|
||||||
450: 0.7
|
450: 0.7
|
||||||
@@ -270,7 +275,7 @@
|
|||||||
hidden: true
|
hidden: true
|
||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Brute: 15
|
Brute: 8
|
||||||
- type: SlowOnDamage
|
- type: SlowOnDamage
|
||||||
speedModifierThresholds:
|
speedModifierThresholds:
|
||||||
200: 0.7
|
200: 0.7
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- shape:
|
- shape:
|
||||||
!type:PhysShapeAabb
|
!type:PhysShapeAabb
|
||||||
bounds: "-0.49,-0.49,0.49,-0.45"
|
bounds: "-0.49,-0.39,0.49,-0.36"
|
||||||
mass: 50
|
mass: 50
|
||||||
mask:
|
mask:
|
||||||
- TabletopMachineMask
|
- TabletopMachineMask
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
fixtures:
|
fixtures:
|
||||||
- shape:
|
- shape:
|
||||||
!type:PhysShapeAabb
|
!type:PhysShapeAabb
|
||||||
bounds: "-0.49,-0.49,0.49,-0.45"
|
bounds: "-0.49,-0.39,0.49,-0.36"
|
||||||
mass: 50
|
mass: 50
|
||||||
mask:
|
mask:
|
||||||
- FullTileMask
|
- FullTileMask
|
||||||
|
|||||||
Reference in New Issue
Block a user