diff --git a/Content.Client/Commands/DebugPathfindingCommand.cs b/Content.Client/Commands/DebugPathfindingCommand.cs index adc4760767..f181823141 100644 --- a/Content.Client/Commands/DebugPathfindingCommand.cs +++ b/Content.Client/Commands/DebugPathfindingCommand.cs @@ -13,7 +13,7 @@ namespace Content.Client.Commands // ReSharper disable once StringLiteralTypo public string Command => "pathfinder"; 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) { @@ -49,6 +49,17 @@ namespace Content.Client.Commands 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; } diff --git a/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs index 746a9b92e2..c35dbe93d7 100644 --- a/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs @@ -7,9 +7,11 @@ using Robust.Client.Graphics.Overlays; using Robust.Client.Graphics.Shaders; using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.Overlays; +using Robust.Client.Player; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.Random; using Robust.Shared.IoC; +using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -30,6 +32,16 @@ namespace Content.Client.GameObjects.EntitySystems.AI 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) @@ -62,10 +74,20 @@ namespace Content.Client.GameObjects.EntitySystems.AI private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message) { - if ((_modes & PathfindingDebugMode.Graph) != 0) - { - _overlay.UpdateGraph(message.Graph); - } + EnableOverlay(); + _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() @@ -114,6 +136,9 @@ namespace Content.Client.GameObjects.EntitySystems.AI var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage(); 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) @@ -144,6 +169,9 @@ namespace Content.Client.GameObjects.EntitySystems.AI 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 public override OverlaySpace Space => OverlaySpace.ScreenSpace; @@ -152,7 +180,21 @@ namespace Content.Client.GameObjects.EntitySystems.AI // Graph debugging public readonly Dictionary> Graph = new Dictionary>(); private readonly Dictionary _graphColors = new Dictionary(); - + + // Cached regions + public readonly Dictionary>> CachedRegions = + new Dictionary>>(); + + private readonly Dictionary> _cachedRegionColors = + new Dictionary>(); + + // Regions + public readonly Dictionary>>> Regions = + new Dictionary>>>(); + + private readonly Dictionary>> _regionColors = + new Dictionary>>(); + // Route debugging // As each pathfinder is very different you'll likely want to draw them completely different public readonly List AStarRoutes = new List(); @@ -161,8 +203,11 @@ namespace Content.Client.GameObjects.EntitySystems.AI public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay)) { Shader = IoCManager.Resolve().Index("unshaded").Instance(); + _eyeManager = IoCManager.Resolve(); + _playerManager = IoCManager.Resolve(); } + #region Graph public void UpdateGraph(Dictionary> graph) { 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(); - var viewport = IoCManager.Resolve().GetWorldViewport(); - foreach (var (chunk, nodes) in Graph) { foreach (var tile in nodes) { if (!viewport.Contains(tile)) continue; - var screenTile = eyeManager.WorldToScreen(tile); - + var screenTile = _eyeManager.WorldToScreen(tile); var box = new UIBox2( screenTile.X - 15.0f, screenTile.Y - 15.0f, @@ -199,21 +240,130 @@ namespace Content.Client.GameObjects.EntitySystems.AI } } } + #endregion - #region pathfinder - - private void DrawAStarRoutes(DrawingHandleScreen screenHandle) + #region Regions + //Server side debugger should increment every region + public void UpdateCachedRegions(GridId gridId, Dictionary> messageRegions, bool cached) { - var eyeManager = IoCManager.Resolve(); - var viewport = eyeManager.GetWorldViewport(); + 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.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>> 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.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) { // Draw box on each tile of route foreach (var position in route.Route) { if (!viewport.Contains(position)) continue; - var screenTile = eyeManager.WorldToScreen(position); + var screenTile = _eyeManager.WorldToScreen(position); // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); var box = new UIBox2( 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(); - var viewport = eyeManager.GetWorldViewport(); - foreach (var route in AStarRoutes) { var highestgScore = route.GScores.Values.Max(); @@ -242,8 +389,7 @@ namespace Content.Client.GameObjects.EntitySystems.AI continue; } - var screenTile = eyeManager.WorldToScreen(tile); - + var screenTile = _eyeManager.WorldToScreen(tile); var box = new UIBox2( screenTile.X - 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(); - var viewport = eyeManager.GetWorldViewport(); - 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); + var screenTile = _eyeManager.WorldToScreen(position); // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); var box = new UIBox2( 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(); - var viewport = eyeManager.GetWorldViewport(); - foreach (var route in JpsRoutes) { foreach (var tile in route.JumpNodes) @@ -297,8 +437,7 @@ namespace Content.Client.GameObjects.EntitySystems.AI continue; } - var screenTile = eyeManager.WorldToScreen(tile); - + var screenTile = _eyeManager.WorldToScreen(tile); var box = new UIBox2( screenTile.X - 15.0f, screenTile.Y - 15.0f, @@ -324,22 +463,33 @@ namespace Content.Client.GameObjects.EntitySystems.AI } var screenHandle = (DrawingHandleScreen) handle; + var viewport = _eyeManager.GetWorldViewport(); if ((Modes & PathfindingDebugMode.Route) != 0) { - DrawAStarRoutes(screenHandle); - DrawJpsRoutes(screenHandle); + DrawAStarRoutes(screenHandle, viewport); + DrawJpsRoutes(screenHandle, viewport); } if ((Modes & PathfindingDebugMode.Nodes) != 0) { - DrawAStarNodes(screenHandle); - DrawJpsNodes(screenHandle); + DrawAStarNodes(screenHandle, viewport); + DrawJpsNodes(screenHandle, viewport); } 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, Graph = 1 << 1, Nodes = 1 << 2, + CachedRegions = 1 << 3, + Regions = 1 << 4, } #endif } diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs index 703ed5362f..736db66a7f 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Utility.Considerations; 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.WorldState; using Content.Server.AI.WorldState.States; @@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves considerationsManager.Get() .BoolCurve(context), considerationsManager.Get().Slot(EquipmentSlotDefines.SlotFlags.GLOVES, context) - .InverseBoolCurve(context) + .InverseBoolCurve(context), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs index 0c06d707d5..038dd54a6b 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Utility.Considerations; 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.WorldState; using Content.Server.AI.WorldState.States; @@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head considerationsManager.Get() .BoolCurve(context), considerationsManager.Get().Slot(EquipmentSlotDefines.SlotFlags.HEAD, context) - .InverseBoolCurve(context) + .InverseBoolCurve(context), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs index a192fda3e7..0eb4ff1cbe 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Utility.Considerations; 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.WorldState; using Content.Server.AI.WorldState.States; @@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing considerationsManager.Get() .BoolCurve(context), considerationsManager.Get().Slot(EquipmentSlotDefines.SlotFlags.OUTERCLOTHING, context) - .InverseBoolCurve(context) + .InverseBoolCurve(context), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs index 60e4d00196..94dd05efd4 100644 --- a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs +++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Content.Server.AI.Operators.Sequences; using Content.Server.AI.Utility.Considerations; 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.WorldState; using Content.Server.AI.WorldState.States; @@ -44,7 +45,9 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes considerationsManager.Get() .BoolCurve(context), considerationsManager.Get().Slot(EquipmentSlotDefines.SlotFlags.SHOES, context) - .InverseBoolCurve(context) + .InverseBoolCurve(context), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs index de65a16279..5b9ce74248 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeWeaponAttackEntity.cs @@ -7,6 +7,7 @@ using Content.Server.AI.Operators.Movement; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations.Combat; 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.WorldState; 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), considerationsManager.Get() .QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs index 1d1ec0ec96..12b502b4dd 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs @@ -42,8 +42,6 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee return new[] { - considerationsManager.Get() - .BoolCurve(context), considerationsManager.Get() .BoolCurve(context), considerationsManager.Get() @@ -54,6 +52,8 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee .QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f), considerationsManager.Get() .QuadraticCurve(context, -1.0f, 0.5f, 1.0f, 0.0f), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs index 7355e216db..d3593cc982 100644 --- a/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/UnarmedAttackEntity.cs @@ -6,6 +6,7 @@ using Content.Server.AI.Operators.Movement; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations.Combat; 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.WorldState; 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), considerationsManager.Get() .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, -0.02f), + considerationsManager.Get() + .BoolCurve(context), // 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) }; diff --git a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs index cd311a3270..4d9fa36f8a 100644 --- a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs +++ b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs @@ -4,9 +4,11 @@ using Content.Server.AI.Operators; using Content.Server.AI.Operators.Inventory; using Content.Server.AI.Operators.Movement; 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.State; using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States.Inventory; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; @@ -31,8 +33,15 @@ namespace Content.Server.AI.Utility.Actions.Idle new MoveToEntityOperator(Owner, lastStorage), new CloseLastStorageOperator(Owner), }); - } - + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + var lastStorage = context.GetState(); + context.GetState().SetValue(lastStorage.GetValue()); + } + protected override IReadOnlyCollection> GetConsiderations(Blackboard context) { var considerationsManager = IoCManager.Resolve(); @@ -43,7 +52,8 @@ namespace Content.Server.AI.Utility.Actions.Idle .InverseBoolCurve(context), considerationsManager.Get() .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), - + considerationsManager.Get() + .BoolCurve(context), }; } diff --git a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs index 3abb90dd3f..ce68e46d78 100644 --- a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs +++ b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs @@ -7,6 +7,9 @@ using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations.ActionBlocker; using Content.Server.AI.Utility.Considerations.Containers; 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.Map; using Robust.Shared.Interfaces.Random; @@ -29,12 +32,12 @@ namespace Content.Server.AI.Utility.Actions.Idle public override void SetupOperators(Blackboard context) { - var randomGrid = FindRandomGrid(); + var robustRandom = IoCManager.Resolve(); + var randomGrid = FindRandomGrid(robustRandom); float waitTime; if (randomGrid != GridCoordinates.InvalidGrid) { - var random = IoCManager.Resolve(); - waitTime = random.NextFloat() * 10; + waitTime = robustRandom.Next(3, 8); } else { @@ -56,32 +59,39 @@ namespace Content.Server.AI.Utility.Actions.Idle { considerationsManager.Get() .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(); + 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(); + + foreach (var region in reachableRegions) + { + foreach (var node in region.Nodes) + { + reachableNodes.Add(node); + } + } + + var targetNode = robustRandom.Pick(reachableNodes); var mapManager = IoCManager.Resolve(); var grid = mapManager.GetGrid(Owner.Transform.GridID); + var targetGrid = grid.GridTileToLocal(targetNode.TileRef.GridIndices); - // Just find a random spot in bounds - // 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(); - 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; + return targetGrid; } } } diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs index cc8f98810a..a7680e6eb8 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs @@ -40,8 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink return new[] { - considerationsManager.Get() - .BoolCurve(context), considerationsManager.Get() .BoolCurve(context), considerationsManager.Get() @@ -50,6 +48,8 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), considerationsManager.Get() .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f), + considerationsManager.Get() + .BoolCurve(context), }; } diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs index 4b6c54a927..840d9c3cab 100644 --- a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs +++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs @@ -40,8 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food return new[] { - considerationsManager.Get() - .BoolCurve(context), considerationsManager.Get() .BoolCurve(context), considerationsManager.Get() @@ -50,6 +48,8 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food .QuadraticCurve(context, 1.0f, 1.0f, 0.02f, 0.0f), considerationsManager.Get() .QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f), + considerationsManager.Get() + .BoolCurve(context), }; } } diff --git a/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs index 5af9ac5a79..6ad25d3b4d 100644 --- a/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs +++ b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs @@ -1,13 +1,18 @@ using Content.Server.AI.WorldState; using Content.Server.AI.WorldState.States; 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.GameObjects.Systems; namespace Content.Server.AI.Utility.Considerations.Containers { /// /// 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 /// public sealed class TargetAccessibleCon : Consideration { @@ -36,7 +41,9 @@ namespace Content.Server.AI.Utility.Considerations.Containers } } - return 1.0f; + var owner = context.GetState().GetValue(); + + return EntitySystem.Get().CanAccess(owner, target, SharedInteractionSystem.InteractionRange) ? 1.0f : 0.0f; } } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs new file mode 100644 index 0000000000..481d192722 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/AiReachableSystem.cs @@ -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 +{ + /// + /// 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. + */ + +#pragma warning disable 649 + [Dependency] private IMapManager _mapmanager; + [Dependency] private IGameTiming _gameTiming; +#pragma warning restore 649 + private PathfindingSystem _pathfindingSystem; + + /// + /// Queued region updates + /// + private HashSet _queuedUpdates = new HashSet(); + + // 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 Dictionary>> _regions = + new Dictionary>>(); + + /// + /// 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 Dictionary Regions)>> _cachedAccessible = + new Dictionary)>>(); + +#if DEBUG + private int _runningCacheIdx = 0; +#endif + + public override void Initialize() + { + _pathfindingSystem = Get(); + SubscribeLocalEvent(RecalculateNodeRegions); +#if DEBUG + SubscribeLocalEvent(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); + } + + /// + /// 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(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); + } + + /// + /// 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(IEntity entity) + { + var entityTile = _mapmanager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition); + 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[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; + } + + /// + /// Add this node to the relevant region. + /// + /// + /// + /// 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, 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.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 {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) + { + 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); + } + } + + /// + /// 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) + { + 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) + { + 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, 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>>(); + var chunkIdx = 0; + var regionIdx = 0; + + foreach (var (_, regions) in _regions[gridId]) + { + 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(_mapmanager); + debugRegionNodes.Add(nodeVector); + } + + regionIdx++; + } + + chunkIdx++; + } + RaiseNetworkEvent(new SharedAiDebug.ReachableChunkRegionsDebugMessage(gridId, debugResult)); + } + + /// + /// Sent whenever the reachable cache for a particular mob is built or retrieved + /// + /// + /// + /// + private void SendRegionCacheMessage(GridId gridId, IEnumerable regions, bool cached) + { + 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(_mapmanager); + + debugResult[_runningCacheIdx].Add(nodeVector); + } + + _runningCacheIdx++; + } + + RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached)); + } +#endif + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/PathfindingRegion.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/PathfindingRegion.cs new file mode 100644 index 0000000000..faea8c75d9 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/PathfindingRegion.cs @@ -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 +{ + /// + /// A group of homogenous PathfindingNodes inside a single chunk + /// + /// Makes the graph smaller and quicker to traverse + public 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 HashSet(); + + public bool IsDoor { get; } + public HashSet Nodes => _nodes; + private HashSet _nodes; + + 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); + } + } + + /// + /// 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; + return GetHashCode() == other.GetHashCode(); + } + + public override int GetHashCode() + { + return OriginNode.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs new file mode 100644 index 0000000000..54af0a73bd --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Accessible/ReachableArgs.cs @@ -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 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(IEntity entity) + { + var collisionMask = 0; + if (entity.TryGetComponent(out CollidableComponent collidableComponent)) + { + collisionMask = collidableComponent.CollisionMask; + } + + var access = AccessReader.FindAccessTags(entity); + var visionRadius = entity.GetComponent().VisionRadius; + + return new ReachableArgs(visionRadius, access, collisionMask); + } + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs index 56c25d4670..ebb2b3657f 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs @@ -3,23 +3,37 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Robust.Shared.GameObjects; 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; 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 TimeSpan LastUpdate { get; private set; } public GridId GridId { get; } public MapIndices Indices => _indices; private readonly MapIndices _indices; // Nodes per chunk row - public static int ChunkSize => 16; + public static int ChunkSize => 8; public PathfindingNode[,] Nodes => _nodes; private PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize]; @@ -29,16 +43,28 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding _indices = indices; } - public void Initialize(IMapGrid grid) + public void Initialize(IMapGrid mapGrid) { for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { - var tileRef = grid.GetTileRef(new MapIndices(x + _indices.X, y + _indices.Y)); + var tileRef = mapGrid.GetTileRef(new MapIndices(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() diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs index 4c5e25390a..cabe4e92e5 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingHelpers.cs @@ -221,6 +221,16 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding return result; } + public static float OctileDistance(int dstX, int dstY) + { + if (dstX > dstY) + { + return 1.4f * dstY + (dstX - dstY); + } + + return 1.4f * dstX + (dstY - dstX); + } + public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode) { // "Fast Euclidean" / octile. diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs index a8aa4fedaf..2d062bce59 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs @@ -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) { TileRef = newTile; + ParentChunk.Dirty(); } /// @@ -111,6 +261,7 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding if (entity.TryGetComponent(out AccessReader accessReader) && !_accessReaders.ContainsKey(entity.Uid)) { _accessReaders.Add(entity.Uid, accessReader); + ParentChunk.Dirty(); } return; } @@ -126,6 +277,7 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding { _blockedCollidables.TryAdd(entity.Uid, collidableComponent.CollisionLayer); GenerateMask(); + ParentChunk.Dirty(); } } } @@ -147,11 +299,13 @@ namespace Content.Server.GameObjects.EntitySystems.Pathfinding else if (_accessReaders.ContainsKey(entity.Uid)) { _accessReaders.Remove(entity.Uid); + ParentChunk.Dirty(); } else if (_blockedCollidables.ContainsKey(entity.Uid)) { _blockedCollidables.Remove(entity.Uid); GenerateMask(); + ParentChunk.Dirty(); } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs index efd558603b..bc39d819b9 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs @@ -178,6 +178,17 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding return newChunk; } + /// + /// Get the entity's tile position, then get the corresponding node + /// + /// + /// + public PathfindingNode GetNode(IEntity entity) + { + var tile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition); + return GetNode(tile); + } + /// /// Return the corresponding PathfindingNode for this tile /// diff --git a/Content.Shared/AI/SharedAiDebug.cs b/Content.Shared/AI/SharedAiDebug.cs index 7033901aa0..87d31ece09 100644 --- a/Content.Shared/AI/SharedAiDebug.cs +++ b/Content.Shared/AI/SharedAiDebug.cs @@ -140,5 +140,34 @@ namespace Content.Shared.AI } } #endregion + #region Reachable Debug + [Serializable, NetSerializable] + public sealed class ReachableChunkRegionsDebugMessage : EntitySystemMessage + { + public GridId GridId { get; } + public Dictionary>> Regions { get; } + + public ReachableChunkRegionsDebugMessage(GridId gridId, Dictionary>> regions) + { + GridId = gridId; + Regions = regions; + } + } + + [Serializable, NetSerializable] + public sealed class ReachableCacheDebugMessage : EntitySystemMessage + { + public GridId GridId { get; } + public Dictionary> Regions { get; } + public bool Cached { get; } + + public ReachableCacheDebugMessage(GridId gridId, Dictionary> regions, bool cached) + { + GridId = gridId; + Regions = regions; + Cached = cached; + } + } + #endregion } }