From f456ad911e62fe8b20a90f82cd16b3c634fb8391 Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Fri, 30 Sep 2022 14:39:48 +1000
Subject: [PATCH] Pathfinder rework (#11452)
---
Content.Client/Commands/DebugAiCommand.cs | 61 --
.../Commands/DebugPathfindingCommand.cs | 81 +-
Content.Client/NPC/ClientAiDebugSystem.cs | 197 ----
.../NPC/ClientPathfindingDebugSystem.cs | 520 ----------
Content.Client/NPC/NPCWindow.xaml | 6 +-
Content.Client/NPC/NPCWindow.xaml.cs | 26 +-
Content.Client/NPC/PathfindingSystem.cs | 526 ++++++++++
.../JobQueues/Queues/AiActionJobQueue.cs | 4 -
Content.Server/Doors/Systems/DoorSystem.cs | 4 +-
.../NPC/Components/NPCMeleeCombatComponent.cs | 5 +
.../NPC/Components/NPCSteeringComponent.cs | 25 +-
Content.Server/NPC/HTN/HTNComponent.cs | 2 +
Content.Server/NPC/HTN/HTNPlanJob.cs | 2 +-
Content.Server/NPC/HTN/HTNSystem.cs | 3 +-
.../NPC/HTN/PrimitiveTasks/HTNOperator.cs | 8 +-
.../Operators/Melee/MeleeOperator.cs | 6 +-
.../Melee/PickMeleeTargetOperator.cs | 20 +-
.../Operators/MoveToOperator.cs | 50 +-
.../Operators/NPCCombatOperator.cs | 68 +-
.../PickAccessibleComponentOperator.cs | 46 +-
.../Operators/PickAccessibleOperator.cs | 47 +-
.../Operators/PickRandomRotationOperator.cs | 4 +-
.../Operators/RandomOperator.cs | 4 +-
.../Ranged/PickRangedTargetOperator.cs | 25 +-
.../Operators/Ranged/RangedOperator.cs | 4 +-
.../Operators/SetFloatOperator.cs | 4 +-
.../Specific/PickNearbyInjectableOperator.cs | 4 +-
.../Test/PickPathfindPointOperator.cs | 4 +-
Content.Server/NPC/NPCBlackboard.cs | 10 +
.../Accessible/AiReachableSystem.cs | 789 ---------------
.../Pathfinding/Accessible/BFSPathfinder.cs | 67 --
.../Accessible/PathfindingRegion.cs | 138 ---
.../Pathfinding/Accessible/ReachableArgs.cs | 39 -
.../NPC/Pathfinding/GridPathfindingChunk.cs | 33 +
.../Pathfinding/GridPathfindingComponent.cs | 22 +-
Content.Server/NPC/Pathfinding/PathFlags.cs | 22 +
Content.Server/NPC/Pathfinding/PathPoly.cs | 64 ++
Content.Server/NPC/Pathfinding/PathPortal.cs | 30 +
Content.Server/NPC/Pathfinding/PathRequest.cs | 110 +++
Content.Server/NPC/Pathfinding/PathResult.cs | 9 +
.../Pathfinders/AStarPathfindingJob.cs | 172 ----
.../Pathfinders/JpsPathfindingJob.cs | 512 ----------
.../Pathfinders/PathfindingArgs.cs | 43 -
.../Pathfinders/PathfindingComparer.cs | 10 -
.../NPC/Pathfinding/PathfindingChunk.cs | 185 ----
.../NPC/Pathfinding/PathfindingHelpers.cs | 350 -------
.../NPC/Pathfinding/PathfindingNode.cs | 317 ------
.../Pathfinding/PathfindingRequestEvent.cs | 11 +
.../Pathfinding/PathfindingSystem.AStar.cs | 143 +++
.../NPC/Pathfinding/PathfindingSystem.BFS.cs | 121 +++
.../Pathfinding/PathfindingSystem.Common.cs | 151 +++
.../Pathfinding/PathfindingSystem.Distance.cs | 46 +
.../NPC/Pathfinding/PathfindingSystem.Grid.cs | 928 +++++++++++++-----
.../PathfindingSystem.Simplifier.cs | 53 -
.../NPC/Pathfinding/PathfindingSystem.cs | 696 ++++++++++++-
.../ServerPathfindingDebugSystem.cs | 92 --
.../NPC/Systems/NPCCombatSystem.Melee.cs | 31 +-
Content.Server/NPC/Systems/NPCCombatSystem.cs | 2 +-
.../Systems/NPCSteeringSystem.Avoidance.cs | 6 -
.../Systems/NPCSteeringSystem.Obstacles.cs | 157 +++
.../NPC/Systems/NPCSteeringSystem.cs | 344 ++++---
.../Shuttles/Components/DockingComponent.cs | 3 +
.../Shuttles/Systems/DockingSystem.cs | 9 +
Content.Shared/AI/SharedAiDebug.cs | 181 ----
Content.Shared/CCVar/CCVars.cs | 2 +-
Content.Shared/NPC/{ => Events}/HTNMessage.cs | 0
.../NPC/Events/PathBreadcrumbsMessage.cs | 23 +
.../NPC/Events/PathPolysRefreshMessage.cs | 15 +
Content.Shared/NPC/Events/PathRouteMessage.cs | 19 +
.../Events/RequestPathfindingDebugMessage.cs | 9 +
Content.Shared/NPC/PathPoly.cs | 32 +
Content.Shared/NPC/PathfindingBoundary.cs | 23 +
Content.Shared/NPC/PathfindingBreadcrumb.cs | 118 +++
Content.Shared/NPC/PathfindingDebugMode.cs | 46 +
Content.Shared/NPC/SharedPathfindingSystem.cs | 22 +
.../Weapons/Melee/SharedMeleeWeaponSystem.cs | 1 +
Resources/Locale/en-US/doors/door.ftl | 1 +
.../Prototypes/Entities/Mobs/NPCs/xeno.yml | 13 +-
.../Doors/Windoors/base_structurewindoors.yml | 2 +-
.../Entities/Structures/Windows/window.yml | 2 +-
80 files changed, 3606 insertions(+), 4374 deletions(-)
delete mode 100644 Content.Client/Commands/DebugAiCommand.cs
delete mode 100644 Content.Client/NPC/ClientAiDebugSystem.cs
delete mode 100644 Content.Client/NPC/ClientPathfindingDebugSystem.cs
create mode 100644 Content.Client/NPC/PathfindingSystem.cs
delete mode 100644 Content.Server/CPUJob/JobQueues/Queues/AiActionJobQueue.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs
create mode 100644 Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathFlags.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathPoly.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathPortal.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathRequest.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathResult.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Pathfinders/AStarPathfindingJob.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Pathfinders/PathfindingArgs.cs
delete mode 100644 Content.Server/NPC/Pathfinding/Pathfinders/PathfindingComparer.cs
delete mode 100644 Content.Server/NPC/Pathfinding/PathfindingChunk.cs
delete mode 100644 Content.Server/NPC/Pathfinding/PathfindingHelpers.cs
delete mode 100644 Content.Server/NPC/Pathfinding/PathfindingNode.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathfindingRequestEvent.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathfindingSystem.AStar.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathfindingSystem.BFS.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
create mode 100644 Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs
delete mode 100644 Content.Server/NPC/Pathfinding/PathfindingSystem.Simplifier.cs
delete mode 100644 Content.Server/NPC/Pathfinding/ServerPathfindingDebugSystem.cs
create mode 100644 Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
delete mode 100644 Content.Shared/AI/SharedAiDebug.cs
rename Content.Shared/NPC/{ => Events}/HTNMessage.cs (100%)
create mode 100644 Content.Shared/NPC/Events/PathBreadcrumbsMessage.cs
create mode 100644 Content.Shared/NPC/Events/PathPolysRefreshMessage.cs
create mode 100644 Content.Shared/NPC/Events/PathRouteMessage.cs
create mode 100644 Content.Shared/NPC/Events/RequestPathfindingDebugMessage.cs
create mode 100644 Content.Shared/NPC/PathPoly.cs
create mode 100644 Content.Shared/NPC/PathfindingBoundary.cs
create mode 100644 Content.Shared/NPC/PathfindingBreadcrumb.cs
create mode 100644 Content.Shared/NPC/PathfindingDebugMode.cs
create mode 100644 Content.Shared/NPC/SharedPathfindingSystem.cs
create mode 100644 Resources/Locale/en-US/doors/door.ftl
diff --git a/Content.Client/Commands/DebugAiCommand.cs b/Content.Client/Commands/DebugAiCommand.cs
deleted file mode 100644
index e35dd21387..0000000000
--- a/Content.Client/Commands/DebugAiCommand.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Content.Client.NPC;
-using JetBrains.Annotations;
-using Robust.Shared.Console;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.Commands
-{
- ///
- /// This is used to handle the tooltips above AI mobs
- ///
- [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();
-
- 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
- }
- }
-}
diff --git a/Content.Client/Commands/DebugPathfindingCommand.cs b/Content.Client/Commands/DebugPathfindingCommand.cs
index 69738559df..9071ea40a7 100644
--- a/Content.Client/Commands/DebugPathfindingCommand.cs
+++ b/Content.Client/Commands/DebugPathfindingCommand.cs
@@ -1,74 +1,61 @@
+using System.Linq;
using Content.Client.NPC;
+using Content.Shared.NPC;
using JetBrains.Annotations;
using Robust.Shared.Console;
-using Robust.Shared.GameObjects;
namespace Content.Client.Commands
{
[UsedImplicitly]
- internal sealed class DebugPathfindingCommand : IConsoleCommand
+ public sealed class DebugPathfindingCommand : IConsoleCommand
{
// ReSharper disable once StringLiteralTypo
public string Command => "pathfinder";
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)
{
-#if DEBUG
- if (args.Length < 1)
+ var system = IoCManager.Resolve().GetEntitySystem();
+
+ if (args.Length == 0)
{
- shell.RemoteExecuteCommand(argStr);
+ system.Modes = PathfindingDebugMode.None;
return;
}
- var anyAction = false;
- var debugSystem = EntitySystem.Get();
-
foreach (var arg in args)
{
- switch (arg)
+ if (!Enum.TryParse(arg, out var mode))
{
- case "hide":
- debugSystem.Disable();
- 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;
+ shell.WriteError($"Unrecognised pathfinder args {arg}");
+ 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)
- shell.RemoteExecuteCommand(argStr);
-#else
- shell.RemoteExecuteCommand(argStr);
-#endif
+ var values = Enum.GetValues().ToList();
+ var options = new List();
+
+ foreach (var val in values)
+ {
+ if (val == PathfindingDebugMode.None)
+ continue;
+
+ options.Add(new CompletionOption(val.ToString()));
+ }
+
+ return CompletionResult.FromOptions(options);
}
}
}
diff --git a/Content.Client/NPC/ClientAiDebugSystem.cs b/Content.Client/NPC/ClientAiDebugSystem.cs
deleted file mode 100644
index 5f807d1398..0000000000
--- a/Content.Client/NPC/ClientAiDebugSystem.cs
+++ /dev/null
@@ -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 _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(0);
- foreach (var (entity, panel) in _aiBoxes)
- {
- if (Deleted(entity))
- {
- deletedEntities.Add(entity);
- continue;
- }
-
- if (!_eyeManager.GetWorldViewport().Contains(EntityManager.GetComponent(entity).WorldPosition))
- {
- panel.Visible = false;
- continue;
- }
-
- var (x, y) = _eyeManager.CoordinatesToScreen(EntityManager.GetComponent(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(HandleUtilityAiDebugMessage);
- SubscribeNetworkEvent(HandleAStarRouteMessage);
- SubscribeNetworkEvent(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();
-
- 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,
- }
-}
diff --git a/Content.Client/NPC/ClientPathfindingDebugSystem.cs b/Content.Client/NPC/ClientPathfindingDebugSystem.cs
deleted file mode 100644
index 7c1de6105e..0000000000
--- a/Content.Client/NPC/ClientPathfindingDebugSystem.cs
+++ /dev/null
@@ -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(HandleAStarRouteMessage);
- SubscribeNetworkEvent(HandleJpsRouteMessage);
- SubscribeNetworkEvent(HandleGraphMessage);
- SubscribeNetworkEvent(HandleRegionsMessage);
- SubscribeNetworkEvent(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();
- _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();
- 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> Graph = new();
- private readonly Dictionary _graphColors = new();
-
- // Cached regions
- public readonly Dictionary>> CachedRegions =
- new();
-
- private readonly Dictionary> _cachedRegionColors =
- new();
-
- // Regions
- public readonly Dictionary>>> Regions =
- new();
-
- private readonly Dictionary>> _regionColors =
- new();
-
- // Route debugging
- // As each pathfinder is very different you'll likely want to draw them completely different
- public readonly List AStarRoutes = new();
- public readonly List JpsRoutes = new();
-
- public DebugPathfindingOverlay(IEntityManager entities, IEyeManager eyeManager, IPlayerManager playerManager, IPrototypeManager prototypeManager)
- {
- _entities = entities;
- _eyeManager = eyeManager;
- _playerManager = playerManager;
- _shader = prototypeManager.Index("unshaded").Instance();
- }
-
- #region Graph
- public void UpdateGraph(Dictionary> graph)
- {
- Graph.Clear();
- _graphColors.Clear();
- var robustRandom = IoCManager.Resolve();
- 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> messageRegions, bool cached)
- {
- if (!CachedRegions.ContainsKey(gridId))
- {
- CachedRegions.Add(gridId, new Dictionary>());
- _cachedRegionColors.Add(gridId, new Dictionary());
- }
-
- 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(_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>> messageRegions)
- {
- if (!Regions.ContainsKey(gridId))
- {
- Regions.Add(gridId, new Dictionary>>());
- _regionColors.Add(gridId, new Dictionary>());
- }
-
- var robustRandom = IoCManager.Resolve();
- foreach (var (chunk, regions) in messageRegions)
- {
- Regions[gridId][chunk] = new Dictionary>();
- _regionColors[gridId][chunk] = new Dictionary();
-
- 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,
- }
-}
diff --git a/Content.Client/NPC/NPCWindow.xaml b/Content.Client/NPC/NPCWindow.xaml
index 29a2a75737..664f15600c 100644
--- a/Content.Client/NPC/NPCWindow.xaml
+++ b/Content.Client/NPC/NPCWindow.xaml
@@ -15,9 +15,11 @@
-
+
+
+
+
-
diff --git a/Content.Client/NPC/NPCWindow.xaml.cs b/Content.Client/NPC/NPCWindow.xaml.cs
index 772da9c377..968c761cc1 100644
--- a/Content.Client/NPC/NPCWindow.xaml.cs
+++ b/Content.Client/NPC/NPCWindow.xaml.cs
@@ -1,4 +1,5 @@
using Content.Client.UserInterface.Controls;
+using Content.Shared.NPC;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
@@ -12,21 +13,18 @@ public sealed partial class NPCWindow : FancyWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var sysManager = IoCManager.Resolve();
- var debugSys = sysManager.GetEntitySystem();
- var path = sysManager.GetEntitySystem();
+ var path = sysManager.GetEntitySystem();
- NPCPath.Pressed = (debugSys.Tooltips & AiDebugMode.Paths) != 0x0;
- NPCThonk.Pressed = (debugSys.Tooltips & AiDebugMode.Thonk) != 0x0;
+ PathCrumbs.Pressed = (path.Modes & PathfindingDebugMode.Breadcrumbs) != 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);
- NPCThonk.OnToggled += args => debugSys.ToggleTooltip(AiDebugMode.Thonk);
-
- PathNodes.Pressed = (path.Modes & PathfindingDebugMode.Nodes) != 0x0;
- PathRegions.Pressed = (path.Modes & PathfindingDebugMode.Regions) != 0x0;
- 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);
+ PathCrumbs.OnToggled += args => path.Modes ^= PathfindingDebugMode.Breadcrumbs;
+ PathPolys.OnToggled += args => path.Modes ^= PathfindingDebugMode.Polys;
+ PathNeighbors.OnToggled += args => path.Modes ^= PathfindingDebugMode.PolyNeighbors;
+ PathRouteCosts.OnToggled += args => path.Modes ^= PathfindingDebugMode.RouteCosts;
+ PathRoutes.OnToggled += args => path.Modes ^= PathfindingDebugMode.Routes;
}
}
diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
new file mode 100644
index 0000000000..3d5fdf85cc
--- /dev/null
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -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();
+
+ if (value == PathfindingDebugMode.None)
+ {
+ Breadcrumbs.Clear();
+ Polys.Clear();
+ overlayManager.RemoveOverlay();
+ }
+ else if (!overlayManager.HasOverlay())
+ {
+ 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>> Breadcrumbs = new();
+ public Dictionary>>> Polys = new();
+ public readonly List<(TimeSpan Time, PathRouteMessage Message)> Routes = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(OnBreadcrumbs);
+ SubscribeNetworkEvent(OnBreadcrumbsRefresh);
+ SubscribeNetworkEvent(OnPolys);
+ SubscribeNetworkEvent(OnPolysRefresh);
+ SubscribeNetworkEvent(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("/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())
+ {
+ 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())
+ {
+ 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(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(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(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);
+ }
+ }
+}
diff --git a/Content.Server/CPUJob/JobQueues/Queues/AiActionJobQueue.cs b/Content.Server/CPUJob/JobQueues/Queues/AiActionJobQueue.cs
deleted file mode 100644
index 7b69bb34b0..0000000000
--- a/Content.Server/CPUJob/JobQueues/Queues/AiActionJobQueue.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Server.CPUJob.JobQueues.Queues
-{
- public sealed class AiActionJobQueue : JobQueue {}
-}
diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs
index b8efec801b..62a25566f9 100644
--- a/Content.Server/Doors/Systems/DoorSystem.cs
+++ b/Content.Server/Doors/Systems/DoorSystem.cs
@@ -170,7 +170,7 @@ public sealed class DoorSystem : SharedDoorSystem
args.Verbs.Add(new AlternativeVerb()
{
- Text = "Pry door",
+ Text = Loc.GetString("door-pry"),
Impact = LogImpact.Low,
Act = () => TryPryDoor(uid, args.User, args.User, component, true),
});
@@ -180,7 +180,7 @@ public sealed class DoorSystem : SharedDoorSystem
///
/// Pry open a door. This does not check if the user is holding the required tool.
///
- 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)
return false;
diff --git a/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs b/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs
index 0d11283fc1..3e46058250 100644
--- a/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs
+++ b/Content.Server/NPC/Components/NPCMeleeCombatComponent.cs
@@ -35,6 +35,11 @@ public enum CombatStatus : byte
///
TargetUnreachable,
+ ///
+ /// If the target is outside of our melee range.
+ ///
+ TargetOutOfRange,
+
///
/// Set if the weapon we were assigned is no longer valid.
///
diff --git a/Content.Server/NPC/Components/NPCSteeringComponent.cs b/Content.Server/NPC/Components/NPCSteeringComponent.cs
index ce1f1cd4ee..4ca90e73fb 100644
--- a/Content.Server/NPC/Components/NPCSteeringComponent.cs
+++ b/Content.Server/NPC/Components/NPCSteeringComponent.cs
@@ -1,5 +1,6 @@
using System.Threading;
using Content.Server.CPUJob.JobQueues;
+using Content.Server.NPC.Pathfinding;
using Robust.Shared.Map;
namespace Content.Server.NPC.Components;
@@ -10,24 +11,23 @@ namespace Content.Server.NPC.Components;
[RegisterComponent]
public sealed class NPCSteeringComponent : Component
{
- [ViewVariables] public Job>? Pathfind = null;
+ ///
+ /// Have we currently requested a path.
+ ///
+ [ViewVariables]
+ public bool Pathfind => PathfindToken != null;
[ViewVariables] public CancellationTokenSource? PathfindToken = null;
///
/// Current path we're following to our coordinates.
///
- [ViewVariables] public Queue CurrentPath = new();
+ [ViewVariables] public Queue CurrentPath = new();
///
/// End target that we're trying to move to.
///
[ViewVariables(VVAccess.ReadWrite)] public EntityCoordinates Coordinates;
- ///
- /// Target that we're trying to move to. If we have a path then this will be the first node on the path.
- ///
- [ViewVariables] public EntityCoordinates CurrentTarget;
-
///
/// How close are we trying to get to the coordinates before being considered in range.
///
@@ -36,9 +36,18 @@ public sealed class NPCSteeringComponent : Component
///
/// How far does the last node in the path need to be before considering re-pathfinding.
///
- [ViewVariables(VVAccess.ReadWrite)] public float RepathRange = 1.5f;
+ [ViewVariables(VVAccess.ReadWrite)] public float RepathRange = 1.2f;
+
+ public const int FailedPathLimit = 3;
+
+ ///
+ /// How many times we've failed to pathfind. Once this hits the limit we'll stop steering.
+ ///
+ [ViewVariables] public int FailedPathCount;
[ViewVariables] public SteeringStatus Status = SteeringStatus.Moving;
+
+ [ViewVariables(VVAccess.ReadWrite)] public PathFlags Flags = PathFlags.None;
}
public enum SteeringStatus : byte
diff --git a/Content.Server/NPC/HTN/HTNComponent.cs b/Content.Server/NPC/HTN/HTNComponent.cs
index b8fcca2a86..4fa5f216be 100644
--- a/Content.Server/NPC/HTN/HTNComponent.cs
+++ b/Content.Server/NPC/HTN/HTNComponent.cs
@@ -42,4 +42,6 @@ public sealed class HTNComponent : NPCComponent
/// Is this NPC currently planning?
///
[ViewVariables] public bool Planning => PlanningJob != null;
+
+
}
diff --git a/Content.Server/NPC/HTN/HTNPlanJob.cs b/Content.Server/NPC/HTN/HTNPlanJob.cs
index 4f971b154b..580803cca7 100644
--- a/Content.Server/NPC/HTN/HTNPlanJob.cs
+++ b/Content.Server/NPC/HTN/HTNPlanJob.cs
@@ -131,7 +131,7 @@ public sealed class HTNPlanJob : Job
return false;
}
- var (valid, effects) = await primitive.Operator.Plan(blackboard);
+ var (valid, effects) = await primitive.Operator.Plan(blackboard, Cancellation);
if (!valid)
return false;
diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs
index 41c52f0d0d..f0a0a13f33 100644
--- a/Content.Server/NPC/HTN/HTNSystem.cs
+++ b/Content.Server/NPC/HTN/HTNSystem.cs
@@ -151,8 +151,9 @@ public sealed class HTNSystem : EntitySystem
{
_sawmill.Fatal($"Received exception on planning job for {comp.Owner}!");
_npc.SleepNPC(comp.Owner);
+ var exc = comp.PlanningJob.Exception;
RemComp(comp.Owner);
- throw comp.PlanningJob.Exception;
+ throw exc;
}
// If a new planning job has finished then handle it.
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs
index 2cf8ca918e..693137442e 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/HTNOperator.cs
@@ -1,11 +1,13 @@
+using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks;
///
/// Concrete code that gets run for an NPC task.
///
-[ImplicitDataDefinitionForInheritors]
+[ImplicitDataDefinitionForInheritors, MeansImplicitUse]
public abstract class HTNOperator
{
///
@@ -20,9 +22,11 @@ public abstract class HTNOperator
/// Called during planning.
///
/// The blackboard for the NPC.
+ ///
/// 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.
- public virtual async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public virtual async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
return (true, null);
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs
index 09e0a3ecf8..1ed2ab9e8f 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/MeleeOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.MobState;
using Content.Server.NPC.Components;
@@ -34,7 +35,8 @@ public sealed class MeleeOperator : HTNOperator
melee.Target = blackboard.GetValue(TargetKey);
}
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue(TargetKey, out var target))
@@ -62,7 +64,6 @@ public sealed class MeleeOperator : HTNOperator
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
base.Update(blackboard, frameTime);
- // TODO:
var owner = blackboard.GetValue(NPCBlackboard.Owner);
var status = HTNOperatorStatus.Continuing;
@@ -79,6 +80,7 @@ public sealed class MeleeOperator : HTNOperator
{
switch (combat.Status)
{
+ case CombatStatus.TargetOutOfRange:
case CombatStatus.Normal:
status = HTNOperatorStatus.Continuing;
break;
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/PickMeleeTargetOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/PickMeleeTargetOperator.cs
index 992f34f72c..5bbfc98dbc 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/PickMeleeTargetOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Melee/PickMeleeTargetOperator.cs
@@ -1,32 +1,24 @@
-using Robust.Shared.Map;
+using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Melee;
///
/// Selects a target for melee.
///
+[MeansImplicitUse]
public sealed class PickMeleeTargetOperator : NPCCombatOperator
{
- protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery xformQuery)
+ protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery xformQuery)
{
- var ourCoordinates = blackboard.GetValueOrDefault(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;
if (existingTarget == uid)
{
- rating += 3f;
+ rating += 2f;
}
- rating += 1f / distance * 4f;
+ if (distance > 0f)
+ rating += 50f / distance;
return rating;
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs
index c16b1ae445..979c40fe31 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/MoveToOperator.cs
@@ -2,10 +2,11 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Server.NPC.Pathfinding;
-using Content.Server.NPC.Pathfinding.Pathfinders;
using Content.Server.NPC.Systems;
+using Content.Shared.NPC;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
+using YamlDotNet.Core.Tokens;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -58,7 +59,8 @@ public sealed class MoveToOperator : HTNOperator
_steering = sysManager.GetEntitySystem();
}
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
if (!blackboard.TryGetValue(TargetKey, out var targetCoordinates))
{
@@ -72,8 +74,7 @@ public sealed class MoveToOperator : HTNOperator
return (false, null);
if (!_mapManager.TryGetGrid(xform.GridUid, out var ownerGrid) ||
- !_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid) ||
- ownerGrid != targetGrid)
+ !_mapManager.TryGetGrid(targetCoordinates.GetGridUid(_entManager), out var targetGrid))
{
return (false, null);
}
@@ -97,30 +98,25 @@ public sealed class MoveToOperator : HTNOperator
});
}
- var cancelToken = new CancellationTokenSource();
- var access = blackboard.GetValueOrDefault>(NPCBlackboard.Access) ?? new List();
+ var path = await _pathfind.GetPath(
+ blackboard.GetValue(NPCBlackboard.Owner),
+ xform.Coordinates,
+ targetCoordinates,
+ range,
+ cancelToken,
+ _pathfind.GetFlags(blackboard));
- var job = _pathfind.RequestPath(
- new PathfindingArgs(
- blackboard.GetValue(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)
+ if (path.Result != PathResult.Path)
+ {
return (false, null);
+ }
return (true, new Dictionary()
{
{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.
@@ -131,23 +127,25 @@ public sealed class MoveToOperator : HTNOperator
// Need to remove the planning value for execution.
blackboard.Remove(NPCBlackboard.OwnerCoordinates);
+ var targetCoordinates = blackboard.GetValue(TargetKey);
// Re-use the path we may have if applicable.
- var comp = _steering.Register(blackboard.GetValue(NPCBlackboard.Owner), blackboard.GetValue(TargetKey));
+ var comp = _steering.Register(blackboard.GetValue(NPCBlackboard.Owner), targetCoordinates);
if (blackboard.TryGetValue(RangeKey, out var range))
{
comp.Range = range;
}
- if (blackboard.TryGetValue>(PathfindKey, out var path))
+ if (blackboard.TryGetValue(PathfindKey, out var result))
{
if (blackboard.TryGetValue(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.
- blackboard.Remove>(PathfindKey);
+ blackboard.Remove(PathfindKey);
if (RemoveKeyOnFinish)
{
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NPCCombatOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NPCCombatOperator.cs
index c151b8030f..e9fe84c6fc 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NPCCombatOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/NPCCombatOperator.cs
@@ -1,6 +1,10 @@
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.Interaction;
+using Content.Server.NPC.Pathfinding;
using Content.Server.NPC.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Robust.Shared.Map;
@@ -10,8 +14,9 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public abstract class NPCCombatOperator : HTNOperator
{
[Dependency] protected readonly IEntityManager EntManager = default!;
- private FactionSystem _tags = default!;
+ private FactionSystem _factions = default!;
protected InteractionSystem Interaction = default!;
+ private PathfindingSystem _pathfinding = default!;
[ViewVariables, DataField("key")] public string Key = "CombatTarget";
@@ -21,16 +26,25 @@ public abstract class NPCCombatOperator : HTNOperator
[ViewVariables, DataField("keyCoordinates")]
public string KeyCoordinates = "CombatTargetCoordinates";
+ ///
+ /// Regardless of pathfinding or LOS these are the max we'll check
+ ///
+ private const int MaxConsideredTargets = 10;
+ private const int MaxTargetCount = 5;
+
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
- _tags = sysManager.GetEntitySystem();
+ sysManager.GetEntitySystem();
+ _factions = sysManager.GetEntitySystem();
Interaction = sysManager.GetEntitySystem();
+ _pathfinding = sysManager.GetEntitySystem();
}
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
- var targets = GetTargets(blackboard);
+ var targets = await GetTargets(blackboard);
if (targets.Count == 0)
{
@@ -49,34 +63,68 @@ public abstract class NPCCombatOperator : HTNOperator
return (true, effects);
}
- private List<(EntityUid Entity, float Rating)> GetTargets(NPCBlackboard blackboard)
+ private async Task> GetTargets(NPCBlackboard blackboard)
{
var owner = blackboard.GetValue(NPCBlackboard.Owner);
var radius = blackboard.GetValueOrDefault(NPCBlackboard.VisionRadius, EntManager);
- var targets = new List<(EntityUid Entity, float Rating)>();
+ var targets = new List<(EntityUid Entity, float Rating, float Distance)>();
blackboard.TryGetValue(Key, out var existingTarget);
var xformQuery = EntManager.GetEntityQuery();
var mobQuery = EntManager.GetEntityQuery();
var canMove = blackboard.GetValueOrDefault(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
- 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))
{
if (mobQuery.TryGetComponent(target, out var mobState) &&
- mobState.CurrentState > DamageState.Alive)
+ mobState.CurrentState > DamageState.Alive ||
+ !xformQuery.TryGetComponent(target, out targetXform))
{
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));
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 xformQuery);
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleComponentOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleComponentOperator.cs
index 4f87ea9354..5640d39eff 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleComponentOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleComponentOperator.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
@@ -13,8 +14,7 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- private PathfindingSystem _path = default!;
+ private PathfindingSystem _pathfinding = default!;
private EntityLookupSystem _lookup = default!;
[DataField("rangeKey", required: true)]
@@ -26,15 +26,22 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
[ViewVariables, DataField("component", required: true)]
public string Component = string.Empty;
+ ///
+ /// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
+ ///
+ [ViewVariables, DataField("pathfindKey")]
+ public string PathfindKey = "MovementPathfind";
+
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
- _path = sysManager.GetEntitySystem();
_lookup = sysManager.GetEntitySystem();
+ _pathfinding = sysManager.GetEntitySystem();
}
///
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
// Check if the component exists
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: Look at unreal HTN to see repeatable ones maybe?
+ // TODO: Need type
foreach (var entity in _lookup.GetEntitiesInRange(coordinates, range))
{
if (entity == owner || !query.TryGetComponent(entity, out var comp))
@@ -69,27 +77,31 @@ public sealed class PickAccessibleComponentOperator : HTNOperator
return (false, null);
}
+ blackboard.TryGetValue(RangeKey, out var maxRange);
+
+ if (maxRange == 0f)
+ maxRange = 7f;
+
while (targets.Count > 0)
{
- // TODO: Get nearest at some stage
- var target = _random.PickAndTake(targets);
+ var path = await _pathfinding.GetRandomPath(
+ owner,
+ 1.4f,
+ maxRange,
+ cancelToken,
+ flags: _pathfinding.GetFlags(blackboard));
- // TODO: God the path api sucks PLUS I need some fast way to get this.
- var job = _path.RequestPath(owner, target.Owner, CancellationToken.None);
-
- if (job == null)
- continue;
-
- await job.AsTask;
-
- if (job.Result == null || !_entManager.TryGetComponent(target.Owner, out var targetXform))
+ if (path.Result != PathResult.Path)
{
- continue;
+ return (false, null);
}
+ var target = path.Path.Last().Coordinates;
+
return (true, new Dictionary()
{
- { TargetKey, targetXform.Coordinates },
+ { TargetKey, target },
+ { PathfindKey, path}
});
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs
index 2b099402fa..f0e1f7869d 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickAccessibleOperator.cs
@@ -1,6 +1,7 @@
+using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
-using Content.Server.NPC.Pathfinding.Accessible;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -10,9 +11,7 @@ namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
///
public sealed class PickAccessibleOperator : HTNOperator
{
- [Dependency] private readonly IEntityManager _entManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- private AiReachableSystem _reachable = default!;
+ private PathfindingSystem _pathfinding = default!;
[DataField("rangeKey", required: true)]
public string RangeKey = string.Empty;
@@ -20,44 +19,48 @@ public sealed class PickAccessibleOperator : HTNOperator
[ViewVariables, DataField("targetKey", required: true)]
public string TargetKey = string.Empty;
+ ///
+ /// Where the pathfinding result will be stored (if applicable). This gets removed after execution.
+ ///
+ [ViewVariables, DataField("pathfindKey")]
+ public string PathfindKey = "MovementPathfind";
+
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
- _reachable = IoCManager.Resolve().GetEntitySystem();
+ _pathfinding = sysManager.GetEntitySystem();
}
///
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
// Very inefficient (should weight each region by its node count) but better than the old system
var owner = blackboard.GetValue(NPCBlackboard.Owner);
- if (!_entManager.TryGetComponent(_entManager.GetComponent(owner).GridUid, out IMapGridComponent? grid))
- return (false, null);
+ blackboard.TryGetValue(RangeKey, out var maxRange);
- var reachableArgs = ReachableArgs.GetArgs(owner, blackboard.GetValueOrDefault(RangeKey));
- var entityRegion = _reachable.GetRegion(owner);
- var reachableRegions = _reachable.GetReachableRegions(reachableArgs, entityRegion);
+ if (maxRange == 0f)
+ maxRange = 7f;
- if (reachableRegions.Count == 0)
- return (false, null);
+ var path = await _pathfinding.GetRandomPath(
+ owner,
+ 1.4f,
+ maxRange,
+ cancelToken,
+ flags: _pathfinding.GetFlags(blackboard));
- var reachableNodes = new List();
-
- foreach (var region in reachableRegions)
+ if (path.Result != PathResult.Path)
{
- foreach (var node in region.Nodes)
- {
- reachableNodes.Add(node);
- }
+ return (false, null);
}
- var targetNode = _random.Pick(reachableNodes);
+ var target = path.Path.Last().Coordinates;
- var target = grid.Grid.GridTileToLocal(targetNode.TileRef.GridIndices);
return (true, new Dictionary()
{
{ TargetKey, target },
+ { PathfindKey, path}
});
}
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickRandomRotationOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickRandomRotationOperator.cs
index 4253e0aa66..7f0fff47c3 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickRandomRotationOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/PickRandomRotationOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Random;
@@ -10,7 +11,8 @@ public sealed class PickRandomRotationOperator : HTNOperator
[ViewVariables, DataField("targetKey")]
public string TargetKey = "RotateTarget";
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
var rotation = _random.NextAngle();
return (true, new Dictionary()
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RandomOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RandomOperator.cs
index f9c6e67b2d..cda29518a9 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RandomOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/RandomOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Random;
@@ -22,7 +23,8 @@ public sealed class RandomOperator : HTNOperator
///
[DataField("maxKey", required: true)] public string MaxKey = string.Empty;
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
return (true, new Dictionary()
{
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/PickRangedTargetOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/PickRangedTargetOperator.cs
index 72ad617bb7..e5dc6599bb 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/PickRangedTargetOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/PickRangedTargetOperator.cs
@@ -1,38 +1,19 @@
-using Robust.Shared.Map;
+using JetBrains.Annotations;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Ranged;
///
/// Selects a target for ranged combat.
///
+[UsedImplicitly]
public sealed class PickRangedTargetOperator : NPCCombatOperator
{
- protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, bool canMove, EntityQuery xformQuery)
+ protected override float GetRating(NPCBlackboard blackboard, EntityUid uid, EntityUid existingTarget, float distance, bool canMove, EntityQuery xformQuery)
{
- var ourCoordinates = blackboard.GetValueOrDefault(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(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.
// Having a debug overlay just to project these would be very useful when finetuning in future.
var rating = 0f;
- if (inLOS)
- rating += 4f;
-
if (existingTarget == uid)
{
rating += 2f;
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs
index 3f4e43703c..296f43ee2c 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Ranged/RangedOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
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.
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
// Don't attack if they're already as wounded as we want them.
if (!blackboard.TryGetValue(TargetKey, out var target))
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SetFloatOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SetFloatOperator.cs
index 591a7f369d..43057cda97 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SetFloatOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SetFloatOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
@@ -12,7 +13,8 @@ public sealed class SetFloatOperator : HTNOperator
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
public float Amount;
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
return (true, new Dictionary()
{
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
index 51a1494cac..f7e9621118 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/PickNearbyInjectableOperator.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.NPC.Components;
@@ -31,7 +32,8 @@ public sealed class PickNearbyInjectableOperator : HTNOperator
_lookup = sysManager.GetEntitySystem();
}
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
var owner = blackboard.GetValue(NPCBlackboard.Owner);
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Test/PickPathfindPointOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Test/PickPathfindPointOperator.cs
index bf05835983..15a185e4b2 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Test/PickPathfindPointOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Test/PickPathfindPointOperator.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Robust.Shared.Random;
@@ -10,7 +11,8 @@ public sealed class PickPathfindPointOperator : HTNOperator
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard)
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
{
var owner = blackboard.GetValue(NPCBlackboard.Owner);
diff --git a/Content.Server/NPC/NPCBlackboard.cs b/Content.Server/NPC/NPCBlackboard.cs
index 52b70058ca..9cc6bee96d 100644
--- a/Content.Server/NPC/NPCBlackboard.cs
+++ b/Content.Server/NPC/NPCBlackboard.cs
@@ -195,6 +195,16 @@ public sealed class NPCBlackboard : IEnumerable>
public const string Owner = "Owner";
public const string OwnerCoordinates = "OwnerCoordinates";
public const string MovementTarget = "MovementTarget";
+
+ ///
+ /// Can the NPC pry open doors for steering.
+ ///
+ public const string NavPry = "NavPry";
+
+ ///
+ /// Can the NPC smash obstacles for steering.
+ ///
+ public const string NavSmash = "NavSmash";
public const string RotateSpeed = "RotateSpeed";
public const string VisionRadius = "VisionRadius";
public const float MeleeRange = 1f;
diff --git a/Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs
deleted file mode 100644
index d840f8b3a6..0000000000
--- a/Content.Server/NPC/Pathfinding/Accessible/AiReachableSystem.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Determines whether an AI has access to a specific pathfinding node.
- ///
- /// 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!;
-
- ///
- /// Queued region updates
- ///
- private readonly HashSet _queuedUpdates = new();
-
- // Oh god the nesting. Shouldn't need to go beyond this
- ///
- /// The corresponding regions for each PathfindingChunk.
- /// Regions are groups of nodes with the same profile (for pathfinding purposes)
- /// i.e. same collision, not-space, same access, etc.
- ///
- private readonly Dictionary>> _regions =
- new();
-
- ///
- /// Minimum time for the cached reachable regions to be stored
- ///
- private const float MinCacheTime = 1.0f;
-
- // Cache what regions are accessible from this region. Cached per ReachableArgs
- // so multiple entities in the same region with the same args should all be able to share their reachable lookup
- // Also need to store when we cached it to know if it's stale if the chunks have updated
-
- // TODO: There's probably a more memory-efficient way to cache this
- // Then again, there's likely also a more memory-efficient way to implement regions.
-
- // Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
- // Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
- private readonly Dictionary Regions)>> _cachedAccessible =
- new();
-
- private readonly List _queuedCacheDeletions = new();
-
-#if DEBUG
- private HashSet _subscribedSessions = new();
- private int _runningCacheIdx = 0;
-#endif
-
- public override void Initialize()
- {
- SubscribeLocalEvent(Reset);
- SubscribeLocalEvent(RecalculateNodeRegions);
- SubscribeLocalEvent(GridRemoved);
-#if DEBUG
- SubscribeNetworkEvent(HandleSubscription);
- SubscribeNetworkEvent(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);
- }
-
- ///
- /// Can the entity reach the target?
- ///
- /// 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
- ///
- ///
- ///
- ///
- public bool CanAccess(EntityUid entity, EntityUid target, float range = 0.0f)
- {
- var xform = EntityManager.GetComponent(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(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);
- }
-
- ///
- /// Retrieve the reachable regions
- ///
- ///
- ///
- ///
- public HashSet 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();
- }
-
- var cachedArgs = GetCachedArgs(reachableArgs);
- (TimeSpan CacheTime, HashSet 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;
- }
-
- ///
- /// Get any adequate cached args if possible, otherwise just use ours
- ///
- /// Essentially any args that have the same access AND >= our vision radius can be used
- ///
- ///
- 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;
- }
-
- ///
- /// Checks whether there's a valid cache for our accessibility args.
- /// Most regular mobs can share their cached accessibility with each other
- ///
- /// Will also remove it from the cache if it is invalid
- ///
- ///
- ///
- private bool IsCacheValid(ReachableArgs accessibleArgs, PathfindingRegion region)
- {
- if (!_cachedAccessible.TryGetValue(accessibleArgs, out var cachedArgs))
- {
- _cachedAccessible.Add(accessibleArgs, new Dictionary)>());
- 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();
- // 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;
- }
-
- ///
- /// Caches the entity's nearby accessible regions in vision radius
- ///
- /// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
- ///
- ///
- private (TimeSpan, HashSet) GetVisionReachable(ReachableArgs reachableArgs, PathfindingRegion entityRegion)
- {
- var openSet = new Queue();
- openSet.Enqueue(entityRegion);
- var closedSet = new HashSet();
- var accessible = new HashSet {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);
- }
-
- ///
- /// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
- ///
- /// Implicitly they would've already been merged if possible
- ///
- ///
- 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);
- }
- }
-
- ///
- /// Get the current region for this entity
- ///
- ///
- ///
- public PathfindingRegion? GetRegion(EntityUid entity)
- {
- var xform = EntityManager.GetComponent(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);
- }
-
- ///
- /// Get the current region for this node
- ///
- ///
- ///
- 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;
- }
-
- ///
- /// Add this node to the relevant region.
- ///
- ///
- /// The cached region for each node
- /// The existing regions in the chunk
- /// This is already calculated in advance so may as well re-use it
- /// This is already calculated in advance so may as well re-use it
- ///
- private PathfindingRegion? CalculateNode(
- PathfindingNode node,
- Dictionary existingRegions,
- HashSet 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(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 {node}, node.AccessReaders.Count > 0);
- _regions[parentChunk.GridId][parentChunk].Add(newRegion);
- existingRegions.Add(node, newRegion);
- UpdateRegionEdge(newRegion, node);
- return newRegion;
- }
-
- ///
- /// Combines the two regions into one bigger region
- ///
- ///
- ///
- private void MergeInto(PathfindingRegion source, PathfindingRegion target, Dictionary? 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);
- }
- }
-
- ///
- /// Remove the cached accessibility lookup for this region
- ///
- ///
- 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();
-
- 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
- }
-
- ///
- /// Generate all of the regions within a chunk
- ///
- /// These can't across over into another chunk and doors are their own region
- ///
- 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>());
- }
-
- 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();
- var chunkRegions = new HashSet();
- _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>>();
- var chunkIdx = 0;
- var regionIdx = 0;
-
- if (!_regions.TryGetValue(gridId, out var dict))
- {
- return;
- }
-
- foreach (var (_, regions) in dict)
- {
- var debugRegions = new Dictionary>();
- debugResult.Add(chunkIdx, debugRegions);
-
- foreach (var region in regions)
- {
- var debugRegionNodes = new List(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);
- }
- }
-
- ///
- /// Sent whenever the reachable cache for a particular mob is built or retrieved
- ///
- ///
- ///
- ///
- private void SendRegionCacheMessage(EntityUid gridId, IEnumerable regions, bool cached)
- {
- if (_subscribedSessions.Count == 0) return;
-
- var grid = _mapManager.GetGrid(gridId);
- var debugResult = new Dictionary>();
-
- foreach (var region in regions)
- {
- debugResult.Add(_runningCacheIdx, new List());
-
- foreach (var node in region.Nodes)
- {
- var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
-
- debugResult[_runningCacheIdx].Add(nodeVector);
- }
-
- _runningCacheIdx++;
- }
-
- foreach (var session in _subscribedSessions)
- {
- RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached), session.ConnectedClient);
- }
- }
-#endif
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs b/Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs
deleted file mode 100644
index f6b88d200d..0000000000
--- a/Content.Server/NPC/Pathfinding/Accessible/BFSPathfinder.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using Content.Server.NPC.Pathfinding.Pathfinders;
-using Robust.Shared.Map;
-
-namespace Content.Server.NPC.Pathfinding.Accessible
-{
- ///
- /// The simplest pathfinder
- ///
- public sealed class BFSPathfinder
- {
- ///
- /// Gets all of the tiles in range that can we access
- ///
- /// If you want Dijkstra then add distances.
- /// Doesn't use the JobQueue as it will generally be encapsulated by other jobs
- ///
- ///
- /// Whether we traverse from the starting tile or the end tile
- ///
- public static IEnumerable GetNodesInRange(PathfindingArgs pathfindingArgs, bool fromStart = true)
- {
- var pathfindingSystem = EntitySystem.Get();
- // Don't need a priority queue given not looking for shortest path
- var openTiles = new Queue();
- var closedTiles = new HashSet();
- 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;
- }
- }
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs b/Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs
deleted file mode 100644
index 8e5725c1ff..0000000000
--- a/Content.Server/NPC/Pathfinding/Accessible/PathfindingRegion.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-namespace Content.Server.NPC.Pathfinding.Accessible
-{
- ///
- /// A group of homogenous PathfindingNodes inside a single chunk
- ///
- /// Makes the graph smaller and quicker to traverse
- public sealed class PathfindingRegion : IEquatable
- {
- ///
- /// Bottom-left reference node of the region
- ///
- 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
-
- ///
- /// Maximum width of the nodes
- ///
- public int Height { get; private set; } = 1;
-
- ///
- /// Maximum width of the nodes
- ///
- public int Width { get; private set; } = 1;
-
- public PathfindingChunk ParentChunk => OriginNode.ParentChunk;
- public HashSet Neighbors { get; } = new();
-
- public bool IsDoor { get; }
- public HashSet Nodes => _nodes;
- private readonly HashSet _nodes;
-
- public bool Deleted { get; private set; }
-
- public PathfindingRegion(PathfindingNode originNode, HashSet nodes, bool isDoor = false)
- {
- OriginNode = originNode;
- _nodes = nodes;
- IsDoor = isDoor;
- }
-
- public void Shutdown()
- {
- // Tell our neighbors we no longer exist ;-/
- var neighbors = new List(Neighbors);
-
- for (var i = 0; i < neighbors.Count; i++)
- {
- var neighbor = neighbors[i];
- neighbor.Neighbors.Remove(this);
- }
-
- _nodes.Clear();
- Neighbors.Clear();
-
- Deleted = true;
- }
-
- ///
- /// Roughly how far away another region is by nearest node
- ///
- ///
- ///
- 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);
- }
-
- ///
- /// Can the given args can traverse this region?
- ///
- ///
- ///
- 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();
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs
deleted file mode 100644
index b86e84e778..0000000000
--- a/Content.Server/NPC/Pathfinding/Accessible/ReachableArgs.cs
+++ /dev/null
@@ -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 Access { get; }
- public int CollisionMask { get; }
-
- public ReachableArgs(float visionRadius, ICollection access, int collisionMask)
- {
- VisionRadius = visionRadius;
- Access = access;
- CollisionMask = collisionMask;
- }
-
- ///
- /// Get appropriate args for a particular entity
- ///
- ///
- ///
- public static ReachableArgs GetArgs(EntityUid entity, float radius)
- {
- var collisionMask = 0;
- var entMan = IoCManager.Resolve();
- if (entMan.TryGetComponent(entity, out IPhysBody? physics))
- {
- collisionMask = physics.CollisionMask;
- }
-
- var accessSystem = EntitySystem.Get();
- var access = accessSystem.FindAccessTags(entity);
-
- return new ReachableArgs(radius, access, collisionMask);
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs b/Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs
new file mode 100644
index 0000000000..30da7cee1d
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/GridPathfindingChunk.cs
@@ -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[] Polygons = new List[SharedPathfindingSystem.ChunkSize * SharedPathfindingSystem.ChunkSize];
+
+ ///
+ /// The relevant polygon for this chunk's portals
+ ///
+ public readonly Dictionary PortalPolys = new();
+
+ ///
+ /// This chunk's portals.
+ ///
+ public readonly List Portals = new();
+
+ public GridPathfindingChunk()
+ {
+ for (var x = 0; x < Polygons.Length; x++)
+ {
+ Polygons[x] = new List();
+ }
+ }
+}
diff --git a/Content.Server/NPC/Pathfinding/GridPathfindingComponent.cs b/Content.Server/NPC/Pathfinding/GridPathfindingComponent.cs
index 4b5e23d8d3..2d5fe8e0c7 100644
--- a/Content.Server/NPC/Pathfinding/GridPathfindingComponent.cs
+++ b/Content.Server/NPC/Pathfinding/GridPathfindingComponent.cs
@@ -1,8 +1,24 @@
namespace Content.Server.NPC.Pathfinding;
-[RegisterComponent]
-[Access(typeof(PathfindingSystem))]
+///
+/// Stores the relevant pathfinding data for grids.
+///
+[RegisterComponent, Access(typeof(PathfindingSystem))]
public sealed class GridPathfindingComponent : Component
{
- public readonly Dictionary Graph = new();
+ public readonly HashSet DirtyChunks = new();
+
+ ///
+ /// Next time the graph is allowed to update.
+ ///
+ public TimeSpan NextUpdate;
+
+ public readonly Dictionary Chunks = new();
+
+ ///
+ /// Retrieves the chunk where the specified portal is stored on this grid.
+ ///
+ public readonly Dictionary PortalLookup = new();
+
+ public readonly List DirtyPortals = new();
}
diff --git a/Content.Server/NPC/Pathfinding/PathFlags.cs b/Content.Server/NPC/Pathfinding/PathFlags.cs
new file mode 100644
index 0000000000..a30592ae21
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathFlags.cs
@@ -0,0 +1,22 @@
+namespace Content.Server.NPC.Pathfinding;
+
+[Flags]
+public enum PathFlags : byte
+{
+ None = 0,
+
+ ///
+ /// Do we have any form of access.
+ ///
+ Access = 1 << 0,
+
+ ///
+ /// Can we pry airlocks if necessary.
+ ///
+ Prying = 1 << 1,
+
+ ///
+ /// Can stuff like walls be broken.
+ ///
+ Smashing = 1 << 2,
+}
diff --git a/Content.Server/NPC/Pathfinding/PathPoly.cs b/Content.Server/NPC/Pathfinding/PathPoly.cs
new file mode 100644
index 0000000000..9f66d4a1c5
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathPoly.cs
@@ -0,0 +1,64 @@
+using Content.Shared.NPC;
+using Robust.Shared.Map;
+
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed class PathPoly : IEquatable
+{
+ public readonly EntityUid GraphUid;
+ public readonly Vector2i ChunkOrigin;
+ public readonly byte TileIndex;
+
+ public readonly Box2 Box;
+ public PathfindingData Data;
+
+ public readonly HashSet Neighbors;
+
+ public PathPoly(EntityUid graphUid, Vector2i chunkOrigin, byte tileIndex, Box2 vertices, PathfindingData data, HashSet 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);
+ }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathPortal.cs b/Content.Server/NPC/Pathfinding/PathPortal.cs
new file mode 100644
index 0000000000..f95801b7eb
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathPortal.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Map;
+
+namespace Content.Server.NPC.Pathfinding;
+
+///
+/// Connects 2 disparate locations.
+///
+///
+/// For example, 2 docking airlocks connecting 2 graphs, or an actual portal on the same graph.
+///
+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;
+ }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathRequest.cs b/Content.Server/NPC/Pathfinding/PathRequest.cs
new file mode 100644
index 0000000000..7df2c02818
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathRequest.cs
@@ -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;
+
+///
+/// Stores the in-progress data of a pathfinding request.
+///
+public abstract class PathRequest
+{
+ public EntityCoordinates Start;
+
+ public Task Task => Tcs.Task;
+ public readonly TaskCompletionSource Tcs;
+
+ public Queue Polys = new();
+
+ public bool Started = false;
+
+ #region Pathfinding state
+
+ public readonly Stopwatch Stopwatch = new();
+ public PriorityQueue> Frontier = default!;
+ public readonly Dictionary CostSoFar = new();
+ public readonly Dictionary 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(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
+{
+ ///
+ /// How far away we're allowed to expand in distance.
+ ///
+ public float ExpansionRange;
+
+ ///
+ /// How many nodes we're allowed to expand
+ ///
+ 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;
+ }
+}
+
+///
+/// Stores the final result of a pathfinding request
+///
+public sealed class PathResultEvent
+{
+ public PathResult Result;
+ public readonly Queue Path;
+
+ public PathResultEvent(PathResult result, Queue path)
+ {
+ Result = result;
+ Path = path;
+ }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathResult.cs b/Content.Server/NPC/Pathfinding/PathResult.cs
new file mode 100644
index 0000000000..dde81846a0
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathResult.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.NPC.Pathfinding;
+
+public enum PathResult : byte
+{
+ NoPath,
+ PartialPath,
+ Path,
+ Continuing,
+}
diff --git a/Content.Server/NPC/Pathfinding/Pathfinders/AStarPathfindingJob.cs b/Content.Server/NPC/Pathfinding/Pathfinders/AStarPathfindingJob.cs
deleted file mode 100644
index 2e3d292461..0000000000
--- a/Content.Server/NPC/Pathfinding/Pathfinders/AStarPathfindingJob.cs
+++ /dev/null
@@ -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>
- {
-#if DEBUG
- public static event Action? 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?> 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>(new PathfindingComparer());
- var costSoFar = new Dictionary();
- var cameFrom = new Dictionary();
-
- 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(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(cameFrom.Count);
- var debugGScores = new Dictionary(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;
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs b/Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs
deleted file mode 100644
index caea46474a..0000000000
--- a/Content.Server/NPC/Pathfinding/Pathfinders/JpsPathfindingJob.cs
+++ /dev/null
@@ -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>
- {
- // 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? 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?> 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>(new PathfindingComparer());
- var gScores = new Dictionary();
- var cameFrom = new Dictionary();
- var closedTiles = new HashSet();
-
-#if DEBUG
- var jumpNodes = new HashSet();
-#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(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;
- }
-
- ///
- /// Check to see if the node is a jump point (only works for cardinal directions)
- ///
- 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;
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingArgs.cs b/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingArgs.cs
deleted file mode 100644
index b125740b77..0000000000
--- a/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingArgs.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using Robust.Shared.Map;
-
-namespace Content.Server.NPC.Pathfinding.Pathfinders
-{
- public struct PathfindingArgs
- {
- public EntityUid Uid { get; }
- public ICollection Access { get; }
- public int CollisionMask { get; }
- public TileRef Start { get; }
- public TileRef End { get; }
- // 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 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;
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingComparer.cs b/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingComparer.cs
deleted file mode 100644
index fb2edf59eb..0000000000
--- a/Content.Server/NPC/Pathfinding/Pathfinders/PathfindingComparer.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Server.NPC.Pathfinding.Pathfinders
-{
- public sealed class PathfindingComparer : IComparer>
- {
- public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y)
- {
- return y.Item1.CompareTo(x.Item1);
- }
- }
-}
diff --git a/Content.Server/NPC/Pathfinding/PathfindingChunk.cs b/Content.Server/NPC/Pathfinding/PathfindingChunk.cs
deleted file mode 100644
index ffb53348c6..0000000000
--- a/Content.Server/NPC/Pathfinding/PathfindingChunk.cs
+++ /dev/null
@@ -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();
- }
-
- ///
- /// Only called when blockers change (i.e. un-anchored physics objects don't trigger)
- ///
- public void Dirty()
- {
- LastUpdate = IoCManager.Resolve().CurTime;
- IoCManager.Resolve().EventBus
- .RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
- }
-
- public IEnumerable GetNeighbors(IEntityManager? entManager = null)
- {
- IoCManager.Resolve(ref entManager);
- var chunkGrid = entManager.GetComponent(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;
- }
-
- ///