AI Reachable system (#1342)

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2020-07-11 23:09:37 +10:00
committed by GitHub
parent 347b4b2893
commit 51d08e8b05
22 changed files with 1337 additions and 80 deletions

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Commands
// ReSharper disable once StringLiteralTypo // ReSharper disable once StringLiteralTypo
public string Command => "pathfinder"; public string Command => "pathfinder";
public string Description => "Toggles visibility of pathfinding debuggers."; public string Description => "Toggles visibility of pathfinding debuggers.";
public string Help => "pathfinder [hide/nodes/routes/graph]"; public string Help => "pathfinder [hide/nodes/routes/graph/regioncache/regions]";
public bool Execute(IDebugConsole console, params string[] args) public bool Execute(IDebugConsole console, params string[] args)
{ {
@@ -49,6 +49,17 @@ namespace Content.Client.Commands
debugSystem.ToggleTooltip(PathfindingDebugMode.Graph); debugSystem.ToggleTooltip(PathfindingDebugMode.Graph);
anyAction = true; anyAction = true;
break; 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: default:
continue; continue;
} }

View File

@@ -7,9 +7,11 @@ using Robust.Client.Graphics.Overlays;
using Robust.Client.Graphics.Shaders; using Robust.Client.Graphics.Shaders;
using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Interfaces.Graphics.Overlays;
using Robust.Client.Player;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
@@ -30,6 +32,16 @@ namespace Content.Client.GameObjects.EntitySystems.AI
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage); SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage); SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
SubscribeNetworkEvent<SharedAiDebug.PathfindingGraphMessage>(HandleGraphMessage); SubscribeNetworkEvent<SharedAiDebug.PathfindingGraphMessage>(HandleGraphMessage);
SubscribeNetworkEvent<SharedAiDebug.ReachableChunkRegionsDebugMessage>(HandleRegionsMessage);
SubscribeNetworkEvent<SharedAiDebug.ReachableCacheDebugMessage>(HandleCachedRegionsMessage);
// I'm lazy
EnableOverlay();
}
public override void Shutdown()
{
base.Shutdown();
DisableOverlay();
} }
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message) private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
@@ -62,10 +74,20 @@ namespace Content.Client.GameObjects.EntitySystems.AI
private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message) private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message)
{ {
if ((_modes & PathfindingDebugMode.Graph) != 0) EnableOverlay();
{
_overlay.UpdateGraph(message.Graph); _overlay.UpdateGraph(message.Graph);
} }
private void HandleRegionsMessage(SharedAiDebug.ReachableChunkRegionsDebugMessage message)
{
EnableOverlay();
_overlay.UpdateRegions(message.GridId, message.Regions);
}
private void HandleCachedRegionsMessage(SharedAiDebug.ReachableCacheDebugMessage message)
{
EnableOverlay();
_overlay.UpdateCachedRegions(message.GridId, message.Regions, message.Cached);
} }
private void EnableOverlay() private void EnableOverlay()
@@ -114,6 +136,9 @@ namespace Content.Client.GameObjects.EntitySystems.AI
var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage(); var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage();
EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage); EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
} }
// TODO: Request region graph, although the client system messages didn't seem to be going through anymore
// So need further investigation.
} }
private void DisableMode(PathfindingDebugMode mode) private void DisableMode(PathfindingDebugMode mode)
@@ -144,6 +169,9 @@ namespace Content.Client.GameObjects.EntitySystems.AI
internal sealed class DebugPathfindingOverlay : Overlay internal sealed class DebugPathfindingOverlay : Overlay
{ {
private IEyeManager _eyeManager;
private IPlayerManager _playerManager;
// TODO: Add a box like the debug one and show the most recent path stuff // TODO: Add a box like the debug one and show the most recent path stuff
public override OverlaySpace Space => OverlaySpace.ScreenSpace; public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -153,6 +181,20 @@ namespace Content.Client.GameObjects.EntitySystems.AI
public readonly Dictionary<int, List<Vector2>> Graph = new Dictionary<int, List<Vector2>>(); public readonly Dictionary<int, List<Vector2>> Graph = new Dictionary<int, List<Vector2>>();
private readonly Dictionary<int, Color> _graphColors = new Dictionary<int, Color>(); private readonly Dictionary<int, Color> _graphColors = new Dictionary<int, Color>();
// Cached regions
public readonly Dictionary<GridId, Dictionary<int, List<Vector2>>> CachedRegions =
new Dictionary<GridId, Dictionary<int, List<Vector2>>>();
private readonly Dictionary<GridId, Dictionary<int, Color>> _cachedRegionColors =
new Dictionary<GridId, Dictionary<int, Color>>();
// Regions
public readonly Dictionary<GridId, Dictionary<int, Dictionary<int, List<Vector2>>>> Regions =
new Dictionary<GridId, Dictionary<int, Dictionary<int, List<Vector2>>>>();
private readonly Dictionary<GridId, Dictionary<int, Dictionary<int, Color>>> _regionColors =
new Dictionary<GridId, Dictionary<int, Dictionary<int, Color>>>();
// Route debugging // Route debugging
// As each pathfinder is very different you'll likely want to draw them completely different // As each pathfinder is very different you'll likely want to draw them completely different
public readonly List<SharedAiDebug.AStarRouteMessage> AStarRoutes = new List<SharedAiDebug.AStarRouteMessage>(); public readonly List<SharedAiDebug.AStarRouteMessage> AStarRoutes = new List<SharedAiDebug.AStarRouteMessage>();
@@ -161,8 +203,11 @@ namespace Content.Client.GameObjects.EntitySystems.AI
public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay)) public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay))
{ {
Shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance(); Shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
_eyeManager = IoCManager.Resolve<IEyeManager>();
_playerManager = IoCManager.Resolve<IPlayerManager>();
} }
#region Graph
public void UpdateGraph(Dictionary<int, List<Vector2>> graph) public void UpdateGraph(Dictionary<int, List<Vector2>> graph)
{ {
Graph.Clear(); Graph.Clear();
@@ -176,19 +221,15 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
} }
private void DrawGraph(DrawingHandleScreen screenHandle) private void DrawGraph(DrawingHandleScreen screenHandle, Box2 viewport)
{ {
var eyeManager = IoCManager.Resolve<IEyeManager>();
var viewport = IoCManager.Resolve<IEyeManager>().GetWorldViewport();
foreach (var (chunk, nodes) in Graph) foreach (var (chunk, nodes) in Graph)
{ {
foreach (var tile in nodes) foreach (var tile in nodes)
{ {
if (!viewport.Contains(tile)) continue; if (!viewport.Contains(tile)) continue;
var screenTile = eyeManager.WorldToScreen(tile); var screenTile = _eyeManager.WorldToScreen(tile);
var box = new UIBox2( var box = new UIBox2(
screenTile.X - 15.0f, screenTile.X - 15.0f,
screenTile.Y - 15.0f, screenTile.Y - 15.0f,
@@ -199,21 +240,130 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
} }
} }
#endregion
#region pathfinder #region Regions
//Server side debugger should increment every region
private void DrawAStarRoutes(DrawingHandleScreen screenHandle) public void UpdateCachedRegions(GridId gridId, Dictionary<int, List<Vector2>> messageRegions, bool cached)
{ {
var eyeManager = IoCManager.Resolve<IEyeManager>(); if (!CachedRegions.ContainsKey(gridId))
var viewport = eyeManager.GetWorldViewport(); {
CachedRegions.Add(gridId, new Dictionary<int, List<Vector2>>());
_cachedRegionColors.Add(gridId, new Dictionary<int, Color>());
}
foreach (var (region, nodes) in messageRegions)
{
CachedRegions[gridId][region] = nodes;
if (cached)
{
_cachedRegionColors[gridId][region] = Color.Blue.WithAlpha(0.3f);
}
else
{
_cachedRegionColors[gridId][region] = Color.Green.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 attachedEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (attachedEntity == null || !CachedRegions.TryGetValue(attachedEntity.Transform.GridID, 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[attachedEntity.Transform.GridID][region]);
}
}
}
public void UpdateRegions(GridId gridId, Dictionary<int, Dictionary<int, List<Vector2>>> messageRegions)
{
if (!Regions.ContainsKey(gridId))
{
Regions.Add(gridId, new Dictionary<int, Dictionary<int, List<Vector2>>>());
_regionColors.Add(gridId, new Dictionary<int, Dictionary<int, Color>>());
}
var robustRandom = IoCManager.Resolve<IRobustRandom>();
foreach (var (chunk, regions) in messageRegions)
{
Regions[gridId][chunk] = new Dictionary<int, List<Vector2>>();
_regionColors[gridId][chunk] = new Dictionary<int, Color>();
foreach (var (region, nodes) in regions)
{
Regions[gridId][chunk].Add(region, nodes);
_regionColors[gridId][chunk][region] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
robustRandom.NextFloat(), 0.3f);
}
}
}
private void DrawRegions(DrawingHandleScreen screenHandle, Box2 viewport)
{
var attachedEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (attachedEntity == null || !Regions.TryGetValue(attachedEntity.Transform.GridID, 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[attachedEntity.Transform.GridID][chunk][region]);
}
}
}
}
#endregion
#region Pathfinder
private void DrawAStarRoutes(DrawingHandleScreen screenHandle, Box2 viewport)
{
foreach (var route in AStarRoutes) foreach (var route in AStarRoutes)
{ {
// Draw box on each tile of route // Draw box on each tile of route
foreach (var position in route.Route) foreach (var position in route.Route)
{ {
if (!viewport.Contains(position)) continue; if (!viewport.Contains(position)) continue;
var screenTile = eyeManager.WorldToScreen(position); var screenTile = _eyeManager.WorldToScreen(position);
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
var box = new UIBox2( var box = new UIBox2(
screenTile.X - 15.0f, screenTile.X - 15.0f,
@@ -225,11 +375,8 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
} }
private void DrawAStarNodes(DrawingHandleScreen screenHandle) private void DrawAStarNodes(DrawingHandleScreen screenHandle, Box2 viewport)
{ {
var eyeManager = IoCManager.Resolve<IEyeManager>();
var viewport = eyeManager.GetWorldViewport();
foreach (var route in AStarRoutes) foreach (var route in AStarRoutes)
{ {
var highestgScore = route.GScores.Values.Max(); var highestgScore = route.GScores.Values.Max();
@@ -242,8 +389,7 @@ namespace Content.Client.GameObjects.EntitySystems.AI
continue; continue;
} }
var screenTile = eyeManager.WorldToScreen(tile); var screenTile = _eyeManager.WorldToScreen(tile);
var box = new UIBox2( var box = new UIBox2(
screenTile.X - 15.0f, screenTile.X - 15.0f,
screenTile.Y - 15.0f, screenTile.Y - 15.0f,
@@ -259,18 +405,15 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
} }
private void DrawJpsRoutes(DrawingHandleScreen screenHandle) private void DrawJpsRoutes(DrawingHandleScreen screenHandle, Box2 viewport)
{ {
var eyeManager = IoCManager.Resolve<IEyeManager>();
var viewport = eyeManager.GetWorldViewport();
foreach (var route in JpsRoutes) foreach (var route in JpsRoutes)
{ {
// Draw box on each tile of route // Draw box on each tile of route
foreach (var position in route.Route) foreach (var position in route.Route)
{ {
if (!viewport.Contains(position)) continue; if (!viewport.Contains(position)) continue;
var screenTile = eyeManager.WorldToScreen(position); var screenTile = _eyeManager.WorldToScreen(position);
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
var box = new UIBox2( var box = new UIBox2(
screenTile.X - 15.0f, screenTile.X - 15.0f,
@@ -282,11 +425,8 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
} }
private void DrawJpsNodes(DrawingHandleScreen screenHandle) private void DrawJpsNodes(DrawingHandleScreen screenHandle, Box2 viewport)
{ {
var eyeManager = IoCManager.Resolve<IEyeManager>();
var viewport = eyeManager.GetWorldViewport();
foreach (var route in JpsRoutes) foreach (var route in JpsRoutes)
{ {
foreach (var tile in route.JumpNodes) foreach (var tile in route.JumpNodes)
@@ -297,8 +437,7 @@ namespace Content.Client.GameObjects.EntitySystems.AI
continue; continue;
} }
var screenTile = eyeManager.WorldToScreen(tile); var screenTile = _eyeManager.WorldToScreen(tile);
var box = new UIBox2( var box = new UIBox2(
screenTile.X - 15.0f, screenTile.X - 15.0f,
screenTile.Y - 15.0f, screenTile.Y - 15.0f,
@@ -324,22 +463,33 @@ namespace Content.Client.GameObjects.EntitySystems.AI
} }
var screenHandle = (DrawingHandleScreen) handle; var screenHandle = (DrawingHandleScreen) handle;
var viewport = _eyeManager.GetWorldViewport();
if ((Modes & PathfindingDebugMode.Route) != 0) if ((Modes & PathfindingDebugMode.Route) != 0)
{ {
DrawAStarRoutes(screenHandle); DrawAStarRoutes(screenHandle, viewport);
DrawJpsRoutes(screenHandle); DrawJpsRoutes(screenHandle, viewport);
} }
if ((Modes & PathfindingDebugMode.Nodes) != 0) if ((Modes & PathfindingDebugMode.Nodes) != 0)
{ {
DrawAStarNodes(screenHandle); DrawAStarNodes(screenHandle, viewport);
DrawJpsNodes(screenHandle); DrawJpsNodes(screenHandle, viewport);
} }
if ((Modes & PathfindingDebugMode.Graph) != 0) if ((Modes & PathfindingDebugMode.Graph) != 0)
{ {
DrawGraph(screenHandle); DrawGraph(screenHandle, viewport);
}
if ((Modes & PathfindingDebugMode.CachedRegions) != 0)
{
DrawCachedRegions(screenHandle, viewport);
}
if ((Modes & PathfindingDebugMode.Regions) != 0)
{
DrawRegions(screenHandle, viewport);
} }
} }
} }
@@ -350,6 +500,8 @@ namespace Content.Client.GameObjects.EntitySystems.AI
Route = 1 << 0, Route = 1 << 0,
Graph = 1 << 1, Graph = 1 << 1,
Nodes = 1 << 2, Nodes = 1 << 2,
CachedRegions = 1 << 3,
Regions = 1 << 4,
} }
#endif #endif
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Clothing; using Content.Server.AI.Utility.Considerations.Clothing;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory; using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
considerationsManager.Get<CanPutTargetInHandsCon>() considerationsManager.Get<CanPutTargetInHandsCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.GLOVES, context) considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.GLOVES, context)
.InverseBoolCurve(context) .InverseBoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Clothing; using Content.Server.AI.Utility.Considerations.Clothing;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory; using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head
considerationsManager.Get<CanPutTargetInHandsCon>() considerationsManager.Get<CanPutTargetInHandsCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.HEAD, context) considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.HEAD, context)
.InverseBoolCurve(context) .InverseBoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Clothing; using Content.Server.AI.Utility.Considerations.Clothing;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory; using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
considerationsManager.Get<CanPutTargetInHandsCon>() considerationsManager.Get<CanPutTargetInHandsCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.OUTERCLOTHING, context) considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.OUTERCLOTHING, context)
.InverseBoolCurve(context) .InverseBoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Clothing; using Content.Server.AI.Utility.Considerations.Clothing;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory; using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
considerationsManager.Get<CanPutTargetInHandsCon>() considerationsManager.Get<CanPutTargetInHandsCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.SHOES, context) considerationsManager.Get<ClothingInInventoryCon>().Slot(EquipmentSlotDefines.SlotFlags.SHOES, context)
.InverseBoolCurve(context) .InverseBoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -7,6 +7,7 @@ using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat; using Content.Server.AI.Utility.Considerations.Combat;
using Content.Server.AI.Utility.Considerations.Combat.Melee; using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement; using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -79,6 +80,8 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
.QuadraticCurve(context, 1.0f, 0.5f, 0.0f, 0.0f), .QuadraticCurve(context, 1.0f, 0.5f, 0.0f, 0.0f),
considerationsManager.Get<MeleeWeaponDamageCon>() considerationsManager.Get<MeleeWeaponDamageCon>()
.QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f), .QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -42,8 +42,6 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
return new[] return new[]
{ {
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
considerationsManager.Get<FreeHandCon>() considerationsManager.Get<FreeHandCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<HasMeleeWeaponCon>() considerationsManager.Get<HasMeleeWeaponCon>()
@@ -54,6 +52,8 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
.QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f), .QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f),
considerationsManager.Get<MeleeWeaponSpeedCon>() considerationsManager.Get<MeleeWeaponSpeedCon>()
.QuadraticCurve(context, -1.0f, 0.5f, 1.0f, 0.0f), .QuadraticCurve(context, -1.0f, 0.5f, 1.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -6,6 +6,7 @@ using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat; using Content.Server.AI.Utility.Considerations.Combat;
using Content.Server.AI.Utility.Considerations.Combat.Melee; using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement; using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
@@ -73,6 +74,8 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
.QuadraticCurve(context, -1.0f, 1.0f, 1.02f, 0.0f), .QuadraticCurve(context, -1.0f, 1.0f, 1.02f, 0.0f),
considerationsManager.Get<TargetHealthCon>() considerationsManager.Get<TargetHealthCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, -0.02f), .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, -0.02f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
// TODO: Consider our Speed and Damage to compare this to using a weapon // TODO: Consider our Speed and Damage to compare this to using a weapon
// Also need to unequip our weapon if we have one (xenos can't hold one so no issue for now) // Also need to unequip our weapon if we have one (xenos can't hold one so no issue for now)
}; };

View File

@@ -4,9 +4,11 @@ using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory; using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Operators.Movement; using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement; using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.Utility.Considerations.State; using Content.Server.AI.Utility.Considerations.State;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Inventory; using Content.Server.AI.WorldState.States.Inventory;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -33,6 +35,13 @@ namespace Content.Server.AI.Utility.Actions.Idle
}); });
} }
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
var lastStorage = context.GetState<LastOpenedStorageState>();
context.GetState<TargetEntityState>().SetValue(lastStorage.GetValue());
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{ {
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>(); var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
@@ -43,7 +52,8 @@ namespace Content.Server.AI.Utility.Actions.Idle
.InverseBoolCurve(context), .InverseBoolCurve(context),
considerationsManager.Get<DistanceCon>() considerationsManager.Get<DistanceCon>()
.QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }

View File

@@ -7,6 +7,9 @@ using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.ActionBlocker; using Content.Server.AI.Utility.Considerations.ActionBlocker;
using Content.Server.AI.Utility.Considerations.Containers; using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Random;
@@ -29,12 +32,12 @@ namespace Content.Server.AI.Utility.Actions.Idle
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
var randomGrid = FindRandomGrid(); var robustRandom = IoCManager.Resolve<IRobustRandom>();
var randomGrid = FindRandomGrid(robustRandom);
float waitTime; float waitTime;
if (randomGrid != GridCoordinates.InvalidGrid) if (randomGrid != GridCoordinates.InvalidGrid)
{ {
var random = IoCManager.Resolve<IRobustRandom>(); waitTime = robustRandom.Next(3, 8);
waitTime = random.NextFloat() * 10;
} }
else else
{ {
@@ -56,32 +59,39 @@ namespace Content.Server.AI.Utility.Actions.Idle
{ {
considerationsManager.Get<CanMoveCon>() considerationsManager.Get<CanMoveCon>()
.BoolCurve(context), .BoolCurve(context),
}; };
} }
private GridCoordinates FindRandomGrid() private GridCoordinates FindRandomGrid(IRobustRandom robustRandom)
{ {
// Very inefficient (should weight each region by its node count) but better than the old system
var reachableSystem = EntitySystem.Get<AiReachableSystem>();
var reachableArgs = ReachableArgs.GetArgs(Owner);
var entityRegion = reachableSystem.GetRegion(Owner);
var reachableRegions = reachableSystem.GetReachableRegions(reachableArgs, entityRegion);
// TODO: When SetupOperators can fail this should be null and fail the setup.
if (reachableRegions.Count == 0)
{
return default;
}
var reachableNodes = new List<PathfindingNode>();
foreach (var region in reachableRegions)
{
foreach (var node in region.Nodes)
{
reachableNodes.Add(node);
}
}
var targetNode = robustRandom.Pick(reachableNodes);
var mapManager = IoCManager.Resolve<IMapManager>(); var mapManager = IoCManager.Resolve<IMapManager>();
var grid = mapManager.GetGrid(Owner.Transform.GridID); var grid = mapManager.GetGrid(Owner.Transform.GridID);
var targetGrid = grid.GridTileToLocal(targetNode.TileRef.GridIndices);
// Just find a random spot in bounds return targetGrid;
// If the grid's a single-tile wide but really tall this won't really work but eh future problem
var gridBounds = grid.WorldBounds;
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var newPosition = gridBounds.BottomLeft + new Vector2(
robustRandom.Next((int) gridBounds.Width),
robustRandom.Next((int) gridBounds.Height));
// Conversions blah blah
var mapIndex = grid.WorldToTile(grid.LocalToWorld(newPosition));
// Didn't find one? Fuck it we're not walkin' into space
if (grid.GetTileRef(mapIndex).Tile.IsEmpty)
{
return GridCoordinates.InvalidGrid;
}
var target = grid.GridTileToLocal(mapIndex);
return target;
} }
} }
} }

View File

@@ -40,8 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
return new[] return new[]
{ {
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
considerationsManager.Get<FreeHandCon>() considerationsManager.Get<FreeHandCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<ThirstCon>() considerationsManager.Get<ThirstCon>()
@@ -50,6 +48,8 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
.QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f),
considerationsManager.Get<DrinkValueCon>() considerationsManager.Get<DrinkValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f), .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }

View File

@@ -40,8 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food
return new[] return new[]
{ {
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
considerationsManager.Get<FreeHandCon>() considerationsManager.Get<FreeHandCon>()
.BoolCurve(context), .BoolCurve(context),
considerationsManager.Get<HungerCon>() considerationsManager.Get<HungerCon>()
@@ -50,6 +48,8 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food
.QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f),
considerationsManager.Get<FoodValueCon>() considerationsManager.Get<FoodValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f), .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
}; };
} }
} }

View File

@@ -1,13 +1,18 @@
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components; using Content.Server.GameObjects.Components;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.AI.Utility.Considerations.Containers namespace Content.Server.AI.Utility.Considerations.Containers
{ {
/// <summary> /// <summary>
/// Returns 1.0f if the item is freely accessible (e.g. in storage we can open, on ground, etc.) /// Returns 1.0f if the item is freely accessible (e.g. in storage we can open, on ground, etc.)
/// This can be expensive so consider using this last for the considerations
/// </summary> /// </summary>
public sealed class TargetAccessibleCon : Consideration public sealed class TargetAccessibleCon : Consideration
{ {
@@ -36,7 +41,9 @@ namespace Content.Server.AI.Utility.Considerations.Containers
} }
} }
return 1.0f; var owner = context.GetState<SelfState>().GetValue();
return EntitySystem.Get<AiReachableSystem>().CanAccess(owner, target, SharedInteractionSystem.InteractionRange) ? 1.0f : 0.0f;
} }
} }
} }

View File

@@ -0,0 +1,644 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Content.Shared.AI;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
/// <summary>
/// Determines whether an AI has access to a specific pathfinding node.
/// </summary>
/// Long-term can be used to do hierarchical pathfinding
[UsedImplicitly]
public sealed class AiReachableSystem : EntitySystem
{
/*
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
* The goal is so that we can more quickly discern if a specific node is reachable or not rather than
* Pathfinding the entire graph.
*
* There's a lot of different implementations of hierarchical or some variation of it: HPA*, PRA, HAA*, etc.
* (HPA* technically caches the edge nodes of each chunk), e.g. Rimworld, Factorio, etc.
* so we'll just write one with SS14's requirements in mind.
*
* There's probably a better data structure to use though you'd need to benchmark multiple ones to compare,
* at the very least on the memory side it could definitely be better.
*/
#pragma warning disable 649
[Dependency] private IMapManager _mapmanager;
[Dependency] private IGameTiming _gameTiming;
#pragma warning restore 649
private PathfindingSystem _pathfindingSystem;
/// <summary>
/// Queued region updates
/// </summary>
private HashSet<PathfindingChunk> _queuedUpdates = new HashSet<PathfindingChunk>();
// Oh god the nesting. Shouldn't need to go beyond this
/// <summary>
/// The corresponding regions for each PathfindingChunk.
/// Regions are groups of nodes with the same profile (for pathfinding purposes)
/// i.e. same collision, not-space, same access, etc.
/// </summary>
private Dictionary<GridId, Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>> _regions =
new Dictionary<GridId, Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>>();
/// <summary>
/// Minimum time for the cached reachable regions to be stored
/// </summary>
private const float MinCacheTime = 1.0f;
// Cache what regions are accessible from this region. Cached per ReachableArgs
// so multiple entities in the same region with the same args should all be able to share their reachable lookup
// Also need to store when we cached it to know if it's stale if the chunks have updated
// TODO: There's probably a more memory-efficient way to cache this
// Then again, there's likely also a more memory-efficient way to implement regions.
// Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
// Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
private Dictionary<ReachableArgs, Dictionary<PathfindingRegion, (TimeSpan CacheTime, HashSet<PathfindingRegion> Regions)>> _cachedAccessible =
new Dictionary<ReachableArgs, Dictionary<PathfindingRegion, (TimeSpan, HashSet<PathfindingRegion>)>>();
#if DEBUG
private int _runningCacheIdx = 0;
#endif
public override void Initialize()
{
_pathfindingSystem = Get<PathfindingSystem>();
SubscribeLocalEvent<PathfindingChunkUpdateMessage>(RecalculateNodeRegions);
#if DEBUG
SubscribeLocalEvent<PlayerAttachSystemMessage>(SendDebugMessage);
#endif
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var chunk in _queuedUpdates)
{
GenerateRegions(chunk);
}
#if DEBUG
if (_queuedUpdates.Count > 0)
{
foreach (var (gridId, regs) in _regions)
{
if (regs.Count > 0)
{
SendRegionsDebugMessage(gridId);
}
}
}
#endif
_queuedUpdates.Clear();
}
public override void Shutdown()
{
base.Shutdown();
_queuedUpdates.Clear();
_regions.Clear();
_cachedAccessible.Clear();
}
private void RecalculateNodeRegions(PathfindingChunkUpdateMessage message)
{
// TODO: Only need to do changed nodes ideally
// For now this is fine but it's a low-hanging fruit optimisation
_queuedUpdates.Add(message.Chunk);
}
/// <summary>
/// Can the entity reach the target?
/// </summary>
/// First it does a quick check to see if there are any traversable nodes in range.
/// Then it will go through the regions to try and see if there's a region connection between the target and itself
/// Will used a cached region if available
/// <param name="entity"></param>
/// <param name="target"></param>
/// <param name="range"></param>
/// <returns></returns>
public bool CanAccess(IEntity entity, IEntity target, float range = 0.0f)
{
var targetTile = _mapmanager.GetGrid(target.Transform.GridID).GetTileRef(target.Transform.GridPosition);
var targetNode = _pathfindingSystem.GetNode(targetTile);
var collisionMask = 0;
if (entity.TryGetComponent(out CollidableComponent collidableComponent))
{
collisionMask = collidableComponent.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.Uid, access, collisionMask, default, targetTile, range);
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
{
targetNode = node;
}
}
return CanAccess(entity, targetNode);
}
public bool CanAccess(IEntity entity, PathfindingNode targetNode)
{
if (entity.Transform.GridID != targetNode.TileRef.GridIndex)
{
return false;
}
var entityTile = _mapmanager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition);
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);
var reachableRegions = GetReachableRegions(reachableArgs, targetRegion);
return reachableRegions.Contains(entityRegion);
}
/// <summary>
/// Retrieve the reachable regions
/// </summary>
/// <param name="reachableArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
public HashSet<PathfindingRegion> GetReachableRegions(ReachableArgs reachableArgs, PathfindingRegion region)
{
// if we're on a node that's not tracked at all atm then region will be null
if (region == null)
{
return new HashSet<PathfindingRegion>();
}
var cachedArgs = GetCachedArgs(reachableArgs);
(TimeSpan CacheTime, HashSet<PathfindingRegion> Regions) cached;
if (!IsCacheValid(cachedArgs, region))
{
cached = GetVisionReachable(cachedArgs, region);
_cachedAccessible[cachedArgs][region] = cached;
#if DEBUG
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, false);
#endif
}
else
{
cached = _cachedAccessible[cachedArgs][region];
#if DEBUG
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, true);
#endif
}
return cached.Regions;
}
/// <summary>
/// Get any adequate cached args if possible, otherwise just use ours
/// </summary>
/// Essentially any args that have the same access AND >= our vision radius can be used
/// <param name="accessibleArgs"></param>
/// <returns></returns>
private ReachableArgs GetCachedArgs(ReachableArgs accessibleArgs)
{
ReachableArgs foundArgs = null;
foreach (var (cachedAccessible, _) in _cachedAccessible)
{
if (Equals(cachedAccessible.Access, accessibleArgs.Access) &&
cachedAccessible.CollisionMask == accessibleArgs.CollisionMask &&
cachedAccessible.VisionRadius <= accessibleArgs.VisionRadius)
{
foundArgs = cachedAccessible;
break;
}
}
return foundArgs ?? accessibleArgs;
}
/// <summary>
/// Checks whether there's a valid cache for our accessibility args.
/// Most regular mobs can share their cached accessibility with each other
/// </summary>
/// Will also remove it from the cache if it is invalid
/// <param name="accessibleArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
private bool IsCacheValid(ReachableArgs accessibleArgs, PathfindingRegion region)
{
if (!_cachedAccessible.TryGetValue(accessibleArgs, out var cachedArgs))
{
_cachedAccessible.Add(accessibleArgs, new Dictionary<PathfindingRegion, (TimeSpan, HashSet<PathfindingRegion>)>());
return false;
}
if (!cachedArgs.TryGetValue(region, out var regionCache))
{
return false;
}
// Just so we don't invalidate the cache every tick we'll store it for a minimum amount of time
var currentTime = _gameTiming.CurTime;
if ((currentTime - regionCache.CacheTime).TotalSeconds < MinCacheTime)
{
return true;
}
var checkedAccess = new HashSet<PathfindingRegion>();
// Check if cache is stale
foreach (var accessibleRegion in regionCache.Regions)
{
if (checkedAccess.Contains(accessibleRegion)) continue;
// Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections)
// TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up
if (accessibleRegion.ParentChunk.LastUpdate > regionCache.CacheTime)
{
// Remove the stale cache, to be updated later
_cachedAccessible[accessibleArgs].Remove(region);
return false;
}
foreach (var neighbor in accessibleRegion.Neighbors)
{
if (checkedAccess.Contains(neighbor)) continue;
if (neighbor.ParentChunk.LastUpdate > regionCache.CacheTime)
{
_cachedAccessible[accessibleArgs].Remove(region);
return false;
}
checkedAccess.Add(neighbor);
}
checkedAccess.Add(accessibleRegion);
}
return true;
}
/// <summary>
/// Caches the entity's nearby accessible regions in vision radius
/// </summary>
/// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
/// <param name="reachableArgs"></param>
/// <param name="entityRegion"></param>
private (TimeSpan, HashSet<PathfindingRegion>) GetVisionReachable(ReachableArgs reachableArgs, PathfindingRegion entityRegion)
{
var openSet = new Queue<PathfindingRegion>();
openSet.Enqueue(entityRegion);
var closedSet = new HashSet<PathfindingRegion>();
var accessible = new HashSet<PathfindingRegion> {entityRegion};
while (openSet.Count > 0)
{
var region = openSet.Dequeue();
closedSet.Add(region);
foreach (var neighbor in region.Neighbors)
{
if (closedSet.Contains(neighbor))
{
continue;
}
// Distance is an approximation here so we'll be generous with it
// TODO: Could do better; the fewer nodes the better it is.
if (!neighbor.RegionTraversable(reachableArgs) ||
neighbor.Distance(entityRegion) > reachableArgs.VisionRadius + 1)
{
closedSet.Add(neighbor);
continue;
}
openSet.Enqueue(neighbor);
accessible.Add(neighbor);
}
}
return (_gameTiming.CurTime, accessible);
}
/// <summary>
/// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
/// </summary>
/// Implicitly they would've already been merged if possible
/// <param name="region"></param>
/// <param name="node"></param>
private void UpdateRegionEdge(PathfindingRegion region, PathfindingNode node)
{
DebugTools.Assert(region.Nodes.Contains(node));
// Originally I tried just doing bottom and left but that doesn't work as the chunk update order is not guaranteed
var checkDirections = new[] {Direction.East, Direction.South, Direction.West, Direction.North};
foreach (var direction in checkDirections)
{
var directionNode = node.GetNeighbor(direction);
if (directionNode == null) continue;
var directionRegion = GetRegion(directionNode);
if (directionRegion == null || directionRegion == region) continue;
region.Neighbors.Add(directionRegion);
directionRegion.Neighbors.Add(region);
}
}
/// <summary>
/// Get the current region for this entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public PathfindingRegion GetRegion(IEntity entity)
{
var entityTile = _mapmanager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition);
var entityNode = _pathfindingSystem.GetNode(entityTile);
return GetRegion(entityNode);
}
/// <summary>
/// Get the current region for this node
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public PathfindingRegion GetRegion(PathfindingNode node)
{
// Not sure on the best way to optimise this
// On the one hand, just storing each node's region is faster buuutttt muh memory
// On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes?
// Someone smarter than me will know better
var parentChunk = node.ParentChunk;
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
if (!_regions[parentChunk.GridId].TryGetValue(parentChunk, out var regions))
{
return null;
}
foreach (var region in regions)
{
if (region.Nodes.Contains(node))
{
return region;
}
}
// Longer term this will probably be guaranteed a region but for now space etc. are no region
return null;
}
/// <summary>
/// Add this node to the relevant region.
/// </summary>
/// <param name="node"></param>
/// <param name="existingRegions"></param>
/// <param name="x">This is already calculated in advance so may as well re-use it</param>
/// <param name="y">This is already calculated in advance so may as well re-use it</param>
/// <returns></returns>
private PathfindingRegion CalculateNode(PathfindingNode node, Dictionary<PathfindingNode, PathfindingRegion> existingRegions, int x, int y)
{
DebugTools.Assert(_regions.ContainsKey(node.ParentChunk.GridId));
DebugTools.Assert(_regions[node.ParentChunk.GridId].ContainsKey(node.ParentChunk));
// TODO For now we don't have these regions but longer-term yeah sure
if (node.BlockedCollisionMask != 0x0 || node.TileRef.Tile.IsEmpty)
{
return null;
}
var parentChunk = node.ParentChunk;
// Doors will be their own separate region
// We won't store them in existingRegions so they don't show up and can't be connected to (at least for now)
if (node.AccessReaders.Count > 0)
{
var region = new PathfindingRegion(node, new HashSet<PathfindingNode>(1) {node}, true);
_regions[parentChunk.GridId][parentChunk].Add(region);
UpdateRegionEdge(region, node);
return region;
}
// Relative x and y of the chunk
// If one of our bottom / left neighbors are in a region try to join them
// Otherwise, make our own region.
var leftNeighbor = x > 0 ? parentChunk.Nodes[x - 1, y] : null;
var bottomNeighbor = y > 0 ? parentChunk.Nodes[x, y - 1] : null;
PathfindingRegion leftRegion;
PathfindingRegion bottomRegion;
// We'll check if our left or down neighbors are already in a region and join them
// Is left node valid to connect to
if (leftNeighbor != null &&
existingRegions.TryGetValue(leftNeighbor, out leftRegion) &&
!leftRegion.IsDoor)
{
// We'll try and connect the left node's region to the bottom region if they're separate (yay merge)
if (bottomNeighbor != null && existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
!bottomRegion.IsDoor)
{
bottomRegion.Add(node);
existingRegions.Add(node, bottomRegion);
MergeInto(leftRegion, bottomRegion);
return bottomRegion;
}
leftRegion.Add(node);
existingRegions.Add(node, leftRegion);
UpdateRegionEdge(leftRegion, node);
return leftRegion;
}
//Is bottom node valid to connect to
if (bottomNeighbor != null &&
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
!bottomRegion.IsDoor)
{
bottomRegion.Add(node);
existingRegions.Add(node, bottomRegion);
UpdateRegionEdge(bottomRegion, node);
return bottomRegion;
}
// If we can't join an existing region then we'll make our own
var newRegion = new PathfindingRegion(node, new HashSet<PathfindingNode> {node}, node.AccessReaders.Count > 0);
_regions[parentChunk.GridId][parentChunk].Add(newRegion);
existingRegions.Add(node, newRegion);
UpdateRegionEdge(newRegion, node);
return newRegion;
}
/// <summary>
/// Combines the two regions into one bigger region
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
private void MergeInto(PathfindingRegion source, PathfindingRegion target)
{
DebugTools.AssertNotNull(source);
DebugTools.AssertNotNull(target);
foreach (var node in source.Nodes)
{
target.Add(node);
}
source.Shutdown();
_regions[source.ParentChunk.GridId][source.ParentChunk].Remove(source);
foreach (var node in target.Nodes)
{
UpdateRegionEdge(target, node);
}
}
/// <summary>
/// Generate all of the regions within a chunk
/// </summary>
/// These can't across over into another chunk and doors are their own region
/// <param name="chunk"></param>
private void GenerateRegions(PathfindingChunk chunk)
{
if (!_regions.ContainsKey(chunk.GridId))
{
_regions.Add(chunk.GridId, new Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>());
}
if (_regions[chunk.GridId].TryGetValue(chunk, out var regions))
{
foreach (var region in regions)
{
region.Shutdown();
}
_regions[chunk.GridId].Remove(chunk);
}
// Temporarily store the corresponding region for each node
// Makes merging regions or adding nodes to existing regions neater.
var nodeRegions = new Dictionary<PathfindingNode, PathfindingRegion>();
var chunkRegions = new HashSet<PathfindingRegion>();
_regions[chunk.GridId].Add(chunk, chunkRegions);
for (var y = 0; y < PathfindingChunk.ChunkSize; y++)
{
for (var x = 0; x < PathfindingChunk.ChunkSize; x++)
{
var node = chunk.Nodes[x, y];
var region = CalculateNode(node, nodeRegions, 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
SendRegionsDebugMessage(chunk.GridId);
#endif
}
#if DEBUG
private void SendDebugMessage(PlayerAttachSystemMessage message)
{
var playerGrid = message.Entity.Transform.GridID;
SendRegionsDebugMessage(playerGrid);
}
private void SendRegionsDebugMessage(GridId gridId)
{
var grid = _mapmanager.GetGrid(gridId);
// Chunk / Regions / Nodes
var debugResult = new Dictionary<int, Dictionary<int, List<Vector2>>>();
var chunkIdx = 0;
var regionIdx = 0;
foreach (var (_, regions) in _regions[gridId])
{
var debugRegions = new Dictionary<int, List<Vector2>>();
debugResult.Add(chunkIdx, debugRegions);
foreach (var region in regions)
{
var debugRegionNodes = new List<Vector2>(region.Nodes.Count);
debugResult[chunkIdx].Add(regionIdx, debugRegionNodes);
foreach (var node in region.Nodes)
{
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(_mapmanager);
debugRegionNodes.Add(nodeVector);
}
regionIdx++;
}
chunkIdx++;
}
RaiseNetworkEvent(new SharedAiDebug.ReachableChunkRegionsDebugMessage(gridId, debugResult));
}
/// <summary>
/// Sent whenever the reachable cache for a particular mob is built or retrieved
/// </summary>
/// <param name="gridId"></param>
/// <param name="regions"></param>
/// <param name="cached"></param>
private void SendRegionCacheMessage(GridId gridId, IEnumerable<PathfindingRegion> regions, bool cached)
{
var grid = _mapmanager.GetGrid(gridId);
var debugResult = new Dictionary<int, List<Vector2>>();
foreach (var region in regions)
{
debugResult.Add(_runningCacheIdx, new List<Vector2>());
foreach (var node in region.Nodes)
{
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(_mapmanager);
debugResult[_runningCacheIdx].Add(nodeVector);
}
_runningCacheIdx++;
}
RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached));
}
#endif
}
}

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
/// <summary>
/// A group of homogenous PathfindingNodes inside a single chunk
/// </summary>
/// Makes the graph smaller and quicker to traverse
public class PathfindingRegion : IEquatable<PathfindingRegion>
{
/// <summary>
/// Bottom-left reference node of the region
/// </summary>
public PathfindingNode OriginNode { get; }
// The shape may be anything within the bounds of a chunk, this is just a quick way to do a bounds-check
/// <summary>
/// Maximum width of the nodes
/// </summary>
public int Height { get; private set; } = 1;
/// <summary>
/// Maximum width of the nodes
/// </summary>
public int Width { get; private set; } = 1;
public PathfindingChunk ParentChunk => OriginNode.ParentChunk;
public HashSet<PathfindingRegion> Neighbors { get; } = new HashSet<PathfindingRegion>();
public bool IsDoor { get; }
public HashSet<PathfindingNode> Nodes => _nodes;
private HashSet<PathfindingNode> _nodes;
public PathfindingRegion(PathfindingNode originNode, HashSet<PathfindingNode> nodes, bool isDoor = false)
{
OriginNode = originNode;
_nodes = nodes;
IsDoor = isDoor;
}
public void Shutdown()
{
// Tell our neighbors we no longer exist ;-/
var neighbors = new List<PathfindingRegion>(Neighbors);
for (var i = 0; i < neighbors.Count; i++)
{
var neighbor = neighbors[i];
neighbor.Neighbors.Remove(this);
}
}
/// <summary>
/// Roughly how far away another region is by nearest node
/// </summary>
/// <param name="otherRegion"></param>
/// <returns></returns>
public float Distance(PathfindingRegion otherRegion)
{
// JANK
var xDistance = otherRegion.OriginNode.TileRef.X - OriginNode.TileRef.X;
var yDistance = otherRegion.OriginNode.TileRef.Y - OriginNode.TileRef.Y;
if (xDistance > 0)
{
xDistance -= Width;
}
else if (xDistance < 0)
{
xDistance = Math.Abs(xDistance + otherRegion.Width);
}
if (yDistance > 0)
{
yDistance -= Height;
}
else if (yDistance < 0)
{
yDistance = Math.Abs(yDistance + otherRegion.Height);
}
return PathfindingHelpers.OctileDistance(xDistance, yDistance);
}
/// <summary>
/// Can the given args can traverse this region?
/// </summary>
/// <param name="reachableArgs"></param>
/// <returns></returns>
public bool RegionTraversable(ReachableArgs reachableArgs)
{
// The assumption is that all nodes in a region have the same pathfinding traits
// As such we can just use the origin node for checking.
return PathfindingHelpers.Traversable(reachableArgs.CollisionMask, reachableArgs.Access,
OriginNode);
}
public void Add(PathfindingNode node)
{
var xWidth = Math.Abs(node.TileRef.X - OriginNode.TileRef.X);
var yHeight = Math.Abs(node.TileRef.Y - OriginNode.TileRef.Y);
if (xWidth > Width)
{
Width = xWidth;
}
if (yHeight > Height)
{
Height = yHeight;
}
_nodes.Add(node);
}
// HashSet wasn't working correctly so uhh we got this.
public bool Equals(PathfindingRegion other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
return GetHashCode() == other.GetHashCode();
}
public override int GetHashCode()
{
return OriginNode.GetHashCode();
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Movement;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
public sealed class ReachableArgs
{
public float VisionRadius { get; set; }
public ICollection<string> Access { get; }
public int CollisionMask { get; }
public ReachableArgs(float visionRadius, ICollection<string> access, int collisionMask)
{
VisionRadius = visionRadius;
Access = access;
CollisionMask = collisionMask;
}
/// <summary>
/// Get appropriate args for a particular entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static ReachableArgs GetArgs(IEntity entity)
{
var collisionMask = 0;
if (entity.TryGetComponent(out CollidableComponent collidableComponent))
{
collisionMask = collidableComponent.CollisionMask;
}
var access = AccessReader.FindAccessTags(entity);
var visionRadius = entity.GetComponent<AiControllerComponent>().VisionRadius;
return new ReachableArgs(visionRadius, access, collisionMask);
}
}
}

View File

@@ -3,23 +3,37 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.EntitySystems.Pathfinding; using Content.Server.GameObjects.EntitySystems.Pathfinding;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{ {
public class PathfindingChunkUpdateMessage : EntitySystemMessage
{
public PathfindingChunk Chunk { get; }
public PathfindingChunkUpdateMessage(PathfindingChunk chunk)
{
Chunk = chunk;
}
}
public class PathfindingChunk public class PathfindingChunk
{ {
public TimeSpan LastUpdate { get; private set; }
public GridId GridId { get; } public GridId GridId { get; }
public MapIndices Indices => _indices; public MapIndices Indices => _indices;
private readonly MapIndices _indices; private readonly MapIndices _indices;
// Nodes per chunk row // Nodes per chunk row
public static int ChunkSize => 16; public static int ChunkSize => 8;
public PathfindingNode[,] Nodes => _nodes; public PathfindingNode[,] Nodes => _nodes;
private PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize]; private PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
@@ -29,16 +43,28 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
_indices = indices; _indices = indices;
} }
public void Initialize(IMapGrid grid) public void Initialize(IMapGrid mapGrid)
{ {
for (var x = 0; x < ChunkSize; x++) for (var x = 0; x < ChunkSize; x++)
{ {
for (var y = 0; y < ChunkSize; y++) for (var y = 0; y < ChunkSize; y++)
{ {
var tileRef = grid.GetTileRef(new MapIndices(x + _indices.X, y + _indices.Y)); var tileRef = mapGrid.GetTileRef(new MapIndices(x + _indices.X, y + _indices.Y));
CreateNode(tileRef); CreateNode(tileRef);
} }
} }
Dirty();
}
/// <summary>
/// Only called when blockers change (i.e. un-anchored physics objects don't trigger)
/// </summary>
public void Dirty()
{
LastUpdate = IoCManager.Resolve<IGameTiming>().CurTime;
IoCManager.Resolve<IEntityManager>().EventBus
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
} }
public IEnumerable<PathfindingChunk> GetNeighbors() public IEnumerable<PathfindingChunk> GetNeighbors()

View File

@@ -221,6 +221,16 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
return result; return result;
} }
public static float OctileDistance(int dstX, int dstY)
{
if (dstX > dstY)
{
return 1.4f * dstY + (dstX - dstY);
}
return 1.4f * dstX + (dstY - dstX);
}
public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode) public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode)
{ {
// "Fast Euclidean" / octile. // "Fast Euclidean" / octile.

View File

@@ -88,9 +88,159 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding
} }
} }
public PathfindingNode GetNeighbor(Direction direction)
{
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
MapIndices neighborMapIndices;
switch (direction)
{
case Direction.East:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset];
}
neighborMapIndices = new MapIndices(TileRef.X + 1, TileRef.Y);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.NorthEast:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset + 1];
}
neighborMapIndices = new MapIndices(TileRef.X + 1, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.North:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset, chunkYOffset + 1];
}
neighborMapIndices = new MapIndices(TileRef.X, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.NorthWest:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset + 1];
}
neighborMapIndices = new MapIndices(TileRef.X - 1, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.West:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset];
}
neighborMapIndices = new MapIndices(TileRef.X - 1, TileRef.Y);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.SouthWest:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset - 1];
}
neighborMapIndices = new MapIndices(TileRef.X - 1, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.South:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset, chunkYOffset - 1];
}
neighborMapIndices = new MapIndices(TileRef.X, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.SouthEast:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset - 1];
}
neighborMapIndices = new MapIndices(TileRef.X + 1, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborMapIndices))
{
return neighbor.Nodes[neighborMapIndices.X - neighbor.Indices.X,
neighborMapIndices.Y - neighbor.Indices.Y];
}
}
return null;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
}
public void UpdateTile(TileRef newTile) public void UpdateTile(TileRef newTile)
{ {
TileRef = newTile; TileRef = newTile;
ParentChunk.Dirty();
} }
/// <summary> /// <summary>
@@ -111,6 +261,7 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding
if (entity.TryGetComponent(out AccessReader accessReader) && !_accessReaders.ContainsKey(entity.Uid)) if (entity.TryGetComponent(out AccessReader accessReader) && !_accessReaders.ContainsKey(entity.Uid))
{ {
_accessReaders.Add(entity.Uid, accessReader); _accessReaders.Add(entity.Uid, accessReader);
ParentChunk.Dirty();
} }
return; return;
} }
@@ -126,6 +277,7 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding
{ {
_blockedCollidables.TryAdd(entity.Uid, collidableComponent.CollisionLayer); _blockedCollidables.TryAdd(entity.Uid, collidableComponent.CollisionLayer);
GenerateMask(); GenerateMask();
ParentChunk.Dirty();
} }
} }
} }
@@ -147,11 +299,13 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding
else if (_accessReaders.ContainsKey(entity.Uid)) else if (_accessReaders.ContainsKey(entity.Uid))
{ {
_accessReaders.Remove(entity.Uid); _accessReaders.Remove(entity.Uid);
ParentChunk.Dirty();
} }
else if (_blockedCollidables.ContainsKey(entity.Uid)) else if (_blockedCollidables.ContainsKey(entity.Uid))
{ {
_blockedCollidables.Remove(entity.Uid); _blockedCollidables.Remove(entity.Uid);
GenerateMask(); GenerateMask();
ParentChunk.Dirty();
} }
} }

View File

@@ -178,6 +178,17 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
return newChunk; return newChunk;
} }
/// <summary>
/// Get the entity's tile position, then get the corresponding node
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public PathfindingNode GetNode(IEntity entity)
{
var tile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition);
return GetNode(tile);
}
/// <summary> /// <summary>
/// Return the corresponding PathfindingNode for this tile /// Return the corresponding PathfindingNode for this tile
/// </summary> /// </summary>

View File

@@ -140,5 +140,34 @@ namespace Content.Shared.AI
} }
} }
#endregion #endregion
#region Reachable Debug
[Serializable, NetSerializable]
public sealed class ReachableChunkRegionsDebugMessage : EntitySystemMessage
{
public GridId GridId { get; }
public Dictionary<int, Dictionary<int, List<Vector2>>> Regions { get; }
public ReachableChunkRegionsDebugMessage(GridId gridId, Dictionary<int, Dictionary<int, List<Vector2>>> regions)
{
GridId = gridId;
Regions = regions;
}
}
[Serializable, NetSerializable]
public sealed class ReachableCacheDebugMessage : EntitySystemMessage
{
public GridId GridId { get; }
public Dictionary<int, List<Vector2>> Regions { get; }
public bool Cached { get; }
public ReachableCacheDebugMessage(GridId gridId, Dictionary<int, List<Vector2>> regions, bool cached)
{
GridId = gridId;
Regions = regions;
Cached = cached;
}
}
#endregion
} }
} }