diff --git a/Content.Client/Commands/DebugAiCommand.cs b/Content.Client/Commands/DebugAiCommand.cs new file mode 100644 index 0000000000..1d463e3072 --- /dev/null +++ b/Content.Client/Commands/DebugAiCommand.cs @@ -0,0 +1,61 @@ +using Content.Client.GameObjects.EntitySystems.AI; +using JetBrains.Annotations; +using Robust.Client.Interfaces.Console; +using Robust.Client.Player; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Client.Commands +{ + /// + /// This is used to handle the tooltips above AI mobs + /// + [UsedImplicitly] + internal sealed class DebugAiCommand : IConsoleCommand + { + // ReSharper disable once StringLiteralTypo + public string Command => "debugai"; + public string Description => "Handles all tooltip debugging above AI mobs"; + public string Help => "debugai [hide/paths/thonk]"; + + public bool Execute(IDebugConsole console, params string[] args) + { +#if DEBUG + if (args.Length < 1) + { + return true; + } + + var anyAction = false; + var debugSystem = EntitySystem.Get(); + + foreach (var arg in args) + { + switch (arg) + { + case "hide": + debugSystem.Disable(); + anyAction = true; + break; + // This will show the pathfinding numbers above the mob's head + case "paths": + debugSystem.ToggleTooltip(AiDebugMode.Paths); + anyAction = true; + break; + // Shows stats on what the AI was thinking. + case "thonk": + debugSystem.ToggleTooltip(AiDebugMode.Thonk); + anyAction = true; + break; + default: + continue; + } + } + + return !anyAction; +#endif + return true; + } + } +} diff --git a/Content.Client/Commands/DebugPathfindingCommand.cs b/Content.Client/Commands/DebugPathfindingCommand.cs new file mode 100644 index 0000000000..8549ed1929 --- /dev/null +++ b/Content.Client/Commands/DebugPathfindingCommand.cs @@ -0,0 +1,62 @@ +using Content.Client.GameObjects.EntitySystems.AI; +using JetBrains.Annotations; +using Robust.Client.Interfaces.Console; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Client.Commands +{ + [UsedImplicitly] + internal sealed class DebugPathfindingCommand : IConsoleCommand + { + // ReSharper disable once StringLiteralTypo + public string Command => "pathfinder"; + public string Description => "Toggles visibility of pathfinding debuggers."; + public string Help => "pathfinder [hide/nodes/routes/graph]"; + + public bool Execute(IDebugConsole console, params string[] args) + { +#if DEBUG + if (args.Length < 1) + { + return true; + } + + var anyAction = false; + var debugSystem = EntitySystem.Get(); + + foreach (var arg in args) + { + switch (arg) + { + case "hide": + debugSystem.Disable(); + anyAction = true; + break; + // Shows all nodes on the closed list + case "nodes": + debugSystem.ToggleTooltip(PathfindingDebugMode.Nodes); + anyAction = true; + break; + // Will show just the constructed route + case "routes": + debugSystem.ToggleTooltip(PathfindingDebugMode.Route); + anyAction = true; + break; + // Shows all of the pathfinding chunks + case "graph": + debugSystem.ToggleTooltip(PathfindingDebugMode.Graph); + anyAction = true; + break; + default: + continue; + } + } + + return !anyAction; +#endif + return true; + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs b/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs new file mode 100644 index 0000000000..9e24a58c66 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using Content.Shared.AI; +using Robust.Client.Interfaces.Graphics.ClientEye; +using Robust.Client.Interfaces.UserInterface; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.EntitySystems.AI +{ +#if DEBUG + public class ClientAiDebugSystem : EntitySystem + { + private AiDebugMode _tooltips = AiDebugMode.None; + private readonly Dictionary _aiBoxes = new Dictionary(); + + public override void Update(float frameTime) + { + base.Update(frameTime); + if (_tooltips == 0) + { + return; + } + + var eyeManager = IoCManager.Resolve(); + foreach (var (entity, panel) in _aiBoxes) + { + if (entity == null) continue; + + if (!eyeManager.GetWorldViewport().Contains(entity.Transform.WorldPosition)) + { + panel.Visible = false; + continue; + } + + var (x, y) = eyeManager.WorldToScreen(entity.Transform.GridPosition).Position; + var offsetPosition = new Vector2(x - panel.Width / 2, y - panel.Height - 50f); + panel.Visible = true; + + LayoutContainer.SetPosition(panel, offsetPosition); + } + } + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(HandleUtilityAiDebugMessage); + SubscribeNetworkEvent(HandleAStarRouteMessage); + SubscribeNetworkEvent(HandleJpsRouteMessage); + } + + private void HandleUtilityAiDebugMessage(SharedAiDebug.UtilityAiDebugMessage message) + { + if ((_tooltips & AiDebugMode.Thonk) != 0) + { + // I guess if it's out of range we don't know about it? + var entityManager = IoCManager.Resolve(); + var entity = entityManager.GetEntity(message.EntityUid); + if (entity == null) + { + return; + } + + TryCreatePanel(entity); + + // Probably shouldn't access by index but it's a debugging tool so eh + var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(0); + label.Text = $"Current Task: {message.FoundTask}\n" + + $"Task score: {message.ActionScore}\n" + + $"Planning time (ms): {message.PlanningTime * 1000:0.0000}\n" + + $"Considered {message.ConsideredTaskCount} tasks"; + } + } + + private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message) + { + if ((_tooltips & AiDebugMode.Paths) != 0) + { + var entityManager = IoCManager.Resolve(); + var entity = entityManager.GetEntity(message.EntityUid); + if (entity == null) + { + return; + } + + TryCreatePanel(entity); + + var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1); + label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" + + $"Nodes traversed: {message.ClosedTiles.Count}\n" + + $"Nodes per ms: {message.ClosedTiles.Count / (message.TimeTaken * 1000)}"; + } + } + + private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message) + { + if ((_tooltips & AiDebugMode.Paths) != 0) + { + var entityManager = IoCManager.Resolve(); + var entity = entityManager.GetEntity(message.EntityUid); + if (entity == null) + { + return; + } + + TryCreatePanel(entity); + + var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1); + label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" + + $"Jump Nodes: {message.JumpNodes.Count}\n" + + $"Jump Nodes per ms: {message.JumpNodes.Count / (message.TimeTaken * 1000)}"; + } + } + + public void Disable() + { + foreach (var tooltip in _aiBoxes.Values) + { + tooltip.Dispose(); + } + _aiBoxes.Clear(); + _tooltips = AiDebugMode.None; + } + + + private void EnableTooltip(AiDebugMode tooltip) + { + _tooltips |= tooltip; + } + + private void DisableTooltip(AiDebugMode tooltip) + { + _tooltips &= ~tooltip; + } + + public void ToggleTooltip(AiDebugMode tooltip) + { + if ((_tooltips & tooltip) != 0) + { + DisableTooltip(tooltip); + } + else + { + EnableTooltip(tooltip); + } + } + + private bool TryCreatePanel(IEntity entity) + { + if (!_aiBoxes.ContainsKey(entity)) + { + var userInterfaceManager = IoCManager.Resolve(); + + var actionLabel = new Label + { + MouseFilter = Control.MouseFilterMode.Ignore, + }; + + var pathfindingLabel = new Label + { + MouseFilter = Control.MouseFilterMode.Ignore, + }; + + var vBox = new VBoxContainer() + { + SeparationOverride = 15, + Children = {actionLabel, pathfindingLabel}, + }; + + var panel = new PanelContainer + { + StyleClasses = {"tooltipBox"}, + Children = {vBox}, + MouseFilter = Control.MouseFilterMode.Ignore, + ModulateSelfOverride = Color.White.WithAlpha(0.75f), + }; + + userInterfaceManager.StateRoot.AddChild(panel); + + _aiBoxes[entity] = panel; + return true; + } + + return false; + } + } + + [Flags] + public enum AiDebugMode : byte + { + None = 0, + Paths = 1 << 1, + Thonk = 1 << 2, + } +#endif +} diff --git a/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs new file mode 100644 index 0000000000..746a9b92e2 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Shared.AI; +using Robust.Client.Graphics.Drawing; +using Robust.Client.Graphics.Overlays; +using Robust.Client.Graphics.Shaders; +using Robust.Client.Interfaces.Graphics.ClientEye; +using Robust.Client.Interfaces.Graphics.Overlays; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timers; + +namespace Content.Client.GameObjects.EntitySystems.AI +{ +#if DEBUG + public class ClientPathfindingDebugSystem : EntitySystem + { + private PathfindingDebugMode _modes = PathfindingDebugMode.None; + private float _routeDuration = 4.0f; // How long before we remove a route from the overlay + private DebugPathfindingOverlay _overlay; + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(HandleAStarRouteMessage); + SubscribeNetworkEvent(HandleJpsRouteMessage); + SubscribeNetworkEvent(HandleGraphMessage); + } + + private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message) + { + if ((_modes & PathfindingDebugMode.Nodes) != 0 || + (_modes & PathfindingDebugMode.Route) != 0) + { + _overlay.AStarRoutes.Add(message); + Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () => + { + if (_overlay == null) return; + _overlay.AStarRoutes.Remove(message); + }); + } + } + + private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message) + { + if ((_modes & PathfindingDebugMode.Nodes) != 0 || + (_modes & PathfindingDebugMode.Route) != 0) + { + _overlay.JpsRoutes.Add(message); + Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () => + { + if (_overlay == null) return; + _overlay.JpsRoutes.Remove(message); + }); + } + } + + private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message) + { + if ((_modes & PathfindingDebugMode.Graph) != 0) + { + _overlay.UpdateGraph(message.Graph); + } + } + + private void EnableOverlay() + { + if (_overlay != null) + { + return; + } + + var overlayManager = IoCManager.Resolve(); + _overlay = new DebugPathfindingOverlay {Modes = _modes}; + overlayManager.AddOverlay(_overlay); + } + + private void DisableOverlay() + { + if (_overlay == null) + { + return; + } + + _overlay.Modes = 0; + var overlayManager = IoCManager.Resolve(); + overlayManager.RemoveOverlay(_overlay.ID); + _overlay = null; + } + + public void Disable() + { + _modes = PathfindingDebugMode.None; + DisableOverlay(); + } + + + private void EnableMode(PathfindingDebugMode tooltip) + { + _modes |= tooltip; + if (_modes != 0) + { + EnableOverlay(); + } + _overlay.Modes = _modes; + + if (tooltip == PathfindingDebugMode.Graph) + { + var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage(); + EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage); + } + } + + private void DisableMode(PathfindingDebugMode mode) + { + _modes &= ~mode; + if (_modes == 0) + { + DisableOverlay(); + } + else + { + _overlay.Modes = _modes; + } + } + + public void ToggleTooltip(PathfindingDebugMode mode) + { + if ((_modes & mode) != 0) + { + DisableMode(mode); + } + else + { + EnableMode(mode); + } + } + } + + internal sealed class DebugPathfindingOverlay : Overlay + { + // TODO: Add a box like the debug one and show the most recent path stuff + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + public PathfindingDebugMode Modes { get; set; } = PathfindingDebugMode.None; + + // Graph debugging + public readonly Dictionary> Graph = new Dictionary>(); + private readonly Dictionary _graphColors = 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(); + public readonly List JpsRoutes = new List(); + + public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay)) + { + Shader = IoCManager.Resolve().Index("unshaded").Instance(); + } + + public void UpdateGraph(Dictionary> graph) + { + Graph.Clear(); + _graphColors.Clear(); + var robustRandom = IoCManager.Resolve(); + foreach (var (chunk, nodes) in graph) + { + Graph[chunk] = nodes; + _graphColors[chunk] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(), + robustRandom.NextFloat(), 0.3f); + } + } + + private void DrawGraph(DrawingHandleScreen screenHandle) + { + 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 box = new UIBox2( + screenTile.X - 15.0f, + screenTile.Y - 15.0f, + screenTile.X + 15.0f, + screenTile.Y + 15.0f); + + screenHandle.DrawRect(box, _graphColors[chunk]); + } + } + } + + #region pathfinder + + private void DrawAStarRoutes(DrawingHandleScreen screenHandle) + { + var eyeManager = IoCManager.Resolve(); + var viewport = eyeManager.GetWorldViewport(); + + foreach (var route in AStarRoutes) + { + // Draw box on each tile of route + foreach (var position in route.Route) + { + if (!viewport.Contains(position)) continue; + var screenTile = eyeManager.WorldToScreen(position); + // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); + var box = new UIBox2( + screenTile.X - 15.0f, + screenTile.Y - 15.0f, + screenTile.X + 15.0f, + screenTile.Y + 15.0f); + screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f)); + } + } + } + + private void DrawAStarNodes(DrawingHandleScreen screenHandle) + { + var eyeManager = IoCManager.Resolve(); + var viewport = eyeManager.GetWorldViewport(); + + foreach (var route in AStarRoutes) + { + var highestgScore = route.GScores.Values.Max(); + + foreach (var (tile, score) in route.GScores) + { + if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) || + !viewport.Contains(tile)) + { + continue; + } + + var screenTile = eyeManager.WorldToScreen(tile); + + var box = new UIBox2( + screenTile.X - 15.0f, + screenTile.Y - 15.0f, + screenTile.X + 15.0f, + screenTile.Y + 15.0f); + + screenHandle.DrawRect(box, new Color( + 0.0f, + score / highestgScore, + 1.0f - (score / highestgScore), + 0.1f)); + } + } + } + + private void DrawJpsRoutes(DrawingHandleScreen screenHandle) + { + 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); + // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue); + var box = new UIBox2( + screenTile.X - 15.0f, + screenTile.Y - 15.0f, + screenTile.X + 15.0f, + screenTile.Y + 15.0f); + screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f)); + } + } + } + + private void DrawJpsNodes(DrawingHandleScreen screenHandle) + { + var eyeManager = IoCManager.Resolve(); + var viewport = eyeManager.GetWorldViewport(); + + foreach (var route in JpsRoutes) + { + foreach (var tile in route.JumpNodes) + { + if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) || + !viewport.Contains(tile)) + { + continue; + } + + var screenTile = eyeManager.WorldToScreen(tile); + + var box = new UIBox2( + screenTile.X - 15.0f, + screenTile.Y - 15.0f, + screenTile.X + 15.0f, + screenTile.Y + 15.0f); + + screenHandle.DrawRect(box, new Color( + 0.0f, + 1.0f, + 0.0f, + 0.2f)); + } + } + } + + #endregion + + protected override void Draw(DrawingHandleBase handle) + { + if (Modes == 0) + { + return; + } + + var screenHandle = (DrawingHandleScreen) handle; + + if ((Modes & PathfindingDebugMode.Route) != 0) + { + DrawAStarRoutes(screenHandle); + DrawJpsRoutes(screenHandle); + } + + if ((Modes & PathfindingDebugMode.Nodes) != 0) + { + DrawAStarNodes(screenHandle); + DrawJpsNodes(screenHandle); + } + + if ((Modes & PathfindingDebugMode.Graph) != 0) + { + DrawGraph(screenHandle); + } + } + } + + [Flags] + public enum PathfindingDebugMode { + None = 0, + Route = 1 << 0, + Graph = 1 << 1, + Nodes = 1 << 2, + } +#endif +} diff --git a/Content.Server/AI/AimShootLifeProcessor.cs b/Content.Server/AI/AimShootLifeProcessor.cs deleted file mode 100644 index 52f7b06d57..0000000000 --- a/Content.Server/AI/AimShootLifeProcessor.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Server.Interfaces.GameObjects.Components.Movement; -using Content.Shared.Physics; -using Robust.Server.AI; -using Robust.Server.Interfaces.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.GameObjects.Components; -using Robust.Shared.Interfaces.Physics; -using Robust.Shared.Interfaces.Timing; -using Robust.Shared.IoC; -using Robust.Shared.Maths; - -namespace Content.Server.AI -{ - /// - /// The object stays stationary. The object will periodically scan for *any* life forms in its radius, and engage them. - /// The object will rotate itself to point at the locked entity, and if it has a weapon will shoot at the entity. - /// - [AiLogicProcessor("AimShootLife")] - class AimShootLifeProcessor : AiLogicProcessor - { -#pragma warning disable 649 - [Dependency] private readonly IPhysicsManager _physMan; - [Dependency] private readonly IServerEntityManager _entMan; - [Dependency] private readonly IGameTiming _timeMan; -#pragma warning restore 649 - - private readonly List _workList = new List(); - - private const float MaxAngSpeed = (float)(Math.PI / 2); // how fast our turret can rotate - private const float ScanPeriod = 1.0f; // tweak this for performance and gameplay experience - private float _lastScan; - - private IEntity _curTarget; - - /// - public override void Update(float frameTime) - { - if (SelfEntity == null) - return; - - DoScanning(); - DoTracking(frameTime); - } - - private void DoScanning() - { - var curTime = _timeMan.CurTime.TotalSeconds; - if (curTime - _lastScan > ScanPeriod) - { - _lastScan = (float)curTime; - _curTarget = FindBestTarget(); - } - } - - private void DoTracking(float frameTime) - { - // not valid entity to target. - if (_curTarget == null || !_curTarget.IsValid()) - { - _curTarget = null; - return; - } - - // point me at the target - var tarPos = _curTarget.GetComponent().WorldPosition; - var selfTransform = SelfEntity.GetComponent(); - var myPos = selfTransform.WorldPosition; - - var curDir = selfTransform.LocalRotation.ToVec(); - var tarDir = (tarPos - myPos).Normalized; - - var fwdAng = Vector2.Dot(curDir, tarDir); - - Vector2 newDir; - if (fwdAng < 0) // target behind turret, just rotate in a direction to get target in front - { - var curRight = new Vector2(-curDir.Y, curDir.X); // right handed coord system - var rightAngle = Vector2.Dot(curDir, new Vector2(-tarDir.Y, tarDir.X)); // right handed coord system - var rotateSign = -Math.Sign(rightAngle); - newDir = curDir + curRight * rotateSign * MaxAngSpeed * frameTime; - } - else // target in front, adjust to aim at him - { - newDir = MoveTowards(curDir, tarDir, MaxAngSpeed, frameTime); - } - - selfTransform.LocalRotation = new Angle(newDir); - - if (fwdAng > -0.9999) - { - // TODO: shoot gun, prob need aimbot because entity rotation lags behind moving target - } - } - - private IEntity FindBestTarget() - { - // "best" target is the closest one with LOS - - var ents = _entMan.GetEntitiesInRange(SelfEntity, VisionRadius); - var myTransform = SelfEntity.GetComponent(); - var maxRayLen = VisionRadius * 2.5f; // circle inscribed in square, square diagonal = 2*r*sqrt(2) - - _workList.Clear(); - foreach (var entity in ents) - { - // filter to "people" entities (entities with controllers) - if (!entity.HasComponent()) - continue; - - // build the ray - var dir = entity.GetComponent().WorldPosition - myTransform.WorldPosition; - var ray = new CollisionRay(myTransform.WorldPosition, dir.Normalized, (int)(CollisionGroup.MobImpassable | CollisionGroup.Impassable)); - - // cast the ray - var result = _physMan.IntersectRay(myTransform.MapID, ray, maxRayLen, SelfEntity).First(); - - // add to visible list - if (result.HitEntity == entity) - _workList.Add(entity); - } - - // get closest entity in list - var closestEnt = GetClosest(myTransform.WorldPosition, _workList); - - // return closest - return closestEnt; - } - - private static IEntity GetClosest(Vector2 origin, IEnumerable list) - { - IEntity closest = null; - var minDistSqrd = float.PositiveInfinity; - - foreach (var ent in list) - { - var pos = ent.GetComponent().WorldPosition; - var distSqrd = (pos - origin).LengthSquared; - - if (distSqrd > minDistSqrd) - continue; - - closest = ent; - minDistSqrd = distSqrd; - } - - return closest; - } - - private static Vector2 MoveTowards(Vector2 current, Vector2 target, float speed, float delta) - { - var maxDeltaDist = speed * delta; - var a = target - current; - var magnitude = a.Length; - if (magnitude <= maxDeltaDist) - { - return target; - } - - return current + a / magnitude * maxDeltaDist; - } - } -} diff --git a/Content.Server/AI/Operators/AiOperator.cs b/Content.Server/AI/Operators/AiOperator.cs new file mode 100644 index 0000000000..1989846ea9 --- /dev/null +++ b/Content.Server/AI/Operators/AiOperator.cs @@ -0,0 +1,55 @@ + +using System; + +namespace Content.Server.AI.Operators +{ + public abstract class AiOperator + { + private bool _hasStartup = false; + private bool _hasShutdown = false; + + /// + /// Called once when the AiLogicProcessor starts this action + /// + public virtual bool TryStartup() + { + // If we've already startup then no point continuing + // This signals to the override that it's already startup + // Should probably throw but it made some code elsewhere marginally easier + if (_hasStartup) + { + return false; + } + + _hasStartup = true; + return true; + } + + /// + /// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails. + /// + public virtual void Shutdown(Outcome outcome) + { + if (_hasShutdown) + { + throw new InvalidOperationException("AiOperator has already shutdown"); + } + + _hasShutdown = true; + } + + /// + /// Called every tick for the AI + /// + /// + /// + public abstract Outcome Execute(float frameTime); + } + + public enum Outcome + { + Success, + Continuing, + Failed, + } +} \ No newline at end of file diff --git a/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs b/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs new file mode 100644 index 0000000000..4756843192 --- /dev/null +++ b/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs @@ -0,0 +1,100 @@ +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Weapon.Ranged; +using Robust.Shared.Interfaces.GameObjects; + + +namespace Content.Server.AI.Operators.Combat.Ranged +{ + public class ShootAtEntityOperator : AiOperator + { + private IEntity _owner; + private IEntity _target; + private float _accuracy; + + private float _burstTime; + + private float _elapsedTime; + + public ShootAtEntityOperator(IEntity owner, IEntity target, float accuracy, float burstTime = 0.5f) + { + _owner = owner; + _target = target; + _accuracy = accuracy; + _burstTime = burstTime; + } + + public override bool TryStartup() + { + if (!base.TryStartup()) + { + return true; + } + + if (!_owner.TryGetComponent(out CombatModeComponent combatModeComponent)) + { + return false; + } + + if (!combatModeComponent.IsInCombatMode) + { + combatModeComponent.IsInCombatMode = true; + } + + return true; + } + + public override void Shutdown(Outcome outcome) + { + base.Shutdown(outcome); + if (_owner.TryGetComponent(out CombatModeComponent combatModeComponent)) + { + combatModeComponent.IsInCombatMode = false; + } + } + + public override Outcome Execute(float frameTime) + { + // TODO: Probably just do all the checks on first try and then after that repeat the fire. + if (_burstTime <= _elapsedTime) + { + return Outcome.Success; + } + + _elapsedTime += frameTime; + + if (_target.TryGetComponent(out DamageableComponent damageableComponent)) + { + if (damageableComponent.IsDead()) + { + return Outcome.Success; + } + } + + if (!_owner.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand == null) + { + return Outcome.Failed; + } + + var equippedWeapon = hands.GetActiveHand.Owner; + + if ((_target.Transform.GridPosition.Position - _owner.Transform.GridPosition.Position).Length > + _owner.GetComponent().VisionRadius) + { + // Not necessarily a hard fail, more of a soft fail + return Outcome.Failed; + } + + // Unless RangedWeaponComponent is removed from hitscan weapons this shouldn't happen + if (!equippedWeapon.TryGetComponent(out RangedWeaponComponent rangedWeaponComponent)) + { + return Outcome.Failed; + } + + // TODO: Accuracy + rangedWeaponComponent.AiFire(_owner, _target.Transform.GridPosition); + return Outcome.Continuing; + } + } +} diff --git a/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs b/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs new file mode 100644 index 0000000000..fc8835f118 --- /dev/null +++ b/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs @@ -0,0 +1,41 @@ +using System; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Combat.Ranged +{ + public class WaitForHitscanChargeOperator : AiOperator + { + private float _lastCharge = 0.0f; + private float _lastFill = 0.0f; + private HitscanWeaponComponent _hitscan; + + public WaitForHitscanChargeOperator(IEntity entity) + { + if (!entity.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent)) + { + throw new InvalidOperationException(); + } + + _hitscan = hitscanWeaponComponent; + } + + public override Outcome Execute(float frameTime) + { + if (_hitscan.CapacitorComponent.Capacity - _hitscan.CapacitorComponent.Charge < 0.01f) + { + return Outcome.Success; + } + + // If we're not charging then just stop + _lastFill = _hitscan.CapacitorComponent.Charge - _lastCharge; + _lastCharge = _hitscan.CapacitorComponent.Charge; + + if (_lastFill == 0.0f) + { + return Outcome.Failed; + } + return Outcome.Continuing; + } + } +} diff --git a/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs b/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs new file mode 100644 index 0000000000..d4c5ade0d6 --- /dev/null +++ b/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs @@ -0,0 +1,74 @@ +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Content.Server.GameObjects.EntitySystems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.AI.Operators.Combat +{ + public class SwingMeleeWeaponOperator : AiOperator + { + private float _burstTime; + private float _elapsedTime; + + private readonly IEntity _owner; + private readonly IEntity _target; + + public SwingMeleeWeaponOperator(IEntity owner, IEntity target, float burstTime = 1.0f) + { + _owner = owner; + _target = target; + _burstTime = burstTime; + } + + public override bool TryStartup() + { + if (!base.TryStartup()) + { + return true; + } + + if (!_owner.TryGetComponent(out CombatModeComponent combatModeComponent)) + { + return false; + } + + if (!combatModeComponent.IsInCombatMode) + { + combatModeComponent.IsInCombatMode = true; + } + + return true; + } + + public override Outcome Execute(float frameTime) + { + if (_burstTime <= _elapsedTime) + { + return Outcome.Success; + } + + if (!_owner.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand == null) + { + return Outcome.Failed; + } + + var meleeWeapon = hands.GetActiveHand.Owner; + meleeWeapon.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent); + + if ((_target.Transform.GridPosition.Position - _owner.Transform.GridPosition.Position).Length > + meleeWeaponComponent.Range) + { + return Outcome.Failed; + } + + var interactionSystem = IoCManager.Resolve().GetEntitySystem(); + + interactionSystem.UseItemInHand(_owner, _target.Transform.GridPosition, _target.Uid); + _elapsedTime += frameTime; + return Outcome.Continuing; + } + } + +} diff --git a/Content.Server/AI/Operators/Generic/WaitOperator.cs b/Content.Server/AI/Operators/Generic/WaitOperator.cs new file mode 100644 index 0000000000..7c93111570 --- /dev/null +++ b/Content.Server/AI/Operators/Generic/WaitOperator.cs @@ -0,0 +1,24 @@ +namespace Content.Server.AI.Operators.Generic +{ + public class WaitOperator : AiOperator + { + private readonly float _waitTime; + private float _accumulatedTime = 0.0f; + + public WaitOperator(float waitTime) + { + _waitTime = waitTime; + } + + public override Outcome Execute(float frameTime) + { + if (_accumulatedTime < _waitTime) + { + _accumulatedTime += frameTime; + return Outcome.Continuing; + } + + return Outcome.Success; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs b/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs new file mode 100644 index 0000000000..193b45077e --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs @@ -0,0 +1,73 @@ +using Content.Server.AI.Utility; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Inventory +{ + /// + /// Close the last EntityStorage we opened + /// This will also update the State for it (which a regular InteractWith won't do) + /// + public sealed class CloseLastStorageOperator : AiOperator + { + private readonly IEntity _owner; + private IEntity _target; + + public CloseLastStorageOperator(IEntity owner) + { + _owner = owner; + } + + public override bool TryStartup() + { + if (!base.TryStartup()) + { + return true; + } + + var blackboard = UtilityAiHelpers.GetBlackboard(_owner); + + if (blackboard == null) + { + return false; + } + + _target = blackboard.GetState().GetValue(); + + return _target != null; + } + + public override void Shutdown(Outcome outcome) + { + base.Shutdown(outcome); + var blackboard = UtilityAiHelpers.GetBlackboard(_owner); + + blackboard?.GetState().SetValue(null); + } + + public override Outcome Execute(float frameTime) + { + if (!InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition)) + { + return Outcome.Failed; + } + + if (!_target.TryGetComponent(out EntityStorageComponent storageComponent) || + storageComponent.IsWeldedShut) + { + return Outcome.Failed; + } + + if (storageComponent.Open) + { + var activateArgs = new ActivateEventArgs {User = _owner, Target = _target}; + storageComponent.Activate(activateArgs); + } + + return Outcome.Success; + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs new file mode 100644 index 0000000000..759c2c28b2 --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs @@ -0,0 +1,32 @@ +using Content.Server.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Log; + +namespace Content.Server.AI.Operators.Inventory +{ + public class DropEntityOperator : AiOperator + { + private readonly IEntity _owner; + private readonly IEntity _entity; + public DropEntityOperator(IEntity owner, IEntity entity) + { + _owner = owner; + _entity = entity; + } + + /// + /// Requires EquipEntityOperator to put it in the active hand first + /// + /// + /// + public override Outcome Execute(float frameTime) + { + if (!_owner.TryGetComponent(out HandsComponent handsComponent)) + { + return Outcome.Failed; + } + + return handsComponent.Drop(_entity) ? Outcome.Success : Outcome.Failed; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs b/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs new file mode 100644 index 0000000000..4966ca172e --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs @@ -0,0 +1,30 @@ +using Content.Server.GameObjects; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Inventory +{ + public class DropHandItemsOperator : AiOperator + { + private readonly IEntity _owner; + + public DropHandItemsOperator(IEntity owner) + { + _owner = owner; + } + + public override Outcome Execute(float frameTime) + { + if (!_owner.TryGetComponent(out HandsComponent handsComponent)) + { + return Outcome.Failed; + } + + foreach (var item in handsComponent.GetAllHeldItems()) + { + handsComponent.Drop(item.Owner); + } + + return Outcome.Success; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs b/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs new file mode 100644 index 0000000000..3eba859abc --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs @@ -0,0 +1,38 @@ +using Content.Server.GameObjects; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Inventory +{ + public sealed class EquipEntityOperator : AiOperator + { + private readonly IEntity _owner; + private readonly IEntity _entity; + public EquipEntityOperator(IEntity owner, IEntity entity) + { + _owner = owner; + _entity = entity; + } + + public override Outcome Execute(float frameTime) + { + if (!_owner.TryGetComponent(out HandsComponent handsComponent)) + { + return Outcome.Failed; + } + // TODO: If in clothing then click on it + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + if (handsComponent.GetHand(hand)?.Owner == _entity) + { + handsComponent.ActiveIndex = hand; + return Outcome.Success; + } + } + + // TODO: Get free hand count; if no hands free then fail right here + + // TODO: Go through inventory + return Outcome.Failed; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs new file mode 100644 index 0000000000..dc2412a335 --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs @@ -0,0 +1,48 @@ +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.AI.Operators.Inventory +{ + /// + /// A Generic interacter; if you need to check stuff then make your own + /// + public class InteractWithEntityOperator : AiOperator + { + private readonly IEntity _owner; + private readonly IEntity _useTarget; + + public InteractWithEntityOperator(IEntity owner, IEntity useTarget) + { + _owner = owner; + _useTarget = useTarget; + + } + + public override Outcome Execute(float frameTime) + { + if (_useTarget.Transform.GridID != _owner.Transform.GridID) + { + return Outcome.Failed; + } + + if (!InteractionChecks.InRangeUnobstructed(_owner, _useTarget.Transform.MapPosition)) + { + return Outcome.Failed; + } + + if (_owner.TryGetComponent(out CombatModeComponent combatModeComponent)) + { + combatModeComponent.IsInCombatMode = false; + } + + // Click on da thing + var interactionSystem = IoCManager.Resolve().GetEntitySystem(); + interactionSystem.UseItemInHand(_owner, _useTarget.Transform.GridPosition, _useTarget.Uid); + + return Outcome.Success; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs b/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs new file mode 100644 index 0000000000..fe2f5e24e1 --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs @@ -0,0 +1,55 @@ +using Content.Server.AI.Utility; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Robust.Shared.Containers; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Inventory +{ + /// + /// If the target is in EntityStorage will open its parent container + /// + public sealed class OpenStorageOperator : AiOperator + { + private readonly IEntity _owner; + private readonly IEntity _target; + + public OpenStorageOperator(IEntity owner, IEntity target) + { + _owner = owner; + _target = target; + } + + public override Outcome Execute(float frameTime) + { + if (!InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition)) + { + return Outcome.Failed; + } + + if (!ContainerHelpers.TryGetContainer(_target, out var container)) + { + return Outcome.Success; + } + + if (!container.Owner.TryGetComponent(out EntityStorageComponent storageComponent) || + storageComponent.IsWeldedShut) + { + return Outcome.Failed; + } + + if (!storageComponent.Open) + { + var activateArgs = new ActivateEventArgs {User = _owner, Target = _target}; + storageComponent.Activate(activateArgs); + } + + var blackboard = UtilityAiHelpers.GetBlackboard(_owner); + blackboard?.GetState().SetValue(container.Owner); + + return Outcome.Success; + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs new file mode 100644 index 0000000000..ba0f40f78f --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs @@ -0,0 +1,65 @@ +using Content.Server.GameObjects; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Robust.Shared.Containers; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.AI.Operators.Inventory +{ + public class PickupEntityOperator : AiOperator + { + // Input variables + private readonly IEntity _owner; + private readonly IEntity _target; + + public PickupEntityOperator(IEntity owner, IEntity target) + { + _owner = owner; + _target = target; + } + + // TODO: When I spawn new entities they seem to duplicate clothing or something? + public override Outcome Execute(float frameTime) + { + if (_target == null || + _target.Deleted || + !_target.HasComponent() || + ContainerHelpers.IsInContainer(_target) || + !InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition)) + { + return Outcome.Failed; + } + + if (!_owner.TryGetComponent(out HandsComponent handsComponent)) + { + return Outcome.Failed; + } + + var emptyHands = false; + + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + if (handsComponent.GetHand(hand) == null) + { + if (handsComponent.ActiveIndex != hand) + { + handsComponent.ActiveIndex = hand; + } + + emptyHands = true; + break; + } + } + + if (!emptyHands) + { + return Outcome.Failed; + } + + var interactionSystem = IoCManager.Resolve().GetEntitySystem(); + interactionSystem.Interaction(_owner, _target); + return Outcome.Success; + } + } +} diff --git a/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs b/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs new file mode 100644 index 0000000000..3e4ed39de8 --- /dev/null +++ b/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs @@ -0,0 +1,49 @@ +using Content.Server.GameObjects; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Inventory +{ + /// + /// Will find the item in storage, put it in an active hand, then use it + /// + public class UseItemInHandsOperator : AiOperator + { + private readonly IEntity _owner; + private readonly IEntity _target; + + public UseItemInHandsOperator(IEntity owner, IEntity target) + { + _owner = owner; + _target = target; + } + + public override Outcome Execute(float frameTime) + { + if (_target == null) + { + return Outcome.Failed; + } + + // TODO: Also have this check storage a la backpack etc. + if (!_owner.TryGetComponent(out HandsComponent handsComponent)) + { + return Outcome.Failed; + } + + if (_target.TryGetComponent(out ItemComponent itemComponent)) + { + return Outcome.Failed; + } + + foreach (var slot in handsComponent.ActivePriorityEnumerable()) + { + if (handsComponent.GetHand(slot) != itemComponent) continue; + handsComponent.ActiveIndex = slot; + handsComponent.ActivateItem(); + return Outcome.Success; + } + + return Outcome.Failed; + } + } +} diff --git a/Content.Server/AI/Operators/Movement/BaseMover.cs b/Content.Server/AI/Operators/Movement/BaseMover.cs new file mode 100644 index 0000000000..d17472378f --- /dev/null +++ b/Content.Server/AI/Operators/Movement/BaseMover.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Timer = Robust.Shared.Timers.Timer; + +namespace Content.Server.AI.Operators.Movement +{ + public abstract class BaseMover : AiOperator + { + /// + /// Invoked every time we move across a tile + /// + public event Action MovedATile; + + /// + /// How close the pathfinder needs to get before returning a route + /// Set at 1.42f just in case there's rounding and diagonally adjacent tiles aren't counted. + /// + /// + public float PathfindingProximity { get; set; } = 1.42f; + protected Queue Route = new Queue(); + /// + /// The final spot we're trying to get to + /// + protected GridCoordinates TargetGrid; + /// + /// As the pathfinder is tilebased we'll move to each tile's grid. + /// + protected GridCoordinates NextGrid; + private const float TileTolerance = 0.2f; + + // Stuck checkers + /// + /// How long we're stuck in general before trying to unstuck + /// + private float _stuckTimerRemaining = 0.5f; + private GridCoordinates _ourLastPosition; + + // Anti-stuck measures. See the AntiStuck() method for more details + private bool _tryingAntiStuck; + public bool IsStuck; + private AntiStuckMethod _antiStuckMethod = AntiStuckMethod.Angle; + private Angle _addedAngle = Angle.Zero; + public event Action Stuck; + private int _antiStuckAttempts = 0; + + private CancellationTokenSource _routeCancelToken; + protected Job> RouteJob; + private IMapManager _mapManager; + private PathfindingSystem _pathfinder; + private AiControllerComponent _controller; + + // Input + protected IEntity Owner; + + protected void Setup(IEntity owner) + { + Owner = owner; + _mapManager = IoCManager.Resolve(); + _pathfinder = IoCManager.Resolve().GetEntitySystem(); + if (!Owner.TryGetComponent(out AiControllerComponent controllerComponent)) + { + throw new InvalidOperationException(); + } + + _controller = controllerComponent; + } + + protected void NextTile() + { + MovedATile?.Invoke(); + } + + /// + /// Will move the AI towards the next position + /// + /// true if movement to be done + protected bool TryMove() + { + // Use collidable just so we don't get stuck on corners as much + // var targetDiff = NextGrid.Position - _ownerCollidable.WorldAABB.Center; + var targetDiff = NextGrid.Position - Owner.Transform.GridPosition.Position; + + // Check distance + if (targetDiff.Length < TileTolerance) + { + return false; + } + // Move towards it + if (_controller == null) + { + return false; + } + _controller.VelocityDir = _addedAngle.RotateVec(targetDiff).Normalized; + return true; + + } + + /// + /// Will try and get around obstacles if stuck + /// + protected void AntiStuck(float frameTime) + { + // TODO: More work because these are sketchy af + // TODO: Check if a wall was spawned in front of us and then immediately dump route if it was + + // First check if we're still in a stuck state from last frame + if (IsStuck && !_tryingAntiStuck) + { + switch (_antiStuckMethod) + { + case AntiStuckMethod.None: + break; + case AntiStuckMethod.Jiggle: + var randomRange = IoCManager.Resolve().Next(0, 359); + var angle = Angle.FromDegrees(randomRange); + Owner.TryGetComponent(out AiControllerComponent mover); + mover.VelocityDir = angle.ToVec().Normalized; + + break; + case AntiStuckMethod.PhaseThrough: + if (Owner.TryGetComponent(out CollidableComponent collidableComponent)) + { + // TODO Fix this because they are yeeting themselves when they charge + // TODO: If something updates this this will fuck it + collidableComponent.CanCollide = false; + + Timer.Spawn(100, () => + { + if (!collidableComponent.CanCollide) + { + collidableComponent.CanCollide = true; + } + }); + } + break; + case AntiStuckMethod.Teleport: + Owner.Transform.DetachParent(); + Owner.Transform.GridPosition = NextGrid; + break; + case AntiStuckMethod.ReRoute: + GetRoute(); + break; + case AntiStuckMethod.Angle: + var random = IoCManager.Resolve(); + _addedAngle = new Angle(random.Next(-60, 60)); + IsStuck = false; + Timer.Spawn(100, () => + { + _addedAngle = Angle.Zero; + }); + break; + default: + throw new InvalidOperationException(); + } + } + + _stuckTimerRemaining -= frameTime; + + // Stuck check cooldown + if (_stuckTimerRemaining > 0.0f) + { + return; + } + + _tryingAntiStuck = false; + _stuckTimerRemaining = 0.5f; + + // Are we actually stuck + if ((_ourLastPosition.Position - Owner.Transform.GridPosition.Position).Length < TileTolerance) + { + _antiStuckAttempts++; + + // Maybe it's just 1 tile that's borked so try next 1? + if (_antiStuckAttempts >= 2 && _antiStuckAttempts < 5 && Route.Count > 1) + { + var nextTile = Route.Dequeue(); + NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices); + return; + } + + if (_antiStuckAttempts >= 5 || Route.Count == 0) + { + Logger.DebugS("ai", $"{Owner} is stuck at {Owner.Transform.GridPosition}, trying new route"); + _antiStuckAttempts = 0; + IsStuck = false; + _ourLastPosition = Owner.Transform.GridPosition; + GetRoute(); + return; + } + Stuck?.Invoke(); + IsStuck = true; + return; + } + + IsStuck = false; + + _ourLastPosition = Owner.Transform.GridPosition; + } + + /// + /// Tells us we don't need to keep moving and resets everything + /// + public void HaveArrived() + { + _routeCancelToken?.Cancel(); // oh thank god no more pathfinding + Route.Clear(); + if (_controller == null) return; + _controller.VelocityDir = Vector2.Zero; + } + + protected void GetRoute() + { + _routeCancelToken?.Cancel(); + _routeCancelToken = new CancellationTokenSource(); + Route.Clear(); + + int collisionMask; + if (!Owner.TryGetComponent(out CollidableComponent collidableComponent)) + { + collisionMask = 0; + } + else + { + collisionMask = collidableComponent.CollisionMask; + } + + var startGrid = _mapManager.GetGrid(Owner.Transform.GridID).GetTileRef(Owner.Transform.GridPosition); + var endGrid = _mapManager.GetGrid(TargetGrid.GridID).GetTileRef(TargetGrid);; + // _routeCancelToken = new CancellationTokenSource(); + + RouteJob = _pathfinder.RequestPath(new PathfindingArgs( + Owner.Uid, + collisionMask, + startGrid, + endGrid, + PathfindingProximity + ), _routeCancelToken.Token); + } + + protected void ReceivedRoute() + { + Route = RouteJob.Result; + RouteJob = null; + + if (Route == null) + { + Route = new Queue(); + // Couldn't find a route to target + return; + } + + // Because the entity may be half on 2 tiles we'll just cut out the first tile. + // This may not be the best solution but sometimes if the AI is chasing for example it will + // stutter backwards to the first tile again. + Route.Dequeue(); + + var nextTile = Route.Peek(); + NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices); + } + + public override Outcome Execute(float frameTime) + { + if (RouteJob != null && RouteJob.Status == JobStatus.Finished) + { + ReceivedRoute(); + } + + return !ActionBlockerSystem.CanMove(Owner) ? Outcome.Failed : Outcome.Continuing; + } + } + + public enum AntiStuckMethod + { + None, + ReRoute, + Jiggle, // Just pick a random direction for a bit and hope for the best + Teleport, // The Half-Life 2 method + PhaseThrough, // Just makes it non-collidable + Angle, // Add a different angle for a bit + } +} diff --git a/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs b/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs new file mode 100644 index 0000000000..317ef7d9e1 --- /dev/null +++ b/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.Server.AI.Operators.Movement +{ + public sealed class MoveToEntityOperator : BaseMover + { + // Instance + private GridCoordinates _lastTargetPosition; + private IMapManager _mapManager; + + // Input + public IEntity Target { get; } + public float DesiredRange { get; set; } + + public MoveToEntityOperator(IEntity owner, IEntity target, float desiredRange = 1.5f) + { + Setup(owner); + Target = target; + _mapManager = IoCManager.Resolve(); + DesiredRange = desiredRange; + } + + public override Outcome Execute(float frameTime) + { + var baseOutcome = base.Execute(frameTime); + // TODO: Given this is probably the most common operator whatever speed boosts you can do here will be gucci + // Could also look at running it every other tick. + + if (baseOutcome == Outcome.Failed || + Target == null || + Target.Deleted || + Target.Transform.GridID != Owner.Transform.GridID) + { + HaveArrived(); + return Outcome.Failed; + } + + if (RouteJob != null) + { + if (RouteJob.Status != JobStatus.Finished) + { + return Outcome.Continuing; + } + ReceivedRoute(); + return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing; + } + + var targetRange = (Target.Transform.GridPosition.Position - Owner.Transform.GridPosition.Position).Length; + + // If they move near us + if (targetRange <= DesiredRange) + { + HaveArrived(); + return Outcome.Success; + } + + // If the target's moved we may need to re-route. + // First we'll check if they're near another tile on the existing route and if so + // we can trim up until that point. + if (_lastTargetPosition != default && + (Target.Transform.GridPosition.Position - _lastTargetPosition.Position).Length > 1.5f) + { + var success = false; + // Technically it should be Route.Count - 1 but if the route's empty it'll throw + var newRoute = new Queue(Route.Count); + + for (var i = 0; i < Route.Count; i++) + { + var tile = Route.Dequeue(); + newRoute.Enqueue(tile); + var tileGrid = _mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + + // Don't use DesiredRange here or above in case it's smaller than a tile; + // when we get close we run straight at them anyway so it shooouullddd be okay... + if ((Target.Transform.GridPosition.Position - tileGrid.Position).Length < 1.5f) + { + success = true; + break; + } + } + + if (success) + { + Route = newRoute; + _lastTargetPosition = Target.Transform.GridPosition; + TargetGrid = Target.Transform.GridPosition; + return Outcome.Continuing; + } + + _lastTargetPosition = default; + } + + // If they move too far or no route + if (_lastTargetPosition == default) + { + // If they're further we could try pathfinding from the furthest tile potentially? + _lastTargetPosition = Target.Transform.GridPosition; + TargetGrid = Target.Transform.GridPosition; + GetRoute(); + return Outcome.Continuing; + } + + AntiStuck(frameTime); + + if (IsStuck) + { + return Outcome.Continuing; + } + + if (TryMove()) + { + return Outcome.Continuing; + } + + // If we're really close just try bee-lining it? + if (Route.Count == 0) + { + if (targetRange < 1.9f) + { + // TODO: If they have a phat hitbox they could block us + NextGrid = TargetGrid; + return Outcome.Continuing; + } + if (targetRange > DesiredRange) + { + HaveArrived(); + return Outcome.Failed; + } + } + + var nextTile = Route.Dequeue(); + NextTile(); + NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices); + return Outcome.Continuing; + } + } +} diff --git a/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs b/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs new file mode 100644 index 0000000000..ad866573ab --- /dev/null +++ b/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs @@ -0,0 +1,94 @@ +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.Server.AI.Operators.Movement +{ + public class MoveToGridOperator : BaseMover + { + private IMapManager _mapManager; + private float _desiredRange; + + public MoveToGridOperator( + IEntity owner, + GridCoordinates gridPosition, + float desiredRange = 1.5f) + { + Setup(owner); + TargetGrid = gridPosition; + _mapManager = IoCManager.Resolve(); + PathfindingProximity = 0.2f; // Accept no substitutes + _desiredRange = desiredRange; + } + + public void UpdateTarget(GridCoordinates newTarget) + { + TargetGrid = newTarget; + HaveArrived(); + GetRoute(); + } + + public override Outcome Execute(float frameTime) + { + var baseOutcome = base.Execute(frameTime); + + if (baseOutcome == Outcome.Failed || + TargetGrid.GridID != Owner.Transform.GridID) + { + HaveArrived(); + return Outcome.Failed; + } + + if (RouteJob != null) + { + if (RouteJob.Status != JobStatus.Finished) + { + return Outcome.Continuing; + } + ReceivedRoute(); + return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing; + } + + var targetRange = (TargetGrid.Position - Owner.Transform.GridPosition.Position).Length; + + // We there + if (targetRange <= _desiredRange) + { + HaveArrived(); + return Outcome.Success; + } + + // No route + if (Route.Count == 0 && RouteJob == null) + { + GetRoute(); + return Outcome.Continuing; + } + + AntiStuck(frameTime); + + if (IsStuck) + { + return Outcome.Continuing; + } + + if (TryMove()) + { + return Outcome.Continuing; + } + + if (Route.Count == 0 && targetRange > 1.5f) + { + HaveArrived(); + return Outcome.Failed; + } + + var nextTile = Route.Dequeue(); + NextTile(); + NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices); + return Outcome.Continuing; + } + } +} diff --git a/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs b/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs new file mode 100644 index 0000000000..65922d2366 --- /dev/null +++ b/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Operators.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Operators.Sequences +{ + public class GoPickupEntitySequence : SequenceOperator + { + public GoPickupEntitySequence(IEntity owner, IEntity target) + { + Sequence = new Queue(new AiOperator[] + { + new MoveToEntityOperator(owner, target), + new OpenStorageOperator(owner, target), + new PickupEntityOperator(owner, target), + }); + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Operators/Sequences/SequenceOperator.cs b/Content.Server/AI/Operators/Sequences/SequenceOperator.cs new file mode 100644 index 0000000000..0b103ffdb3 --- /dev/null +++ b/Content.Server/AI/Operators/Sequences/SequenceOperator.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace Content.Server.AI.Operators.Sequences +{ + /// + /// Sequential chain of operators + /// Saves having to duplicate stuff like MoveTo and PickUp everywhere + /// + public abstract class SequenceOperator : AiOperator + { + public Queue Sequence { get; protected set; } + + public override Outcome Execute(float frameTime) + { + if (Sequence.Count == 0) + { + return Outcome.Success; + } + + var op = Sequence.Peek(); + op.TryStartup(); + var outcome = op.Execute(frameTime); + + switch (outcome) + { + case Outcome.Success: + op.Shutdown(outcome); + // Not over until all operators are done + Sequence.Dequeue(); + return Outcome.Continuing; + case Outcome.Continuing: + return Outcome.Continuing; + case Outcome.Failed: + op.Shutdown(outcome); + Sequence.Clear(); + return Outcome.Failed; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/StaticBarkerProcessor.cs b/Content.Server/AI/StaticBarkerProcessor.cs deleted file mode 100644 index 2d0cdb163c..0000000000 --- a/Content.Server/AI/StaticBarkerProcessor.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Server.Interfaces.Chat; -using Robust.Server.AI; -using Robust.Shared.Interfaces.Timing; -using Robust.Shared.IoC; -using Robust.Shared.Utility; -using CannyFastMath; -using Math = CannyFastMath.Math; -using MathF = CannyFastMath.MathF; - -namespace Content.Server.AI -{ - /// - /// Designed for a a stationary entity that regularly advertises things (vending machine). - /// - [AiLogicProcessor("StaticBarker")] - class StaticBarkerProcessor : AiLogicProcessor - { -#pragma warning disable 649 - [Dependency] private readonly IGameTiming _timeMan; - [Dependency] private readonly IChatManager _chatMan; -#pragma warning restore 649 - - private static readonly TimeSpan MinimumDelay = TimeSpan.FromSeconds(15); - private TimeSpan _nextBark; - - - private static List slogans = new List - { - "Come try my great products today!", - "More value for the way you live.", - "Quality you'd expect at prices you wouldn't.", - "The right stuff. The right price.", - }; - - public override void Update(float frameTime) - { - if(_timeMan.CurTime < _nextBark) - return; - - var rngState = GenSeed(); - _nextBark = _timeMan.CurTime + MinimumDelay + TimeSpan.FromSeconds(Random01(ref rngState) * 10); - - var pick = (int)Math.Round(Random01(ref rngState) * (slogans.Count - 1)); - _chatMan.EntitySay(SelfEntity, slogans[pick]); - } - - private uint GenSeed() - { - return RotateRight((uint)_timeMan.CurTick.GetHashCode(), 11) ^ (uint)SelfEntity.Uid.GetHashCode(); - } - - private uint RotateRight(uint n, int s) - { - return (n << (32 - s)) | (n >> s); - } - - private float Random01(ref uint state) - { - DebugTools.Assert(state != 0); - - //xorshift32 - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - return state / (float)uint.MaxValue; - } - } -} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs new file mode 100644 index 0000000000..1d382044d1 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Clothing; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Gloves +{ + public sealed class EquipGloves : UtilityAction + { + private IEntity _entity; + + public EquipGloves(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.GLOVES, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs new file mode 100644 index 0000000000..b2402d4065 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs @@ -0,0 +1,43 @@ +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.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Gloves +{ + public sealed class PickUpGloves : UtilityAction + { + private readonly IEntity _entity; + + public PickUpGloves(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.GLOVES, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.GLOVES, + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs new file mode 100644 index 0000000000..95bd1a772f --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Clothing; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Head +{ + public sealed class EquipHead : UtilityAction + { + private IEntity _entity; + + public EquipHead(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.HEAD, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs new file mode 100644 index 0000000000..98a999cc8d --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs @@ -0,0 +1,43 @@ +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.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Head +{ + public sealed class PickUpHead : UtilityAction + { + private readonly IEntity _entity; + + public PickUpHead(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.HEAD, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.HEAD, + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs new file mode 100644 index 0000000000..6d1eae4445 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Clothing; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing +{ + public sealed class EquipOuterClothing : UtilityAction + { + private IEntity _entity; + + public EquipOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.OUTERCLOTHING, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs new file mode 100644 index 0000000000..359c4ccdd4 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs @@ -0,0 +1,43 @@ +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.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing +{ + public sealed class PickUpOuterClothing : UtilityAction + { + private readonly IEntity _entity; + + public PickUpOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.OUTERCLOTHING, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.OUTERCLOTHING, + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs new file mode 100644 index 0000000000..78dd53ab73 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Clothing; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Shoes +{ + public sealed class EquipShoes : UtilityAction + { + private IEntity _entity; + + public EquipShoes(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.SHOES, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs new file mode 100644 index 0000000000..f755873688 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs @@ -0,0 +1,43 @@ +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.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Clothing.Shoes +{ + public sealed class PickUpShoes : UtilityAction + { + private readonly IEntity _entity; + + public PickUpShoes(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new ClothingInSlotCon(EquipmentSlotDefines.Slots.SHOES, + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.SHOES, + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs new file mode 100644 index 0000000000..0fdb5e02a2 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Melee; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Melee +{ + public sealed class EquipMelee : UtilityAction + { + private IEntity _entity; + + public EquipMelee(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity) + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new MeleeWeaponEquippedCon( + new InverseBoolCurve()), + // We'll prioritise equipping ranged weapons; If we try and score this then it'll just keep swapping between ranged and melee + new RangedWeaponEquippedCon( + new InverseBoolCurve()), + new CanPutTargetInHandsCon( + new BoolCurve()), + new MeleeWeaponSpeedCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new MeleeWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs new file mode 100644 index 0000000000..403ad6411a --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Combat; +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.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.AI.WorldState.States.Movement; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Melee +{ + public sealed class MeleeAttackEntity : UtilityAction + { + private IEntity _entity; + + public MeleeAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + var moveOperator = new MoveToEntityOperator(Owner, _entity); + var equipped = context.GetState().GetValue(); + if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) + { + moveOperator.DesiredRange = meleeWeaponComponent.Range - 0.01f; + } + + ActionOperators = new Queue(new AiOperator[] + { + moveOperator, + new SwingMeleeWeaponOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + var equipped = context.GetState().GetValue(); + context.GetState().SetValue(equipped); + } + + protected override Consideration[] Considerations { get; } = { + // Check if we have a weapon; easy-out + new MeleeWeaponEquippedCon( + new BoolCurve()), + // Don't attack a dead target + new TargetIsDeadCon( + new InverseBoolCurve()), + // Deprioritise a target in crit + new TargetIsCritCon( + new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)), + // Somewhat prioritise distance + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + // Prefer weaker targets + new TargetHealthCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)), + new MeleeWeaponSpeedCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new MeleeWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs new file mode 100644 index 0000000000..ae4e8da589 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs @@ -0,0 +1,52 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Melee; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Melee +{ + public sealed class PickUpMeleeWeapon : UtilityAction + { + private IEntity _entity; + + public PickUpMeleeWeapon(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + new HasMeleeWeaponCon( + new InverseBoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + new MeleeWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + new MeleeWeaponSpeedCon( + new QuadraticCurve(-1.0f, 0.5f, 1.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs new file mode 100644 index 0000000000..a899644074 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Combat.Ranged; +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.Ranged.Ballistic; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic +{ + public sealed class BallisticAttackEntity : UtilityAction + { + private IEntity _entity; + private MoveToEntityOperator _moveOperator; + + public BallisticAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void Shutdown() + { + base.Shutdown(); + if (_moveOperator != null) + { + _moveOperator.MovedATile -= InLos; + } + } + + public override void SetupOperators(Blackboard context) + { + _moveOperator = new MoveToEntityOperator(Owner, _entity); + _moveOperator.MovedATile += InLos; + + // TODO: Accuracy in blackboard + ActionOperators = new Queue(new AiOperator[] + { + _moveOperator, + new ShootAtEntityOperator(Owner, _entity, 0.7f), + }); + + // We will do a quick check now to see if we even need to move which also saves a pathfind + InLos(); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + var equipped = context.GetState().GetValue(); + context.GetState().SetValue(equipped); + } + + protected override Consideration[] Considerations { get; } = { + // Check if we have a weapon; easy-out + new BallisticWeaponEquippedCon( + new BoolCurve()), + new BallisticAmmoCon( + new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)), + // Don't attack a dead target + new TargetIsDeadCon( + new InverseBoolCurve()), + // Deprioritise a target in crit + new TargetIsCritCon( + new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)), + // Somewhat prioritise distance + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)), + // Prefer weaker targets + new TargetHealthCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)), + }; + + private void InLos() + { + // This should only be called if the movement operator is the current one; + // if that turns out not to be the case we can just add a check here. + if (Visibility.InLineOfSight(Owner, _entity)) + { + _moveOperator.HaveArrived(); + var mover = ActionOperators.Dequeue(); + mover.Shutdown(Outcome.Success); + } + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs new file mode 100644 index 0000000000..15de797219 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic +{ + public sealed class DropEmptyBallistic : UtilityAction + { + public sealed override float Bonus => 20.0f; + private IEntity _entity; + + public DropEmptyBallistic(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new DropEntityOperator(Owner, _entity) + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new TargetInOurInventoryCon( + new BoolCurve()), + // Need to put in hands to drop + new CanPutTargetInHandsCon( + new BoolCurve()), + // Drop that sucker + new BallisticAmmoCon( + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs new file mode 100644 index 0000000000..3c3ec8d84c --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Melee; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic +{ + public sealed class EquipBallistic : UtilityAction + { + private IEntity _entity; + + public EquipBallistic(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity) + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new EquippedBallisticCon( + new InverseBoolCurve()), + new MeleeWeaponEquippedCon( + new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)), + new CanPutTargetInHandsCon( + new BoolCurve()), + new BallisticAmmoCon( + new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)), + new RangedWeaponFireRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs new file mode 100644 index 0000000000..cf897a19f9 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs @@ -0,0 +1,46 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic +{ + public sealed class PickUpAmmo : UtilityAction + { + private IEntity _entity; + + public PickUpAmmo(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + //TODO: Consider ammo's type and what guns we have + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs new file mode 100644 index 0000000000..ef1934565a --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs @@ -0,0 +1,57 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic +{ + public sealed class PickUpBallisticMagWeapon : UtilityAction + { + private IEntity _entity; + + public PickUpBallisticMagWeapon(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new HeldRangedWeaponsCon( + new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)), + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + // For now don't grab empty guns - at least until we can start storing stuff in inventory + new BallisticAmmoCon( + new BoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + new RangedWeaponFireRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + // TODO: Ballistic accuracy? Depends how the design transitions + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs new file mode 100644 index 0000000000..b8b6143dc6 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +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.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.AI.WorldState.States.Movement; +using Content.Server.GameObjects.Components.Power.Chargers; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class PutHitscanInCharger : UtilityAction + { + // Maybe a bad idea to not allow override + public override bool CanOverride => false; + private readonly IEntity _charger; + + public PutHitscanInCharger(IEntity owner, IEntity charger, float weight) : base(owner) + { + _charger = charger; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + var weapon = context.GetState().GetValue(); + + if (weapon == null || _charger.GetComponent().HeldItem != null) + { + ActionOperators = new Queue(); + return; + } + + ActionOperators = new Queue(new AiOperator[] + { + new MoveToEntityOperator(Owner, _charger), + new InteractWithEntityOperator(Owner, _charger), + // Separate task will deal with picking it up + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_charger); + context.GetState().SetValue(_charger); + } + + protected override Consideration[] Considerations { get; } = + { + new HitscanWeaponEquippedCon( + new BoolCurve()), + new HitscanChargerFullCon( + new InverseBoolCurve()), + new HitscanChargerRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + new HitscanChargeCon( + new QuadraticCurve(-1.2f, 2.0f, 1.2f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs new file mode 100644 index 0000000000..f56bde67e8 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class DropEmptyHitscan : UtilityAction + { + private IEntity _entity; + + public DropEmptyHitscan(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new DropEntityOperator(Owner, _entity) + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new TargetInOurInventoryCon( + new BoolCurve()), + // Need to put in hands to drop + new CanPutTargetInHandsCon( + new BoolCurve()), + // If completely empty then drop that sucker + new HitscanChargeCon( + new InverseBoolCurve()), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs new file mode 100644 index 0000000000..becf2a9e0b --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Melee; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Inventory; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class EquipHitscan : UtilityAction + { + private IEntity _entity; + + public EquipHitscan(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity) + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new EquippedHitscanCon( + new InverseBoolCurve()), + new MeleeWeaponEquippedCon( + new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)), + new CanPutTargetInHandsCon( + new BoolCurve()), + new HitscanChargeCon( + new QuadraticCurve(1.0f, 1.0f, 0.0f, 0.0f)), + new RangedWeaponFireRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new HitscanWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs new file mode 100644 index 0000000000..b79e4615db --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Combat.Ranged; +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.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class HitscanAttackEntity : UtilityAction + { + private IEntity _entity; + private MoveToEntityOperator _moveOperator; + + public HitscanAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void Shutdown() + { + base.Shutdown(); + if (_moveOperator != null) + { + _moveOperator.MovedATile -= InLos; + } + } + + public override void SetupOperators(Blackboard context) + { + _moveOperator = new MoveToEntityOperator(Owner, _entity); + _moveOperator.MovedATile += InLos; + + // TODO: Accuracy in blackboard + ActionOperators = new Queue(new AiOperator[] + { + _moveOperator, + new ShootAtEntityOperator(Owner, _entity, 0.7f), + }); + + InLos(); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + var equipped = context.GetState().GetValue(); + context.GetState().SetValue(equipped); + } + + protected override Consideration[] Considerations { get; } = { + // Check if we have a weapon; easy-out + new HitscanWeaponEquippedCon( + new BoolCurve()), + new HitscanChargeCon( + new QuadraticCurve(1.0f, 0.1f, 0.0f, 0.0f)), + // Don't attack a dead target + new TargetIsDeadCon( + new InverseBoolCurve()), + // Deprioritise a target in crit + new TargetIsCritCon( + new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)), + // Somewhat prioritise distance + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)), + // Prefer weaker targets + new TargetHealthCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)), + }; + + private void InLos() + { + // This should only be called if the movement operator is the current one; + // if that turns out not to be the case we can just add a check here. + if (Visibility.InLineOfSight(Owner, _entity)) + { + _moveOperator.HaveArrived(); + var mover = ActionOperators.Dequeue(); + mover.Shutdown(Outcome.Success); + } + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs new file mode 100644 index 0000000000..7bd1fd48f1 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Combat.Ranged; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Operators.Movement; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class PickUpHitscanFromCharger : UtilityAction + { + private IEntity _entity; + private IEntity _charger; + + public PickUpHitscanFromCharger(IEntity owner, IEntity entity, IEntity charger, float weight) : base(owner) + { + _entity = entity; + _charger = charger; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new MoveToEntityOperator(Owner, _charger), + new WaitForHitscanChargeOperator(_entity), + new PickupEntityOperator(Owner, _entity), + }); + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new HeldRangedWeaponsCon( + new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)), + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + // TODO: ChargerHasPower + new RangedWeaponFireRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new HitscanWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs new file mode 100644 index 0000000000..463de4f0f9 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs @@ -0,0 +1,59 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Combat.Ranged; +using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.AI.WorldState.States.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan +{ + public sealed class PickUpHitscanWeapon : UtilityAction + { + private IEntity _entity; + + public PickUpHitscanWeapon(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + context.GetState().SetValue(_entity); + } + + protected override Consideration[] Considerations { get; } = { + new HeldRangedWeaponsCon( + new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)), + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + // For now don't grab empty guns - at least until we can start storing stuff in inventory + new HitscanChargeCon( + new BoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + // TODO: Weapon charge level + new RangedWeaponFireRateCon( + new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)), + new HitscanWeaponDamageCon( + new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)), + }; + } +} diff --git a/Content.Server/AI/Utility/Actions/IAiUtility.cs b/Content.Server/AI/Utility/Actions/IAiUtility.cs new file mode 100644 index 0000000000..679d410cb7 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/IAiUtility.cs @@ -0,0 +1,9 @@ +using Content.Server.AI.Utility.AiLogic; + +namespace Content.Server.AI.Utility.Actions +{ + public interface IAiUtility + { + float Bonus { get; } + } +} diff --git a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs new file mode 100644 index 0000000000..1fd934cdc2 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +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.Movement; +using Content.Server.AI.Utility.Considerations.State; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Idle +{ + /// + /// If we just picked up a bunch of stuff and have time then close it + /// + public sealed class CloseLastEntityStorage : UtilityAction + { + public override float Bonus => 1.5f; + + public CloseLastEntityStorage(IEntity owner) : base(owner) {} + + protected override Consideration[] Considerations => new Consideration[] + { + new StoredStateIsNullCon( + new InverseBoolCurve()), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + }; + public override void SetupOperators(Blackboard context) + { + var lastStorage = context.GetState().GetValue(); + + ActionOperators = new Queue(new AiOperator[] + { + new MoveToEntityOperator(Owner, lastStorage), + new CloseLastStorageOperator(Owner), + }); + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs new file mode 100644 index 0000000000..52f03c4046 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Generic; +using Content.Server.AI.Operators.Movement; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.ActionBlocker; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Random; + +namespace Content.Server.AI.Utility.Actions.Idle +{ + /// + /// Will move to a random spot close by + /// + public sealed class WanderAndWait : UtilityAction + { + public override bool CanOverride => false; + public override float Bonus => IdleBonus; + + public WanderAndWait(IEntity owner) : base(owner) + { + // TODO: Need a Success method that gets called to update context (e.g. when we last did X) + } + + public override void SetupOperators(Blackboard context) + { + var randomGrid = FindRandomGrid(); + float waitTime; + if (randomGrid != GridCoordinates.InvalidGrid) + { + var random = IoCManager.Resolve(); + waitTime = random.NextFloat() * 10; + } + else + { + waitTime = 0.0f; + } + + ActionOperators = new Queue(new AiOperator[] + { + new MoveToGridOperator(Owner, randomGrid), + new WaitOperator(waitTime), + }); + } + + protected override Consideration[] Considerations { get; } = { + new CanMoveCon( + new BoolCurve()) + // Last wander? If we also want to sit still + }; + + private GridCoordinates FindRandomGrid() + { + var mapManager = IoCManager.Resolve(); + var grid = mapManager.GetGrid(Owner.Transform.GridID); + + // 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; + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs new file mode 100644 index 0000000000..e9c181c5b7 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs @@ -0,0 +1,49 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Considerations.Nutrition.Drink; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Nutrition.Drink +{ + public sealed class PickUpDrink : UtilityAction + { + private IEntity _entity; + + public PickUpDrink(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override Consideration[] Considerations => new Consideration[] + { + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + new ThirstCon( + new LogisticCurve(1000f, 1.3f, -1.0f, 0.5f)), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + new DrinkValueCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)), + }; + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs new file mode 100644 index 0000000000..eae360a69e --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Nutrition.Drink; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Nutrition.Drink +{ + public sealed class UseDrinkInInventory : UtilityAction + { + private IEntity _entity; + + public UseDrinkInInventory(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override Consideration[] Considerations => new Consideration[] + { + new TargetInOurHandsCon( + new BoolCurve()), + new ThirstCon( + new LogisticCurve(1000f, 1.3f, -0.3f, 0.5f)), + new DrinkValueCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)) + }; + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs new file mode 100644 index 0000000000..9db0b814dd --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs @@ -0,0 +1,49 @@ +using Content.Server.AI.Operators.Sequences; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Containers; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Movement; +using Content.Server.AI.Utility.Considerations.Nutrition; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Nutrition.Food +{ + public sealed class PickUpFood : UtilityAction + { + private IEntity _entity; + + public PickUpFood(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; + } + + protected override Consideration[] Considerations => new Consideration[] + { + new TargetAccessibleCon( + new BoolCurve()), + new FreeHandCon( + new BoolCurve()), + new HungerCon( + new LogisticCurve(1000f, 1.3f, -1.0f, 0.5f)), + new DistanceCon( + new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)), + new FoodValueCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)), + }; + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs new file mode 100644 index 0000000000..2996cf3769 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Inventory; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Considerations.Hands; +using Content.Server.AI.Utility.Considerations.Nutrition; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.Actions.Nutrition.Food +{ + public sealed class UseFoodInInventory : UtilityAction + { + private IEntity _entity; + + public UseFoodInInventory(IEntity owner, IEntity entity, float weight) : base(owner) + { + _entity = entity; + Bonus = weight; + } + + public override void SetupOperators(Blackboard context) + { + ActionOperators = new Queue(new AiOperator[] + { + new EquipEntityOperator(Owner, _entity), + new UseItemInHandsOperator(Owner, _entity), + }); + } + + protected override Consideration[] Considerations => new Consideration[] + { + new TargetInOurHandsCon( + new BoolCurve()), + new HungerCon( + new LogisticCurve(1000f, 1.3f, -0.3f, 0.5f)), + new FoodValueCon( + new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)) + }; + + protected override void UpdateBlackboard(Blackboard context) + { + base.UpdateBlackboard(context); + context.GetState().SetValue(_entity); + } + } +} diff --git a/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs new file mode 100644 index 0000000000..a2f49c1c79 --- /dev/null +++ b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Movement; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Server.AI.Utility.Actions.Test +{ + /// + /// Used for pathfinding debugging + /// + public class MoveRightAndLeftTen : UtilityAction + { + public override bool CanOverride => false; + + public MoveRightAndLeftTen(IEntity owner) : base(owner) {} + + protected override Consideration[] Considerations { get; } = { + new DummyCon( + new BoolCurve()) + }; + + public override void SetupOperators(Blackboard context) + { + var currentPosition = Owner.Transform.GridPosition; + var nextPosition = Owner.Transform.GridPosition.Offset(new Vector2(10.0f, 0.0f)); + var originalPosOp = new MoveToGridOperator(Owner, currentPosition, 0.25f); + var newPosOp = new MoveToGridOperator(Owner, nextPosition, 0.25f); + + ActionOperators = new Queue(new AiOperator[] + { + newPosOp, + originalPosOp + }); + } + } +} diff --git a/Content.Server/AI/Utility/Actions/UtilityAction.cs b/Content.Server/AI/Utility/Actions/UtilityAction.cs new file mode 100644 index 0000000000..1d5a385beb --- /dev/null +++ b/Content.Server/AI/Utility/Actions/UtilityAction.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Operators; +using Content.Server.AI.Utility.Considerations; +using Content.Server.AI.WorldState; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Utility; + +namespace Content.Server.AI.Utility.Actions +{ + /// + /// The same DSE can be used across multiple actions. + /// + public abstract class UtilityAction : IAiUtility + { + /// + /// If we're trying to find a new action can we replace a currently running one with one of the same type. + /// e.g. If you're already wandering you don't want to replace it with a different wander. + /// + public virtual bool CanOverride => false; + + /// + /// This is used to sort actions; if there's a top-tier action available we won't bother checking the lower tiers. + /// Threshold doesn't necessarily mean we'll do an action at a higher threshold; + /// if it's really un-optimal (i.e. low score) then we'll also check lower tiers + /// + public virtual float Bonus { get; protected set; } = IdleBonus; + // For GW2 they had the bonuses close together but IMO it feels better when they're more like discrete tiers. + + // These are just baselines to make mass-updates easier; actions can do whatever + // e.g. if you want shooting a gun to be considered before picking up a gun you could + 1.0f it or w/e + public const float IdleBonus = 1.0f; + public const float NormalBonus = 5.0f; + public const float NeedsBonus = 10.0f; + public const float CombatPrepBonus = 20.0f; + public const float CombatBonus = 30.0f; + public const float DangerBonus = 50.0f; + + protected IEntity Owner { get; } + + /// + /// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible. + /// Ideally you put anything that's easy to assess and can cause an early-out first just so the rest aren't evaluated. + /// + protected abstract Consideration[] Considerations { get; } + + /// + /// To keep the operators simple we can chain them together here, e.g. move to can be chained with other operators. + /// + public Queue ActionOperators { get; protected set; } + + /// + /// Sometimes we may need to set the target for an action or the likes. + /// This is mainly useful for expandable states so each one can have a separate target. + /// + /// + protected virtual void UpdateBlackboard(Blackboard context) {} + + protected UtilityAction(IEntity owner) + { + Owner = owner; + } + + public virtual void Shutdown() {} + + /// + /// If this action is chosen then setup the operators to run. This also allows for operators to be reset. + /// + public abstract void SetupOperators(Blackboard context); + + // Call the task's operator with Execute and get the outcome + public Outcome Execute(float frameTime) + { + if (!ActionOperators.TryPeek(out var op)) + { + return Outcome.Success; + } + + op.TryStartup(); + var outcome = op.Execute(frameTime); + + switch (outcome) + { + case Outcome.Success: + op.Shutdown(outcome); + ActionOperators.Dequeue(); + break; + case Outcome.Continuing: + break; + case Outcome.Failed: + op.Shutdown(outcome); + ActionOperators.Clear(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return outcome; + } + + /// + /// AKA the Decision Score Evaluator (DSE) + /// This is where the magic happens + /// + /// + /// + /// + /// + public float GetScore(Blackboard context, float min) + { + UpdateBlackboard(context); + DebugTools.Assert(Considerations.Length > 0); + // I used the IAUS video although I did have some confusion on how to structure it overall + // as some of the slides seemed contradictory + + // Ideally we should early-out each action as cheaply as possible if it's not valid + + // We also need some way to tell if the action isn't going to + // have a better score than the current action (if applicable) and early-out that way as well. + + // 23:00 Building a better centaur + var finalScore = 1.0f; + var minThreshold = min / Bonus; + var modificationFactor = 1.0f - 1.0f / Considerations.Length; + // See 10:09 for this and the adjustments + + foreach (var consideration in Considerations) + { + var score = consideration.GetScore(context); + var makeUpValue = (1.0f - score) * modificationFactor; + var adjustedScore = score + makeUpValue * score; + var response = consideration.ComputeResponseCurve(adjustedScore); + + finalScore *= response; + + DebugTools.Assert(!float.IsNaN(response)); + + // The score can only ever go down from each consideration so if we're below minimum no point continuing. + if (0.0f >= finalScore || finalScore < minThreshold) { + return 0.0f; + } + } + + DebugTools.Assert(finalScore <= 1.0f); + + return finalScore * Bonus; + } + } +} diff --git a/Content.Server/AI/Utility/AiLogic/Civilian.cs b/Content.Server/AI/Utility/AiLogic/Civilian.cs new file mode 100644 index 0000000000..03c621e8a2 --- /dev/null +++ b/Content.Server/AI/Utility/AiLogic/Civilian.cs @@ -0,0 +1,21 @@ +using Content.Server.AI.Utility.BehaviorSets; +using JetBrains.Annotations; +using Robust.Server.AI; + +namespace Content.Server.AI.Utility.AiLogic +{ + [AiLogicProcessor("Civilian")] + [UsedImplicitly] + public sealed class Civilian : UtilityAi + { + public override void Setup() + { + base.Setup(); + AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false); + AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false); + AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false); + AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false); + SortActions(); + } + } +} diff --git a/Content.Server/AI/Utility/AiLogic/PathingDummy.cs b/Content.Server/AI/Utility/AiLogic/PathingDummy.cs new file mode 100644 index 0000000000..3c493021b3 --- /dev/null +++ b/Content.Server/AI/Utility/AiLogic/PathingDummy.cs @@ -0,0 +1,18 @@ +using Content.Server.AI.Utility.BehaviorSets; +using JetBrains.Annotations; +using Robust.Server.AI; + +namespace Content.Server.AI.Utility.AiLogic +{ + [AiLogicProcessor("PathingDummy")] + [UsedImplicitly] + public sealed class PathingDummy : UtilityAi + { + public override void Setup() + { + base.Setup(); + BehaviorSets.Add(typeof(PathingDummyBehaviorSet), new PathingDummyBehaviorSet(SelfEntity)); + SortActions(); + } + } +} diff --git a/Content.Server/AI/Utility/AiLogic/Spirate.cs b/Content.Server/AI/Utility/AiLogic/Spirate.cs new file mode 100644 index 0000000000..9a8935f57f --- /dev/null +++ b/Content.Server/AI/Utility/AiLogic/Spirate.cs @@ -0,0 +1,22 @@ +using Content.Server.AI.Utility.BehaviorSets; +using JetBrains.Annotations; +using Robust.Server.AI; + +namespace Content.Server.AI.Utility.AiLogic +{ + [AiLogicProcessor("Spirate")] + [UsedImplicitly] + public sealed class Spirate : UtilityAi + { + public override void Setup() + { + base.Setup(); + AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false); + AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false); + AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false); + AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false); + AddBehaviorSet(new SpirateBehaviorSet(SelfEntity), false); + SortActions(); + } + } +} diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs new file mode 100644 index 0000000000..0b76293e8f --- /dev/null +++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Content.Server.AI.Operators; +using Content.Server.AI.Operators.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.BehaviorSets; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Utility; +using Content.Server.GameObjects; +using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Robust.Server.AI; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Content.Server.AI.Utility.AiLogic +{ + public abstract class UtilityAi : AiLogicProcessor + { + // TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT) + // Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity) + private AiActionSystem _planner; + public Blackboard Blackboard => _blackboard; + private Blackboard _blackboard; + + /// + /// The sum of all BehaviorSets gives us what actions the AI can take + /// + public Dictionary BehaviorSets { get; } = new Dictionary(); + private readonly List _availableActions = new List(); + + /// + /// The currently running action; most importantly are the operators. + /// + public UtilityAction CurrentAction { get; private set; } + + /// + /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown, + /// or if there's no players nearby increase it. + /// + public float PlanCooldown { get; } = 0.5f; + private float _planCooldownRemaining; + + /// + /// If we've requested a plan then wait patiently for the action + /// + private AiActionRequestJob _actionRequest; + + private CancellationTokenSource _actionCancellation; + + /// + /// If we can't do anything then stop thinking; should probably use ActionBlocker instead + /// + private bool _isDead = false; + + // These 2 methods will be used eventually if / when we get a director AI + public void AddBehaviorSet(T behaviorSet, bool sort = true) where T : BehaviorSet + { + if (BehaviorSets.TryAdd(typeof(T), behaviorSet) && sort) + { + SortActions(); + } + } + + public void RemoveBehaviorSet(Type behaviorSet) + { + DebugTools.Assert(behaviorSet.IsAssignableFrom(typeof(BehaviorSet))); + + if (BehaviorSets.ContainsKey(behaviorSet)) + { + BehaviorSets.Remove(behaviorSet); + SortActions(); + } + } + + /// + /// Whenever the behavior sets are changed we'll re-sort the actions by bonus + /// + protected void SortActions() + { + _availableActions.Clear(); + foreach (var set in BehaviorSets.Values) + { + foreach (var action in set.Actions) + { + var found = false; + + for (var i = 0; i < _availableActions.Count; i++) + { + if (_availableActions[i].Bonus < action.Bonus) + { + _availableActions.Insert(i, action); + found = true; + break; + } + } + + if (!found) + { + _availableActions.Add(action); + } + } + } + + _availableActions.Reverse(); + } + + public override void Setup() + { + base.Setup(); + _planCooldownRemaining = PlanCooldown; + _blackboard = new Blackboard(SelfEntity); + _planner = IoCManager.Resolve().GetEntitySystem(); + if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent)) + { + damageableComponent.DamageThresholdPassed += DeathHandle; + } + } + + public override void Shutdown() + { + // TODO: If DamageableComponent removed still need to unsubscribe? + if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent)) + { + damageableComponent.DamageThresholdPassed -= DeathHandle; + } + } + + private void DeathHandle(object sender, DamageThresholdPassedEventArgs eventArgs) + { + if (eventArgs.DamageThreshold.ThresholdType == ThresholdType.Death) + { + _isDead = true; + } + + // TODO: If we get healed - double-check what it should be + if (eventArgs.DamageThreshold.ThresholdType == ThresholdType.None) + { + _isDead = false; + } + } + + private void ReceivedAction() + { + var action = _actionRequest.Result; + _actionRequest = null; + // Actions with lower scores should be implicitly dumped by GetAction + // If we're not allowed to replace the action with an action of the same type then dump. + if (action == null || !action.CanOverride && CurrentAction?.GetType() == action.GetType()) + { + return; + } + + CurrentAction = action; + action.SetupOperators(_blackboard); + } + + public override void Update(float frameTime) + { + // If we can't do anything then there's no point thinking + if (_isDead || BehaviorSets.Count == 0) + { + _actionCancellation?.Cancel(); + _blackboard.GetState().SetValue(0.0f); + CurrentAction?.Shutdown(); + CurrentAction = null; + return; + } + + // If we asked for a new action we don't want to dump the existing one. + if (_actionRequest != null) + { + if (_actionRequest.Status != JobStatus.Finished) + { + return; + } + + ReceivedAction(); + // Do something next tick + return; + } + + _planCooldownRemaining -= frameTime; + + // Might find a better action while we're doing one already + if (_planCooldownRemaining <= 0.0f) + { + _planCooldownRemaining = PlanCooldown; + _actionCancellation = new CancellationTokenSource(); + _actionRequest = _planner.RequestAction(new AiActionRequest(SelfEntity.Uid, _blackboard, _availableActions), _actionCancellation); + + return; + } + + // When we spawn in we won't get an action for a bit + if (CurrentAction == null) + { + return; + } + + var outcome = CurrentAction.Execute(frameTime); + + switch (outcome) + { + case Outcome.Success: + if (CurrentAction.ActionOperators.Count == 0) + { + CurrentAction.Shutdown(); + CurrentAction = null; + // Nothing to compare new action to + _blackboard.GetState().SetValue(0.0f); + } + break; + case Outcome.Continuing: + break; + case Outcome.Failed: + CurrentAction.Shutdown(); + CurrentAction = null; + _blackboard.GetState().SetValue(0.0f); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs new file mode 100644 index 0000000000..0651885c7e --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs @@ -0,0 +1,22 @@ +using System.Collections; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + /// + /// AKA DecisionMaker in IAUS. Just a group of actions that can be dynamically added or taken away from an AI. + /// + public abstract class BehaviorSet + { + protected IEntity Owner; + + public BehaviorSet(IEntity owner) + { + Owner = owner; + } + + public IEnumerable Actions { get; protected set; } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs new file mode 100644 index 0000000000..eb0530bdb5 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs @@ -0,0 +1,27 @@ +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves; +using Content.Server.AI.Utility.ExpandableActions.Clothing.Head; +using Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing; +using Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public sealed class ClothingBehaviorSet : BehaviorSet + { + public ClothingBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new EquipAnyHeadExp(), + new EquipAnyOuterClothingExp(), + new EquipAnyGlovesExp(), + new EquipAnyShoesExp(), + new PickUpAnyNearbyHeadExp(), + new PickUpAnyNearbyOuterClothingExp(), + new PickUpAnyNearbyGlovesExp(), + new PickUpAnyNearbyShoesExp(), + }; + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs new file mode 100644 index 0000000000..a9c6117af1 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Nutrition; +using Content.Server.AI.Utility.ExpandableActions.Nutrition; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public sealed class HungerBehaviorSet : BehaviorSet + { + public HungerBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new PickUpNearbyFoodExp(), + new UseFoodInInventoryExp(), + }; + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs new file mode 100644 index 0000000000..ac1b843585 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs @@ -0,0 +1,18 @@ +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Idle; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public class IdleBehaviorSet : BehaviorSet + { + public IdleBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new CloseLastEntityStorage(Owner), + new WanderAndWait(Owner), + }; + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs new file mode 100644 index 0000000000..cb90072b53 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs @@ -0,0 +1,17 @@ +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Test; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public sealed class PathingDummyBehaviorSet : BehaviorSet + { + public PathingDummyBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new MoveRightAndLeftTen(owner), + }; + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs new file mode 100644 index 0000000000..4228c33517 --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.Utility.ExpandableActions.Combat; +using Content.Server.AI.Utility.ExpandableActions.Combat.Melee; +using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged; +using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public sealed class SpirateBehaviorSet : BehaviorSet + { + public SpirateBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new PickUpRangedExp(), + // TODO: Reload Ballistic + new DropEmptyBallisticExp(), + // TODO: Ideally long-term we should just store the weapons in backpack + new DropEmptyHitscanExp(), + new EquipMeleeExp(), + new EquipBallisticExp(), + new EquipHitscanExp(), + new PickUpHitscanFromChargersExp(), + new ChargeEquippedHitscanExp(), + new RangedAttackNearbySpeciesExp(), + new PickUpMeleeWeaponExp(), + new MeleeAttackNearbySpeciesExp(), + }; + } + } +} diff --git a/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs new file mode 100644 index 0000000000..7274d9034f --- /dev/null +++ b/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs @@ -0,0 +1,18 @@ +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.ExpandableActions.Nutrition; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility.BehaviorSets +{ + public sealed class ThirstBehaviorSet : BehaviorSet + { + public ThirstBehaviorSet(IEntity owner) : base(owner) + { + Actions = new IAiUtility[] + { + new PickUpNearbyDrinkExp(), + new UseDrinkInHandsExp(), + }; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs b/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs new file mode 100644 index 0000000000..5702009b0a --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs @@ -0,0 +1,23 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.EntitySystems; + +namespace Content.Server.AI.Utility.Considerations.ActionBlocker +{ + public sealed class CanMoveCon : Consideration + { + public CanMoveCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var self = context.GetState().GetValue(); + if (!ActionBlockerSystem.CanMove(self)) + { + return 0.0f; + } + + return 1.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs new file mode 100644 index 0000000000..7101e1ba1b --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs @@ -0,0 +1,38 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.Considerations.Clothing +{ + public sealed class ClothingInInventoryCon : Consideration + { + private readonly EquipmentSlotDefines.SlotFlags _slot; + + public ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags slotFlags, IResponseCurve curve) : base(curve) + { + _slot = slotFlags; + } + + public override float GetScore(Blackboard context) + { + var inventory = context.GetState().GetValue(); + + foreach (var entity in inventory) + { + if (!entity.TryGetComponent(out ClothingComponent clothingComponent)) + { + continue; + } + + if ((clothingComponent.SlotFlags & _slot) != 0) + { + return 1.0f; + } + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs new file mode 100644 index 0000000000..11b30c0027 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Clothing; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.Considerations.Clothing +{ + public class ClothingInSlotCon : Consideration + { + private EquipmentSlotDefines.Slots _slot; + + public ClothingInSlotCon(EquipmentSlotDefines.Slots slot, IResponseCurve curve) : base(curve) + { + _slot = slot; + } + + public override float GetScore(Blackboard context) + { + var inventory = context.GetState().GetValue(); + + return inventory.ContainsKey(_slot) ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs new file mode 100644 index 0000000000..5d9567589f --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Melee; + +namespace Content.Server.AI.Utility.Considerations.Combat.Melee +{ + public sealed class HasMeleeWeaponCon : Consideration + { + public HasMeleeWeaponCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + foreach (var item in context.GetState().GetValue()) + { + if (item.HasComponent()) + { + return 1.0f; + } + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs new file mode 100644 index 0000000000..31165ec6c0 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Melee; + +namespace Content.Server.AI.Utility.Considerations.Combat.Melee +{ + public sealed class MeleeWeaponDamageCon : Consideration + { + public MeleeWeaponDamageCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) + { + return 0.0f; + } + + // Just went with max health + return meleeWeaponComponent.Damage / 300.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs new file mode 100644 index 0000000000..3bca99db7a --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Melee; + +namespace Content.Server.AI.Utility.Considerations.Combat.Melee +{ + public sealed class MeleeWeaponEquippedCon : Consideration + { + public MeleeWeaponEquippedCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null) + { + return 0.0f; + } + + return equipped.HasComponent() ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs new file mode 100644 index 0000000000..1c16870804 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Melee; + +namespace Content.Server.AI.Utility.Considerations.Combat.Melee +{ + public sealed class MeleeWeaponSpeedCon : Consideration + { + public MeleeWeaponSpeedCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) + { + return 0.0f; + } + + return meleeWeaponComponent.CooldownTime / 10.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs new file mode 100644 index 0000000000..faa740bad6 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs @@ -0,0 +1,39 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic +{ + public class BallisticAmmoCon : Consideration + { + public BallisticAmmoCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var weapon = context.GetState().GetValue(); + + if (weapon == null || !weapon.TryGetComponent(out BallisticMagazineWeaponComponent ballistic)) + { + return 0.0f; + } + + var contained = ballistic.MagazineSlot.ContainedEntity; + + if (contained == null) + { + return 0.0f; + } + + var mag = contained.GetComponent(); + + if (mag.CountLoaded == 0) + { + // TODO: Do this better + return ballistic.GetChambered(0) != null ? 1.0f : 0.0f; + } + + return (float) mag.CountLoaded / mag.Capacity; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs new file mode 100644 index 0000000000..9e7672feaf --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic +{ + public class BallisticWeaponEquippedCon : Consideration + { + public BallisticWeaponEquippedCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null) + { + return 0.0f; + } + + // Maybe change this to BallisticMagazineWeapon + return equipped.HasComponent() ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs new file mode 100644 index 0000000000..d2d289fd3f --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic +{ + public class EquippedBallisticCon : Consideration + { + public EquippedBallisticCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null || !equipped.HasComponent()) + { + return 0.0f; + } + + return 1.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs new file mode 100644 index 0000000000..f27aa41256 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged +{ + public class HasTargetLosCon : Consideration + { + public HasTargetLosCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var owner = context.GetState().GetValue(); + var target = context.GetState().GetValue(); + if (target == null) + { + return 0.0f; + } + + return Visibility.InLineOfSight(owner, target) ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs new file mode 100644 index 0000000000..949db84849 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs @@ -0,0 +1,29 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Melee; +using Content.Server.GameObjects.Components.Weapon.Ranged; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged +{ + public sealed class HeldRangedWeaponsCon : Consideration + { + public HeldRangedWeaponsCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var count = 0; + const int max = 3; + + foreach (var item in context.GetState().GetValue()) + { + if (item.HasComponent()) + { + count++; + } + } + + return (float) count / max; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs new file mode 100644 index 0000000000..ae5c0b7bf1 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class EquippedHitscanCon : Consideration + { + public EquippedHitscanCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null || !equipped.HasComponent()) + { + return 0.0f; + } + + return 1.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs new file mode 100644 index 0000000000..2e24b8c8ca --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class HitscanChargeCon : Consideration + { + public HitscanChargeCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var weapon = context.GetState().GetValue(); + + if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent)) + { + return 0.0f; + } + + return hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.CapacitorComponent.Capacity; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs new file mode 100644 index 0000000000..eaad383287 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs @@ -0,0 +1,26 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Power.Chargers; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class HitscanChargerFullCon : Consideration + { + public HitscanChargerFullCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || + !target.TryGetComponent(out WeaponCapacitorChargerComponent chargerComponent) || + chargerComponent.HeldItem != null) + { + return 1.0f; + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs new file mode 100644 index 0000000000..87eb9e332d --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Power.Chargers; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class HitscanChargerRateCon : Consideration + { + public HitscanChargerRateCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + if (target == null || !target.TryGetComponent(out WeaponCapacitorChargerComponent weaponCharger)) + { + return 0.0f; + } + + // AI don't care about efficiency, psfft! + return weaponCharger.TransferRatio; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs new file mode 100644 index 0000000000..d3652a4da3 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class HitscanWeaponDamageCon : Consideration + { + public HitscanWeaponDamageCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var weapon = context.GetState().GetValue(); + + if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent)) + { + return 0.0f; + } + + // Just went with max health + return hitscanWeaponComponent.Damage / 300.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs new file mode 100644 index 0000000000..fae397aa5e --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan +{ + public sealed class HitscanWeaponEquippedCon : Consideration + { + public HitscanWeaponEquippedCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null) + { + return 0.0f; + } + + return equipped.HasComponent() ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs new file mode 100644 index 0000000000..52d2e48117 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged +{ + public sealed class RangedWeaponEquippedCon : Consideration + { + public RangedWeaponEquippedCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var equipped = context.GetState().GetValue(); + + if (equipped == null || !equipped.HasComponent()) + { + return 0.0f; + } + + return 1.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs new file mode 100644 index 0000000000..a44e5a9552 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States.Combat; +using Content.Server.GameObjects.Components.Weapon.Ranged; + +namespace Content.Server.AI.Utility.Considerations.Combat.Ranged +{ + public class RangedWeaponFireRateCon : Consideration + { + public RangedWeaponFireRateCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var weapon = context.GetState().GetValue(); + + if (weapon == null || !weapon.TryGetComponent(out RangedWeaponComponent ranged)) + { + return 0.0f; + } + + return ranged.FireRate / 100.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs new file mode 100644 index 0000000000..5f02a42ac2 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs @@ -0,0 +1,26 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; +using Content.Shared.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Combat +{ + public sealed class TargetHealthCon : Consideration + { + public TargetHealthCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || !target.TryGetComponent(out DamageableComponent damageableComponent)) + { + return 0.0f; + } + + // Just went with max health + return damageableComponent.CurrentDamage[DamageType.Total] / 300.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs new file mode 100644 index 0000000000..7528323e9f --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs @@ -0,0 +1,30 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; +using Content.Shared.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Combat +{ + public sealed class TargetIsCritCon : Consideration + { + public TargetIsCritCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent)) + { + return 0.0f; + } + + if (speciesComponent.CurrentDamageState is CriticalState) + { + return 1.0f; + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs new file mode 100644 index 0000000000..c3c7ebc80c --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs @@ -0,0 +1,29 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Combat +{ + public sealed class TargetIsDeadCon : Consideration + { + public TargetIsDeadCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent)) + { + return 0.0f; + } + + if (speciesComponent.CurrentDamageState is DeadState) + { + return 1.0f; + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Consideration.cs b/Content.Server/AI/Utility/Considerations/Consideration.cs new file mode 100644 index 0000000000..25694ec5e7 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Consideration.cs @@ -0,0 +1,25 @@ +using System; +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; + +namespace Content.Server.AI.Utility.Considerations +{ + public abstract class Consideration + { + protected IResponseCurve Curve { get; } + + public Consideration(IResponseCurve curve) + { + Curve = curve; + } + + public abstract float GetScore(Blackboard context); + + public float ComputeResponseCurve(float score) + { + var clampedScore = Math.Clamp(score, 0.0f, 1.0f); + var curvedResponse = Math.Clamp(Curve.GetResponse(clampedScore), 0.0f, 1.0f); + return curvedResponse; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs new file mode 100644 index 0000000000..a9af493745 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs @@ -0,0 +1,39 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components; +using Robust.Shared.Containers; + +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.) + /// + public sealed class TargetAccessibleCon : Consideration + { + public TargetAccessibleCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + if (target == null) + { + return 0.0f; + } + + if (ContainerHelpers.TryGetContainer(target, out var container)) + { + if (container.Owner.TryGetComponent(out EntityStorageComponent storageComponent)) + { + if (storageComponent.IsWeldedShut && !storageComponent.Open) + { + return 0.0f; + } + } + } + + return 1.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/DummyCon.cs b/Content.Server/AI/Utility/Considerations/DummyCon.cs new file mode 100644 index 0000000000..07d0587ddb --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/DummyCon.cs @@ -0,0 +1,12 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; + +namespace Content.Server.AI.Utility.Considerations +{ + public class DummyCon : Consideration + { + public DummyCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) => 1.0f; + } +} diff --git a/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs b/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs new file mode 100644 index 0000000000..2b2162d80f --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs @@ -0,0 +1,36 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Hands +{ + public class FreeHandCon : Consideration + { + public FreeHandCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var owner = context.GetState().GetValue(); + + if (!owner.TryGetComponent(out HandsComponent handsComponent)) + { + return 0.0f; + } + + var handCount = 0; + var freeCount = 0; + + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + handCount++; + if (handsComponent.GetHand(hand) == null) + { + freeCount += 1; + } + } + + return (float) freeCount / handCount; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs b/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs new file mode 100644 index 0000000000..cda872a05b --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs @@ -0,0 +1,30 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Hands +{ + /// + /// Returns 1 if in our hands else 0 + /// + public sealed class TargetInOurHandsCon : Consideration + { + public TargetInOurHandsCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var owner = context.GetState().GetValue(); + var target = context.GetState().GetValue(); + + if (target == null || + !target.HasComponent() || + !owner.TryGetComponent(out HandsComponent handsComponent)) + { + return 0.0f; + } + + return handsComponent.IsHolding(target) ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs new file mode 100644 index 0000000000..646cba8506 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs @@ -0,0 +1,38 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Hands; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Inventory +{ + public class CanPutTargetInHandsCon : Consideration + { + public CanPutTargetInHandsCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + // First check if target in inventory already + // If not then check if we have a free hand + var target = context.GetState().GetValue(); + + if (target == null || !target.HasComponent()) + { + return 0.0f; + } + + var inventory = context.GetState().GetValue(); + + foreach (var item in inventory) + { + if (item == target) + { + return 1.0f; + } + } + + return context.GetState().GetValue() ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs new file mode 100644 index 0000000000..1bd645df23 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs @@ -0,0 +1,34 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; + +namespace Content.Server.AI.Utility.Considerations.Inventory +{ + public class TargetInOurInventoryCon : Consideration + { + public TargetInOurInventoryCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var inventory = context.GetState().GetValue(); + var target = context.GetState().GetValue(); + + if (target == null || !target.HasComponent()) + { + return 0.0f; + } + + foreach (var item in inventory) + { + if (item == target) + { + return 1.0f; + } + } + + return 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs b/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs new file mode 100644 index 0000000000..3ecb715c8a --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; + +namespace Content.Server.AI.Utility.Considerations.Movement +{ + public sealed class DistanceCon : Consideration + { + public DistanceCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var self = context.GetState().GetValue(); + var target = context.GetState().GetValue(); + if (target == null || target.Transform.GridID != self.Transform.GridID) + { + return 0.0f; + } + + // TODO: Remove 1 - + // Kind of just pulled a max distance out of nowhere. Add 0.01 just in case it's reaally far and we have no choice so it'll still be considered at least. + return 1 - ((target.Transform.GridPosition.Position - self.Transform.GridPosition.Position).Length / 100 + 0.01f); + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs new file mode 100644 index 0000000000..73e6505e7e --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs @@ -0,0 +1,32 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Chemistry; + +namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink +{ + public sealed class DrinkValueCon : Consideration + { + public DrinkValueCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (!target.TryGetComponent(out SolutionComponent drink)) + { + return 0.0f; + } + + var nutritionValue = 0; + + foreach (var reagent in drink.ReagentList) + { + // TODO + nutritionValue += (reagent.Quantity * 30).Int(); + } + + return nutritionValue / 1000.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs new file mode 100644 index 0000000000..02e8719d2a --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Nutrition; + +namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink +{ + public class ThirstCon : Consideration + { + public ThirstCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var owner = context.GetState().GetValue(); + + if (!owner.TryGetComponent(out ThirstComponent thirst)) + { + return 0.0f; + } + + return 1 - (thirst.CurrentThirst / thirst.ThirstThresholds[ThirstThreshold.OverHydrated]); + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs new file mode 100644 index 0000000000..a316799c3d --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs @@ -0,0 +1,32 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Chemistry; + +namespace Content.Server.AI.Utility.Considerations.Nutrition +{ + public sealed class FoodValueCon : Consideration + { + public FoodValueCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var target = context.GetState().GetValue(); + + if (!target.TryGetComponent(out SolutionComponent food)) + { + return 0.0f; + } + + var nutritionValue = 0; + + foreach (var reagent in food.ReagentList) + { + // TODO + nutritionValue += (reagent.Quantity * 30).Int(); + } + + return nutritionValue / 1000.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs new file mode 100644 index 0000000000..170e7d22b8 --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Nutrition; + +namespace Content.Server.AI.Utility.Considerations.Nutrition +{ + + public sealed class HungerCon : Consideration + { + public HungerCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var owner = context.GetState().GetValue(); + + if (!owner.TryGetComponent(out HungerComponent hunger)) + { + return 0.0f; + } + + return 1 - (hunger.CurrentHunger / hunger.HungerThresholds[HungerThreshold.Overfed]); + } + } +} diff --git a/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs b/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs new file mode 100644 index 0000000000..b93757bf9e --- /dev/null +++ b/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs @@ -0,0 +1,24 @@ +using Content.Server.AI.Utility.Curves; +using Content.Server.AI.WorldState; + +namespace Content.Server.AI.Utility.Considerations.State +{ + /// + /// Simple NullCheck on a StoredState + /// + public sealed class StoredStateIsNullCon : Consideration where T : StoredStateData + { + public StoredStateIsNullCon(IResponseCurve curve) : base(curve) {} + + public override float GetScore(Blackboard context) + { + var state = context.GetState(); + if (state.GetValue() == null) + { + return 1.0f; + } + + return 0.0f; + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Utility/Curves/BoolCurve.cs b/Content.Server/AI/Utility/Curves/BoolCurve.cs new file mode 100644 index 0000000000..d7e86c3c8f --- /dev/null +++ b/Content.Server/AI/Utility/Curves/BoolCurve.cs @@ -0,0 +1,13 @@ +namespace Content.Server.AI.Utility.Curves +{ + /// + /// For stuff that's a simple 0.0f or 1.0f + /// + public struct BoolCurve : IResponseCurve + { + public float GetResponse(float score) + { + return score > 0.0f ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Curves/IResponseCurve.cs b/Content.Server/AI/Utility/Curves/IResponseCurve.cs new file mode 100644 index 0000000000..a2a78c839f --- /dev/null +++ b/Content.Server/AI/Utility/Curves/IResponseCurve.cs @@ -0,0 +1,10 @@ +namespace Content.Server.AI.Utility.Curves +{ + /// + /// Using an interface also lets us define preset curves that can be re-used + /// + public interface IResponseCurve + { + float GetResponse(float score); + } +} diff --git a/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs b/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs new file mode 100644 index 0000000000..4296ffee5b --- /dev/null +++ b/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs @@ -0,0 +1,11 @@ +namespace Content.Server.AI.Utility.Curves +{ + public struct InverseBoolCurve : IResponseCurve + { + public float GetResponse(float score) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return score == 0.0f ? 1.0f : 0.0f; + } + } +} diff --git a/Content.Server/AI/Utility/Curves/LogisticCurve.cs b/Content.Server/AI/Utility/Curves/LogisticCurve.cs new file mode 100644 index 0000000000..fb21e2a550 --- /dev/null +++ b/Content.Server/AI/Utility/Curves/LogisticCurve.cs @@ -0,0 +1,28 @@ +using System; + +namespace Content.Server.AI.Utility.Curves +{ + public struct LogisticCurve : IResponseCurve + { + private readonly float _slope; + + private readonly float _exponent; + // Vertical shift + private readonly float _yOffset; + // Horizontal shift + private readonly float _xOffset; + + public LogisticCurve(float slope, float exponent, float yOffset, float xOffset) + { + _slope = slope; + _exponent = exponent; + _yOffset = yOffset; + _xOffset = xOffset; + } + + public float GetResponse(float score) + { + return _exponent * (1 / (1 + (float) Math.Pow(Math.Log(1000) * _slope, -1 * score + _xOffset))) + _yOffset; + } + } +} diff --git a/Content.Server/AI/Utility/Curves/QuadraticCurve.cs b/Content.Server/AI/Utility/Curves/QuadraticCurve.cs new file mode 100644 index 0000000000..5933915aeb --- /dev/null +++ b/Content.Server/AI/Utility/Curves/QuadraticCurve.cs @@ -0,0 +1,31 @@ +using System; + +namespace Content.Server.AI.Utility.Curves +{ + /// + /// Also Linear + /// + public struct QuadraticCurve : IResponseCurve + { + private readonly float _slope; + + private readonly float _exponent; + // Vertical shift + private readonly float _yOffset; + // Horizontal shift + private readonly float _xOffset; + + public QuadraticCurve(float slope, float exponent, float yOffset, float xOffset) + { + _slope = slope; + _exponent = exponent; + _yOffset = yOffset; + _xOffset = xOffset; + } + + public float GetResponse(float score) + { + return _slope * (float) Math.Pow(score - _xOffset, _exponent) + _yOffset; + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs new file mode 100644 index 0000000000..700dff2f94 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Gloves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Movement; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves +{ + /// + /// Equip any head item currently in our inventory + /// + public sealed class EquipAnyGlovesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + if (!owner.TryGetComponent(out AiControllerComponent controller)) + { + throw new InvalidOperationException(); + } + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) + { + yield return new EquipGloves(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs new file mode 100644 index 0000000000..7b3f165e4a --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Gloves; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Clothing; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves +{ + public sealed class PickUpAnyNearbyGlovesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) + { + yield return new PickUpGloves(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs new file mode 100644 index 0000000000..f885e7818a --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Head; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Movement; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head +{ + /// + /// Equip any head item currently in our inventory + /// + public sealed class EquipAnyHeadExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) + { + yield return new EquipHead(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs new file mode 100644 index 0000000000..dfb2f6d2a9 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Head; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Clothing; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head +{ + public sealed class PickUpAnyNearbyHeadExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) + { + yield return new PickUpHead(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs new file mode 100644 index 0000000000..b7a7980974 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.OuterClothing; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing +{ + /// + /// Equip any head item currently in our inventory + /// + public sealed class EquipAnyOuterClothingExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) + { + yield return new EquipOuterClothing(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs new file mode 100644 index 0000000000..dd1d06c0c7 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.OuterClothing; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Clothing; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing +{ + public sealed class PickUpAnyNearbyOuterClothingExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) + { + yield return new PickUpOuterClothing(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs new file mode 100644 index 0000000000..517fd735c0 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Shoes; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes +{ + /// + /// Equip any head item currently in our inventory + /// + public sealed class EquipAnyShoesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) + { + yield return new EquipShoes(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs new file mode 100644 index 0000000000..598d180808 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Clothing.Shoes; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Clothing; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes +{ + public sealed class PickUpAnyNearbyShoesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NormalBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.TryGetComponent(out ClothingComponent clothing) && + (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) + { + yield return new PickUpShoes(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs new file mode 100644 index 0000000000..67bfac1c98 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Melee; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee +{ + public sealed class EquipMeleeExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new EquipMelee(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs new file mode 100644 index 0000000000..e70dba6c86 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Melee; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Movement; +using Robust.Server.GameObjects; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee +{ + public sealed class MeleeAttackNearbyPlayerExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + if (!owner.TryGetComponent(out AiControllerComponent controller)) + { + throw new InvalidOperationException(); + } + + foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent), + controller.VisionRadius)) + { + if (entity.HasComponent() && entity != owner) + { + yield return new MeleeAttackEntity(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs new file mode 100644 index 0000000000..ad00223233 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Melee; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Mobs; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee +{ + public sealed class MeleeAttackNearbySpeciesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + foreach (var entity in context.GetState().GetValue()) + { + yield return new MeleeAttackEntity(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs new file mode 100644 index 0000000000..781a72c2a1 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Melee; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat.Nearby; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee +{ + public sealed class PickUpMeleeWeaponExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new PickUpMeleeWeapon(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs new file mode 100644 index 0000000000..db3e281905 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic +{ + public sealed class DropEmptyBallisticExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.HasComponent()) + { + yield return new DropEmptyBallistic(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs new file mode 100644 index 0000000000..16e98819d9 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic +{ + public sealed class EquipBallisticExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new EquipBallistic(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs new file mode 100644 index 0000000000..cc770a057d --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic +{ + public sealed class PickUpAmmoExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + if (!owner.TryGetComponent(out AiControllerComponent controller)) + { + throw new InvalidOperationException(); + } + + foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(BallisticMagazineComponent), + controller.VisionRadius)) + { + yield return new PickUpAmmo(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/ChargeEquippedHitscanExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/ChargeEquippedHitscanExp.cs new file mode 100644 index 0000000000..b6d545d4a1 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/ChargeEquippedHitscanExp.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Power.Chargers; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan +{ + public sealed class ChargeEquippedHitscanExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + if (!owner.TryGetComponent(out AiControllerComponent controller)) + { + throw new InvalidOperationException(); + } + + foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent), + controller.VisionRadius)) + { + yield return new PutHitscanInCharger(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/DropEmptyHitscanExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/DropEmptyHitscanExp.cs new file mode 100644 index 0000000000..b293ae3b14 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/DropEmptyHitscanExp.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan +{ + public class DropEmptyHitscanExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.HasComponent()) + { + yield return new DropEmptyHitscan(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/EquipHitscanExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/EquipHitscanExp.cs new file mode 100644 index 0000000000..7f10f22ed0 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/EquipHitscanExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan +{ + public sealed class EquipHitscanExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new EquipHitscan(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/PickUpHitscanFromChargersExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/PickUpHitscanFromChargersExp.cs new file mode 100644 index 0000000000..7c272f8fd3 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Hitscan/PickUpHitscanFromChargersExp.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.Utils; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Power.Chargers; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan +{ + public sealed class PickUpHitscanFromChargersExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + if (!owner.TryGetComponent(out AiControllerComponent controller)) + { + throw new InvalidOperationException(); + } + + foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent), + controller.VisionRadius)) + { + var contained = entity.GetComponent().HeldItem; + + if (contained != null) + { + yield return new PickUpHitscanFromCharger(owner, entity, contained, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/PickUpRangedExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/PickUpRangedExp.cs new file mode 100644 index 0000000000..91795c6b86 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/PickUpRangedExp.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Combat.Nearby; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged +{ + public sealed class PickUpRangedExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatPrepBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + if (entity.HasComponent()) + { + yield return new PickUpHitscanWeapon(owner, entity, Bonus); + } + + if (entity.HasComponent()) + { + yield return new PickUpBallisticMagWeapon(owner, entity, Bonus); + } + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/RangedAttackNearbySpeciesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/RangedAttackNearbySpeciesExp.cs new file mode 100644 index 0000000000..146e584f22 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/RangedAttackNearbySpeciesExp.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic; +using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Mobs; + +namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged +{ + public sealed class RangedAttackNearbySpeciesExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.CombatBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new HitscanAttackEntity(owner, entity, Bonus); + yield return new BallisticAttackEntity(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs b/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs new file mode 100644 index 0000000000..619fc36139 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/ExpandableUtilityAction.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.WorldState; + +namespace Content.Server.AI.Utility.ExpandableActions +{ + /// + /// Expands into multiple separate utility actions for consideration, e.g. 5 nearby weapons 5 different actions + /// Ideally you would use the cached states for this + /// + public abstract class ExpandableUtilityAction : IAiUtility + { + public abstract float Bonus { get; } + + // e.g. you may have a "PickupFood" action for all nearby food sources if you have the "Hungry" BehaviorSet. + public abstract IEnumerable GetActions(Blackboard context); + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs new file mode 100644 index 0000000000..87f735b7f8 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyDrinkExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Nutrition.Drink; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Nutrition; + +namespace Content.Server.AI.Utility.ExpandableActions.Nutrition +{ + public sealed class PickUpNearbyDrinkExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NeedsBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new PickUpDrink(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs new file mode 100644 index 0000000000..01affba951 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/PickUpNearbyFoodExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Nutrition.Food; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Nutrition; + +namespace Content.Server.AI.Utility.ExpandableActions.Nutrition +{ + public sealed class PickUpNearbyFoodExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NeedsBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new PickUpFood(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInHandsExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInHandsExp.cs new file mode 100644 index 0000000000..830249bf89 --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseDrinkInHandsExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Nutrition.Drink; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Nutrition +{ + public sealed class UseDrinkInHandsExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NeedsBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new UseDrinkInInventory(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs new file mode 100644 index 0000000000..69e32c7f2f --- /dev/null +++ b/Content.Server/AI/Utility/ExpandableActions/Nutrition/UseFoodInInventoryExp.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.Actions.Nutrition.Food; +using Content.Server.AI.WorldState; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Inventory; + +namespace Content.Server.AI.Utility.ExpandableActions.Nutrition +{ + public sealed class UseFoodInInventoryExp : ExpandableUtilityAction + { + public override float Bonus => UtilityAction.NeedsBonus; + + public override IEnumerable GetActions(Blackboard context) + { + var owner = context.GetState().GetValue(); + + foreach (var entity in context.GetState().GetValue()) + { + yield return new UseFoodInInventory(owner, entity, Bonus); + } + } + } +} diff --git a/Content.Server/AI/Utility/UtilityAiHelpers.cs b/Content.Server/AI/Utility/UtilityAiHelpers.cs new file mode 100644 index 0000000000..a15cd5c9c2 --- /dev/null +++ b/Content.Server/AI/Utility/UtilityAiHelpers.cs @@ -0,0 +1,25 @@ +using Content.Server.AI.Utility.AiLogic; +using Content.Server.AI.WorldState; +using Content.Server.GameObjects.Components.Movement; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.Utility +{ + public static class UtilityAiHelpers + { + public static Blackboard GetBlackboard(IEntity entity) + { + if (!entity.TryGetComponent(out AiControllerComponent aiControllerComponent)) + { + return null; + } + + if (aiControllerComponent.Processor is UtilityAi utilityAi) + { + return utilityAi.Blackboard; + } + + return null; + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/Utils/Visibility.cs b/Content.Server/AI/Utils/Visibility.cs new file mode 100644 index 0000000000..1c66937cde --- /dev/null +++ b/Content.Server/AI/Utils/Visibility.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Movement; +using Content.Shared.Physics; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Physics; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.AI.Utils +{ + public static class Visibility + { + // Just do a simple range check, then chuck the ray out. If we get bigger than 1 tile mobs may need to adjust this + public static bool InLineOfSight(IEntity owner, IEntity target) + { + var range = 50.0f; + + if (owner.Transform.GridID != target.Transform.GridID) + { + return false; + } + + if (owner.TryGetComponent(out AiControllerComponent controller)) + { + var targetRange = (target.Transform.GridPosition.Position - owner.Transform.GridPosition.Position).Length; + if (targetRange > controller.VisionRadius) + { + return false; + } + + range = controller.VisionRadius; + } + + var angle = new Angle(target.Transform.GridPosition.Position - owner.Transform.GridPosition.Position); + var ray = new CollisionRay( + owner.Transform.GridPosition.Position, + angle.ToVec(), + (int)(CollisionGroup.Opaque | CollisionGroup.Impassable | CollisionGroup.MobImpassable)); + + var rayCastResults = IoCManager.Resolve().IntersectRay(owner.Transform.MapID, ray, range, owner).ToList(); + + return rayCastResults.Count > 0 && rayCastResults[0].HitEntity == target; + } + + // Should this be in robust or something? Fark it + public static IEnumerable GetNearestEntities(GridCoordinates grid, Type component, float range) + { + var inRange = GetEntitiesInRange(grid, component, range).ToList(); + var sortedInRange = inRange.OrderBy(o => (o.Transform.GridPosition.Position - grid.Position).Length); + + return sortedInRange; + } + + public static IEnumerable GetEntitiesInRange(GridCoordinates grid, Type component, float range) + { + var entityManager = IoCManager.Resolve(); + foreach (var entity in entityManager.GetEntities(new TypeEntityQuery(component))) + { + if (entity.Transform.GridPosition.GridID != grid.GridID) + { + continue; + } + + if ((entity.Transform.GridPosition.Position - grid.Position).Length <= range) + { + yield return entity; + } + } + } + } +} diff --git a/Content.Server/AI/WanderProcessor.cs b/Content.Server/AI/WanderProcessor.cs deleted file mode 100644 index f1430efaf5..0000000000 --- a/Content.Server/AI/WanderProcessor.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Server.GameObjects.Components.Movement; -using Content.Server.GameObjects.EntitySystems; -using Content.Server.Interfaces.Chat; -using Content.Shared.Physics; -using Robust.Server.AI; -using Robust.Shared.GameObjects.Components; -using Robust.Shared.Interfaces.Physics; -using Robust.Shared.Interfaces.Timing; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Utility; - -namespace Content.Server.AI -{ - /// - /// Designed to control a mob. The mob will wander around, then idle at a the destination for awhile. - /// - [AiLogicProcessor("Wander")] - class WanderProcessor : AiLogicProcessor - { -#pragma warning disable 649 - [Dependency] private readonly IPhysicsManager _physMan; - [Dependency] private readonly IGameTiming _timeMan; - [Dependency] private readonly IChatManager _chatMan; -#pragma warning restore 649 - - private static readonly TimeSpan IdleTimeSpan = TimeSpan.FromSeconds(1); - private static readonly TimeSpan WalkingTimeout = TimeSpan.FromSeconds(3); - private static readonly TimeSpan DisabledTimeout = TimeSpan.FromSeconds(10); - - private static List _normalAssistantConversation = new List - { - "stat me", - "roll it easy!", - "waaaaaagh!!!", - "red wonz go fasta", - "FOR TEH EMPRAH", - "lol2cat", - "dem dwarfs man, dem dwarfs", - "SPESS MAHREENS", - "hwee did eet fhor khayosss", - "lifelike texture ;_;", - "luv can bloooom", - "PACKETS!!!", - "SARAH HALE DID IT!!!", - "Don't tell Chase", - "not so tough now huh", - "WERE NOT BAY!!", - "IF YOU DONT LIKE THE CYBORGS OR SLIMES WHY DONT YU O JUST MAKE YORE OWN!", - "DONT TALK TO ME ABOUT BALANCE!!!!", - "YOU AR JUS LAZY AND DUMB JAMITORS AND SERVICE ROLLS", - "BLAME HOSHI!!!", - "ARRPEE IZ DED!!!", - "THERE ALL JUS MEATAFRIENDS!", - "SOTP MESING WITH THE ROUNS SHITMAN!!!", - "SKELINGTON IS 4 SHITERS!", - "MOMMSI R THE WURST SCUM!!", - "How do we engiener=", - "try to live freely and automatically good bye", - "why woud i take a pin pointner??", - "How do I set up the. SHow do I set u p the Singu. how I the scrungularity????", - }; - - private const float MaxWalkDistance = 3; // meters - private const float AdditionalIdleTime = 2; // 0 to this many more seconds - - private FsmState _CurrentState; - private TimeSpan _startStateTime; - private Vector2 _walkTargetPos; - - public override void Update(float frameTime) - { - if (SelfEntity == null) - return; - - ProcessState(); - } - - private void ProcessState() - { - switch (_CurrentState) - { - case FsmState.None: - _CurrentState = FsmState.Idle; - break; - case FsmState.Idle: - IdleState(); - break; - case FsmState.Walking: - WalkingState(); - break; - case FsmState.Disabled: - DisabledState(); - break; - } - } - - private void IdlePositiveEdge(ref uint rngState) - { - _startStateTime = _timeMan.CurTime + IdleTimeSpan + TimeSpan.FromSeconds(Random01(ref rngState) * AdditionalIdleTime); - _CurrentState = FsmState.Idle; - - EmitProfanity(ref rngState); - } - - private void IdleState() - { - if (!ActionBlockerSystem.CanMove(SelfEntity)) - { - DisabledPositiveEdge(); - return; - } - - if (_timeMan.CurTime < _startStateTime + IdleTimeSpan) - return; - - var entWorldPos = SelfEntity.Transform.WorldPosition; - - if (SelfEntity.TryGetComponent(out var bounds)) - entWorldPos = ((IPhysBody) bounds).WorldAABB.Center; - - var rngState = GenSeed(); - for (var i = 0; i < 3; i++) // you get 3 chances to find a place to walk - { - var dir = new Vector2(Random01(ref rngState) * 2 - 1, Random01(ref rngState) *2 -1).Normalized; - var ray = new CollisionRay(entWorldPos, dir, (int) CollisionGroup.Impassable); - var rayResults = _physMan.IntersectRay(SelfEntity.Transform.MapID, ray, MaxWalkDistance, SelfEntity).ToList(); - - if (rayResults.Count == 1) - { - var rayResult = rayResults[0]; - if (rayResult.Distance > 1) // hit an impassable object - { - // set the new position back from the wall a bit - _walkTargetPos = entWorldPos + dir * (rayResult.Distance - 0.5f); - WalkingPositiveEdge(); - return; - } - } - else // hit nothing (path clear) - { - _walkTargetPos = dir * MaxWalkDistance; - WalkingPositiveEdge(); - return; - } - } - - // can't find clear spot, do nothing, sleep longer - _startStateTime = _timeMan.CurTime; - } - - private void WalkingPositiveEdge() - { - _startStateTime = _timeMan.CurTime; - _CurrentState = FsmState.Walking; - } - - private void WalkingState() - { - var rngState = GenSeed(); - if (_timeMan.CurTime > _startStateTime + WalkingTimeout) // walked too long, go idle - { - IdlePositiveEdge(ref rngState); - return; - } - - var targetDiff = _walkTargetPos - SelfEntity.Transform.WorldPosition; - - if (targetDiff.LengthSquared < 0.1) // close enough - { - // stop walking - if (SelfEntity.TryGetComponent(out var mover)) - { - mover.VelocityDir = Vector2.Zero; - } - - IdlePositiveEdge(ref rngState); - return; - } - - // continue walking - if (SelfEntity.TryGetComponent(out var moverTwo)) - { - moverTwo.VelocityDir = targetDiff.Normalized; - } - } - - private void DisabledPositiveEdge() - { - _startStateTime = _timeMan.CurTime; - _CurrentState = FsmState.Disabled; - } - - private void DisabledState() - { - if(_timeMan.CurTime < _startStateTime + DisabledTimeout) - return; - - if (ActionBlockerSystem.CanMove(SelfEntity)) - { - var rngState = GenSeed(); - IdlePositiveEdge(ref rngState); - } - else - DisabledPositiveEdge(); - } - - private void EmitProfanity(ref uint rngState) - { - if(Random01(ref rngState) < 0.5f) - return; - - var pick = (int) MathF.Round(Random01(ref rngState) * (_normalAssistantConversation.Count - 1)); - _chatMan.EntitySay(SelfEntity, _normalAssistantConversation[pick]); - } - - private uint GenSeed() - { - return RotateRight((uint)_timeMan.CurTick.GetHashCode(), 11) ^ (uint)SelfEntity.Uid.GetHashCode(); - } - - private uint RotateRight(uint n, int s) - { - return (n << (32 - s)) | (n >> s); - } - - private float Random01(ref uint state) - { - DebugTools.Assert(state != 0); - - //xorshift32 - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - return state / (float)uint.MaxValue; - } - - private enum FsmState - { - None, - Idle, - Walking, - Disabled - } - } -} diff --git a/Content.Server/AI/WorldState/Blackboard.cs b/Content.Server/AI/WorldState/Blackboard.cs new file mode 100644 index 0000000000..b40daa2baa --- /dev/null +++ b/Content.Server/AI/WorldState/Blackboard.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Reflection; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Content.Server.AI.WorldState +{ + /// + /// The blackboard functions as an AI's repository of knowledge in a common format. + /// + public sealed class Blackboard + { + // Some stuff like "My Health" is easy to represent as components but abstract stuff like "How much food is nearby" + // is harder. This also allows data to be cached if it's being hit frequently. + + // This also stops you from re-writing the same boilerplate everywhere of stuff like "Do I have OuterClothing on?" + + private readonly Dictionary _states = new Dictionary(); + private readonly List _planningStates = new List(); + + public Blackboard(IEntity owner) + { + Setup(owner); + } + + private void Setup(IEntity owner) + { + var typeFactory = IoCManager.Resolve(); + var blackboardManager = IoCManager.Resolve(); + + foreach (var state in blackboardManager.AiStates) + { + var newState = (IAiState) typeFactory.CreateInstance(state); + newState.Setup(owner); + _states.Add(newState.GetType(), newState); + + switch (newState) + { + case IPlanningState planningState: + _planningStates.Add(planningState); + break; + } + } + } + + /// + /// All planning states will have their values reset + /// + public void ResetPlanning() + { + foreach (var state in _planningStates) + { + state.Reset(); + } + } + + /// + /// Get the AI state class + /// + /// + /// + /// + public T GetState() where T : IAiState + { + return (T) _states[typeof(T)]; + } + } +} diff --git a/Content.Server/AI/WorldState/BlackboardManager.cs b/Content.Server/AI/WorldState/BlackboardManager.cs new file mode 100644 index 0000000000..f0ef10f5d2 --- /dev/null +++ b/Content.Server/AI/WorldState/BlackboardManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Interfaces.Reflection; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Content.Server.AI.WorldState +{ + // This will also handle the global blackboard at some point + /// + /// Manager the AI blackboard states + /// + public sealed class BlackboardManager + { + // Cache the known types + public IReadOnlyCollection AiStates => _aiStates; + private List _aiStates = new List(); + + public void Initialize() + { + var reflectionManager = IoCManager.Resolve(); + + foreach (var state in reflectionManager.GetAllChildren(typeof(IAiState))) + { + _aiStates.Add(state); + } + + DebugTools.AssertNotNull(_aiStates); + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/WorldState/StateData.cs b/Content.Server/AI/WorldState/StateData.cs new file mode 100644 index 0000000000..cc55f767e7 --- /dev/null +++ b/Content.Server/AI/WorldState/StateData.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState +{ + /// + /// Basic StateDate, no frills + /// + public interface IAiState + { + void Setup(IEntity owner); + } + + public interface IPlanningState + { + void Reset(); + } + + public interface ICachedState + { + void CheckCache(); + } + + /// + /// The default class for state values. Also see CachedStateData and PlanningStateData + /// + /// + public abstract class StateData : IAiState + { + public abstract string Name { get; } + protected IEntity Owner { get; private set; } + + public void Setup(IEntity owner) + { + Owner = owner; + } + + public abstract T GetValue(); + } + + /// + /// For when we want to set StateData but not reset it when re-planning actions + /// Useful for group blackboard sharing or to avoid repeating the same action (e.g. bark phrases). + /// + /// + public abstract class StoredStateData : IAiState + { + // Probably not the best class name but couldn't think of anything better + public abstract string Name { get; } + private IEntity Owner { get; set; } + + private T _value; + + public void Setup(IEntity owner) + { + Owner = owner; + } + + public virtual void SetValue(T value) + { + _value = value; + } + + public T GetValue() + { + return _value; + } + } + + /// + /// This is state data that is transient and forgotten every time we re-plan + /// e.g. "Current Target" gets updated for every action we consider + /// + /// + public abstract class PlanningStateData : IAiState, IPlanningState + { + public abstract string Name { get; } + protected IEntity Owner { get; private set; } + protected T Value; + + public void Setup(IEntity owner) + { + Owner = owner; + } + + public abstract void Reset(); + + public T GetValue() + { + return Value; + } + + public virtual void SetValue(T value) + { + Value = value; + } + } + + /// + /// This is state data that is cached for n seconds before being discarded. + /// Mostly useful to get nearby components and store the value. + /// + /// + public abstract class CachedStateData : IAiState, ICachedState + { + public abstract string Name { get; } + protected IEntity Owner { get; private set; } + private bool _cached; + protected T Value; + private DateTime _lastCache = DateTime.Now; + /// + /// How long something stays in the cache before new values are retrieved + /// + protected float CacheTime { get; set; } = 2.0f; + + public void Setup(IEntity owner) + { + Owner = owner; + } + + public void CheckCache() + { + if (!_cached || (DateTime.Now - _lastCache).TotalSeconds >= CacheTime) + { + _cached = false; + return; + } + + _cached = true; + } + + /// + /// When the cache is stale we'll retrieve the actual value and store it again + /// + protected abstract T GetTrueValue(); + + public T GetValue() + { + CheckCache(); + if (!_cached) + { + Value = GetTrueValue(); + _cached = true; + _lastCache = DateTime.Now; + } + + return Value; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Clothing/EquippedClothingState.cs b/Content.Server/AI/WorldState/States/Clothing/EquippedClothingState.cs new file mode 100644 index 0000000000..2e91376bab --- /dev/null +++ b/Content.Server/AI/WorldState/States/Clothing/EquippedClothingState.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Clothing +{ + [UsedImplicitly] + public sealed class EquippedClothingState : StateData> + { + public override string Name => "EquippedClothing"; + + public override Dictionary GetValue() + { + var result = new Dictionary(); + + if (!Owner.TryGetComponent(out InventoryComponent inventoryComponent)) + { + return result; + } + + foreach (var slot in EquipmentSlotDefines.AllSlots) + { + if (!inventoryComponent.HasSlot(slot)) continue; + var slotItem = inventoryComponent.GetSlotItem(slot); + + if (slotItem != null) + { + result.Add(slot, slotItem.Owner); + } + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs new file mode 100644 index 0000000000..83a54abee1 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Clothing/NearbyClothingState.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components; +using Content.Server.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Clothing +{ + [UsedImplicitly] + public sealed class NearbyClothingState : CachedStateData> + { + public override string Name => "NearbyClothing"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(ClothingComponent), controller.VisionRadius)) + { + if (ContainerHelpers.TryGetContainer(entity, out var container)) + { + if (!container.Owner.HasComponent()) + { + continue; + } + } + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserChargersState.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserChargersState.cs new file mode 100644 index 0000000000..f8e956e92d --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserChargersState.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Power.Chargers; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Combat.Nearby +{ + [UsedImplicitly] + public sealed class NearbyLaserChargersState : StateData> + { + public override string Name => "NearbyLaserChargers"; + + public override List GetValue() + { + var nearby = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return nearby; + } + + foreach (var result in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent), controller.VisionRadius)) + { + nearby.Add(result); + } + + return nearby; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserWeapons.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserWeapons.cs new file mode 100644 index 0000000000..2d3362f109 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyLaserWeapons.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Combat.Nearby +{ + [UsedImplicitly] + public sealed class NearbyLaserWeapons : StateData> + { + public override string Name => "NearbyLaserWeapons"; + + public override List GetValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(HitscanWeaponComponent), controller.VisionRadius)) + { + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs new file mode 100644 index 0000000000..c6d88f5530 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyMeleeWeapons.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Weapon.Melee; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Combat.Nearby +{ + [UsedImplicitly] + public sealed class NearbyMeleeWeapons : CachedStateData> + { + public override string Name => "NearbyMeleeWeapons"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(MeleeWeaponComponent), controller.VisionRadius)) + { + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyRangedWeapons.cs b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyRangedWeapons.cs new file mode 100644 index 0000000000..7d2307b3e2 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Nearby/NearbyRangedWeapons.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Weapon.Ranged; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Combat.Nearby +{ + [UsedImplicitly] + public sealed class NearbyRangedWeapons : CachedStateData> + { + public override string Name => "NearbyRangedWeapons"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(RangedWeaponComponent), controller.VisionRadius)) + { + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Ranged/Accuracy.cs b/Content.Server/AI/WorldState/States/Combat/Ranged/Accuracy.cs new file mode 100644 index 0000000000..6b44cbcd79 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Ranged/Accuracy.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Combat.Ranged +{ + [UsedImplicitly] + public sealed class Accuracy : StateData + { + public override string Name => "Accuracy"; + + public override float GetValue() + { + // TODO: Maybe just make it a SetValue (maybe make a third type besides sensor / daemon called settablestate) + return 1.0f; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Ranged/BurstCooldown.cs b/Content.Server/AI/WorldState/States/Combat/Ranged/BurstCooldown.cs new file mode 100644 index 0000000000..3a870a83cb --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Ranged/BurstCooldown.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Combat.Ranged +{ + /// + /// How long to wait between bursts + /// + [UsedImplicitly] + public sealed class BurstCooldown : PlanningStateData + { + public override string Name => "BurstCooldown"; + public override void Reset() + { + Value = 0.0f; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Ranged/EquippedRangedWeaponAmmo.cs b/Content.Server/AI/WorldState/States/Combat/Ranged/EquippedRangedWeaponAmmo.cs new file mode 100644 index 0000000000..024d55e2e6 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Ranged/EquippedRangedWeaponAmmo.cs @@ -0,0 +1,39 @@ +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan; +using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile; +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Combat.Ranged +{ + /// + /// Gets the discrete ammo count + /// + [UsedImplicitly] + public sealed class EquippedRangedWeaponAmmo : StateData + { + public override string Name => "EquippedRangedWeaponAmmo"; + + public override int? GetValue() + { + if (!Owner.TryGetComponent(out HandsComponent handsComponent)) + { + return null; + } + + var equippedItem = handsComponent.GetActiveHand?.Owner; + if (equippedItem == null) return null; + + if (equippedItem.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent)) + { + return (int) hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.BaseFireCost; + } + + if (equippedItem.TryGetComponent(out BallisticMagazineWeaponComponent ballisticComponent)) + { + return ballisticComponent.MagazineSlot.ContainedEntities.Count; + } + + return null; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/Ranged/MaxBurstCount.cs b/Content.Server/AI/WorldState/States/Combat/Ranged/MaxBurstCount.cs new file mode 100644 index 0000000000..f7d10301dd --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/Ranged/MaxBurstCount.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Combat.Ranged +{ + /// + /// How many shots to take before cooling down + /// + [UsedImplicitly] + public sealed class MaxBurstCount : PlanningStateData + { + public override string Name => "BurstCount"; + public override void Reset() + { + Value = 0; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Combat/WeaponEntityState.cs b/Content.Server/AI/WorldState/States/Combat/WeaponEntityState.cs new file mode 100644 index 0000000000..c148d379fa --- /dev/null +++ b/Content.Server/AI/WorldState/States/Combat/WeaponEntityState.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Combat +{ + [UsedImplicitly] + public sealed class WeaponEntityState : PlanningStateData + { + // Similar to TargetEntity + public override string Name => "WeaponEntity"; + public override void Reset() + { + Value = null; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Hands/AnyFreeHandState.cs b/Content.Server/AI/WorldState/States/Hands/AnyFreeHandState.cs new file mode 100644 index 0000000000..92a7d9735b --- /dev/null +++ b/Content.Server/AI/WorldState/States/Hands/AnyFreeHandState.cs @@ -0,0 +1,28 @@ +using Content.Server.GameObjects; +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Hands +{ + [UsedImplicitly] + public class AnyFreeHandState : StateData + { + public override string Name => "AnyFreeHand"; + public override bool GetValue() + { + if (!Owner.TryGetComponent(out HandsComponent handsComponent)) + { + return false; + } + + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + if (handsComponent.GetHand(hand) == null) + { + return true; + } + } + + return false; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Hands/FreeHands.cs b/Content.Server/AI/WorldState/States/Hands/FreeHands.cs new file mode 100644 index 0000000000..7a31d8b00b --- /dev/null +++ b/Content.Server/AI/WorldState/States/Hands/FreeHands.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Hands +{ + [UsedImplicitly] + public sealed class FreeHands : StateData> + { + public override string Name => "FreeHands"; + + public override List GetValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out HandsComponent handsComponent)) + { + return result; + } + + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + if (handsComponent.GetHand(hand) == null) + { + result.Add(hand); + } + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Hands/HandItemsState.cs b/Content.Server/AI/WorldState/States/Hands/HandItemsState.cs new file mode 100644 index 0000000000..3e4f227454 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Hands/HandItemsState.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Hands +{ + [UsedImplicitly] + public class HandItemsState : StateData> + { + public override string Name => "HandItems"; + public override List GetValue() + { + var result = new List(); + if (!Owner.TryGetComponent(out HandsComponent handsComponent)) + { + return result; + } + + foreach (var hand in handsComponent.ActivePriorityEnumerable()) + { + var item = handsComponent.GetHand(hand); + + if (item != null) + { + result.Add(item.Owner); + } + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Inventory/EquippedEntityState.cs b/Content.Server/AI/WorldState/States/Inventory/EquippedEntityState.cs new file mode 100644 index 0000000000..190df49bc1 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Inventory/EquippedEntityState.cs @@ -0,0 +1,25 @@ +using Content.Server.GameObjects; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Inventory +{ + /// + /// AKA what's in active hand + /// + [UsedImplicitly] + public sealed class EquippedEntityState : StateData + { + public override string Name => "EquippedEntity"; + + public override IEntity GetValue() + { + if (!Owner.TryGetComponent(out HandsComponent handsComponent)) + { + return null; + } + + return handsComponent.GetActiveHand?.Owner; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs b/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs new file mode 100644 index 0000000000..d139eac9cf --- /dev/null +++ b/Content.Server/AI/WorldState/States/Inventory/InventoryState.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Inventory +{ + [UsedImplicitly] + public sealed class InventoryState : StateData> + { + public override string Name => "Inventory"; + + public override List GetValue() + { + var inventory = new List(); + + if (Owner.TryGetComponent(out HandsComponent handsComponent)) + { + foreach (var item in handsComponent.GetAllHeldItems()) + { + inventory.Add(item.Owner); + } + } + + // TODO: InventoryComponent (Pockets were throwing) + + return inventory; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Inventory/LastOpenedStorageState.cs b/Content.Server/AI/WorldState/States/Inventory/LastOpenedStorageState.cs new file mode 100644 index 0000000000..636cb8ea63 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Inventory/LastOpenedStorageState.cs @@ -0,0 +1,26 @@ +using Content.Server.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; +using Logger = Robust.Shared.Log.Logger; + +namespace Content.Server.AI.WorldState.States.Inventory +{ + /// + /// If we open a storage locker than it will be stored here + /// Useful if we want to close it after + /// + public sealed class LastOpenedStorageState : StoredStateData + { + // TODO: IF we chain lockers need to handle it. + // Fine for now I guess + public override string Name => "LastOpenedStorage"; + + public override void SetValue(IEntity value) + { + base.SetValue(value); + if (value != null && !value.HasComponent()) + { + Logger.Warning("Set LastOpenedStorageState for an entity that doesn't have a storage component"); + } + } + } +} \ No newline at end of file diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs new file mode 100644 index 0000000000..802ef04258 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Mobs/NearbyPlayersState.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.AI.WorldState.States.Mobs +{ + [UsedImplicitly] + public sealed class NearbyPlayersState : CachedStateData> + { + public override string Name => "NearbyPlayers"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + var playerManager = IoCManager.Resolve(); + var nearbyPlayers = playerManager.GetPlayersInRange(Owner.Transform.GridPosition, (int) controller.VisionRadius); + + foreach (var player in nearbyPlayers) + { + if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent()) + { + result.Add(player.AttachedEntity); + } + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs b/Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs new file mode 100644 index 0000000000..68ac8fd355 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Mobs/NearbySpeciesState.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Mobs +{ + [UsedImplicitly] + public sealed class NearbySpeciesState : CachedStateData> + { + public override string Name => "NearbySpecies"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility.GetEntitiesInRange(Owner.Transform.GridPosition, typeof(SpeciesComponent), controller.VisionRadius)) + { + if (entity == Owner) continue; + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Movement/MoveTargetState.cs b/Content.Server/AI/WorldState/States/Movement/MoveTargetState.cs new file mode 100644 index 0000000000..44d520832c --- /dev/null +++ b/Content.Server/AI/WorldState/States/Movement/MoveTargetState.cs @@ -0,0 +1,15 @@ +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Movement +{ + [UsedImplicitly] + public sealed class MoveTargetState : PlanningStateData + { + public override string Name => "MoveTarget"; + public override void Reset() + { + Value = null; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Nutrition/HungryState.cs b/Content.Server/AI/WorldState/States/Nutrition/HungryState.cs new file mode 100644 index 0000000000..2143a827b3 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Nutrition/HungryState.cs @@ -0,0 +1,39 @@ +using System; +using Content.Server.GameObjects.Components.Nutrition; +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Nutrition +{ + [UsedImplicitly] + public sealed class HungryState : StateData + { + public override string Name => "Hungry"; + + public override bool GetValue() + { + if (!Owner.TryGetComponent(out HungerComponent hungerComponent)) + { + return false; + } + + switch (hungerComponent.CurrentHungerThreshold) + { + case HungerThreshold.Overfed: + return false; + case HungerThreshold.Okay: + return false; + case HungerThreshold.Peckish: + return true; + case HungerThreshold.Starving: + return true; + case HungerThreshold.Dead: + return true; + default: + throw new ArgumentOutOfRangeException( + nameof(hungerComponent.CurrentHungerThreshold), + hungerComponent.CurrentHungerThreshold, + null); + } + } + } +} diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs new file mode 100644 index 0000000000..bfee177d69 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyDrinkState.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Nutrition; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Nutrition +{ + [UsedImplicitly] + public sealed class NearbyDrinkState: CachedStateData> + { + public override string Name => "NearbyDrink"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(DrinkComponent), controller.VisionRadius)) + { + if (ContainerHelpers.TryGetContainer(entity, out var container)) + { + if (!container.Owner.HasComponent()) + { + continue; + } + } + result.Add(entity); + } + + return result; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs new file mode 100644 index 0000000000..e33bca5a0c --- /dev/null +++ b/Content.Server/AI/WorldState/States/Nutrition/NearbyFoodState.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Content.Server.AI.Utils; +using Content.Server.GameObjects.Components; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.Components.Nutrition; +using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States.Nutrition +{ + [UsedImplicitly] + public sealed class NearbyFoodState : CachedStateData> + { + public override string Name => "NearbyFood"; + + protected override List GetTrueValue() + { + var result = new List(); + + if (!Owner.TryGetComponent(out AiControllerComponent controller)) + { + return result; + } + + foreach (var entity in Visibility + .GetNearestEntities(Owner.Transform.GridPosition, typeof(FoodComponent), controller.VisionRadius)) + { + if (ContainerHelpers.TryGetContainer(entity, out var container)) + { + if (!container.Owner.HasComponent()) + { + continue; + } + } + result.Add(entity); + } + + return result; + + } + } +} diff --git a/Content.Server/AI/WorldState/States/Nutrition/ThirstyState.cs b/Content.Server/AI/WorldState/States/Nutrition/ThirstyState.cs new file mode 100644 index 0000000000..02ea016c56 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Nutrition/ThirstyState.cs @@ -0,0 +1,40 @@ +using System; +using Content.Server.GameObjects.Components.Nutrition; +using JetBrains.Annotations; +using ThirstComponent = Content.Server.GameObjects.Components.Nutrition.ThirstComponent; + +namespace Content.Server.AI.WorldState.States.Nutrition +{ + [UsedImplicitly] + public class ThirstyState : StateData + { + public override string Name => "Thirsty"; + + public override bool GetValue() + { + if (!Owner.TryGetComponent(out ThirstComponent thirstComponent)) + { + return false; + } + + switch (thirstComponent.CurrentThirstThreshold) + { + case ThirstThreshold.OverHydrated: + return false; + case ThirstThreshold.Okay: + return false; + case ThirstThreshold.Thirsty: + return true; + case ThirstThreshold.Parched: + return true; + case ThirstThreshold.Dead: + return true; + default: + throw new ArgumentOutOfRangeException( + nameof(thirstComponent.CurrentThirstThreshold), + thirstComponent.CurrentThirstThreshold, + null); + } + } + } +} diff --git a/Content.Server/AI/WorldState/States/SelfState.cs b/Content.Server/AI/WorldState/States/SelfState.cs new file mode 100644 index 0000000000..9f77743565 --- /dev/null +++ b/Content.Server/AI/WorldState/States/SelfState.cs @@ -0,0 +1,16 @@ +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States +{ + [UsedImplicitly] + public sealed class SelfState : StateData + { + public override string Name => "Self"; + + public override IEntity GetValue() + { + return Owner; + } + } +} diff --git a/Content.Server/AI/WorldState/States/TargetEntityState.cs b/Content.Server/AI/WorldState/States/TargetEntityState.cs new file mode 100644 index 0000000000..33ea22ff75 --- /dev/null +++ b/Content.Server/AI/WorldState/States/TargetEntityState.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.AI.WorldState.States +{ + /// + /// Could be target item to equip, target to attack, etc. + /// + [UsedImplicitly] + public sealed class TargetEntityState : PlanningStateData + { + public override string Name => "TargetEntity"; + + public override void Reset() + { + Value = null; + } + } +} diff --git a/Content.Server/AI/WorldState/States/Utility/LastUtilityScoreState.cs b/Content.Server/AI/WorldState/States/Utility/LastUtilityScoreState.cs new file mode 100644 index 0000000000..68dad19ff0 --- /dev/null +++ b/Content.Server/AI/WorldState/States/Utility/LastUtilityScoreState.cs @@ -0,0 +1,24 @@ +using JetBrains.Annotations; + +namespace Content.Server.AI.WorldState.States.Utility +{ + /// + /// Used for the utility AI; sets the threshold score we need to beat + /// + [UsedImplicitly] + public class LastUtilityScoreState : StateData + { + public override string Name => "LastBonus"; + private float _value = 0.0f; + + public void SetValue(float value) + { + _value = value; + } + + public override float GetValue() + { + return _value; + } + } +} diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 5751fc51a0..b21ddf4f6f 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -1,4 +1,5 @@ using Content.Server.Interfaces; +using Content.Server.AI.WorldState; using Content.Server.Interfaces.Chat; using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.PDA; @@ -85,6 +86,7 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().FinishInit(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); } diff --git a/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs b/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs index d341520159..b61f7d6608 100644 --- a/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs +++ b/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs @@ -55,6 +55,21 @@ namespace Content.Server.GameObjects }); } + public bool IsDead() + { + var currentDamage = _currentDamage[DamageType.Total]; + foreach (var threshold in Thresholds[DamageType.Total]) + { + if (threshold.Value <= currentDamage) + { + if (threshold.ThresholdType != ThresholdType.Death) continue; + return true; + } + } + + return false; + } + /// public override void Initialize() { diff --git a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs index 4b4968d61f..cc48a4604c 100644 --- a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs @@ -118,6 +118,7 @@ namespace Content.Server.GameObjects _entitySystemManager.GetEntitySystem().EquippedInteraction(Owner, item.Owner, slot); Dirty(); + return true; } @@ -196,6 +197,7 @@ namespace Content.Server.GameObjects _entitySystemManager.GetEntitySystem().UnequippedInteraction(Owner, item.Owner, slot); Dirty(); + return true; } @@ -270,7 +272,7 @@ namespace Content.Server.GameObjects /// /// The underlying Container System just notified us that an entity was removed from it. - /// We need to make sure we process that removed entity as being unequpped from the slot. + /// We need to make sure we process that removed entity as being unequipped from the slot. /// private void ForceUnequip(IContainer container, IEntity entity) { @@ -281,7 +283,9 @@ namespace Content.Server.GameObjects return; if (entity.TryGetComponent(out ItemComponent itemComp)) + { itemComp.RemovedFromSlot(); + } Dirty(); } diff --git a/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs b/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs index 38410a58cb..7a3ac2e479 100644 --- a/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs @@ -104,7 +104,7 @@ namespace Content.Server.GameObjects /// /// Enumerates over the hand keys, returning the active hand first. /// - private IEnumerable ActivePriorityEnumerable() + public IEnumerable ActivePriorityEnumerable() { yield return ActiveIndex; foreach (var hand in hands.Keys) @@ -557,6 +557,7 @@ namespace Content.Server.GameObjects } Dirty(); + if (!message.Entity.TryGetComponent(out PhysicsComponent physics)) { return; diff --git a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs index aa45e6a4b3..884e03479d 100644 --- a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs @@ -1,4 +1,6 @@ -using Content.Server.Interfaces.GameObjects.Components.Movement; +using Content.Server.AI.Utility; +using Content.Server.AI.Utility.AiLogic; +using Content.Server.Interfaces.GameObjects.Components.Movement; using Robust.Server.AI; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; @@ -56,6 +58,12 @@ namespace Content.Server.GameObjects.Components.Movement serializer.DataField(ref _visionRadius, "vision", 8.0f); } + protected override void Shutdown() + { + base.Shutdown(); + Processor.Shutdown(); + } + /// /// Movement speed (m/s) that the entity walks, after modifiers /// @@ -103,7 +111,7 @@ namespace Content.Server.GameObjects.Components.Movement /// Is the entity Sprinting (running)? /// [ViewVariables] - public bool Sprinting { get; set; } + public bool Sprinting { get; set; } = true; /// /// Calculated linear velocity direction of the entity. diff --git a/Content.Server/GameObjects/Components/Power/Chargers/BaseCharger.cs b/Content.Server/GameObjects/Components/Power/Chargers/BaseCharger.cs index 17e144bad8..ffd26a8cb6 100644 --- a/Content.Server/GameObjects/Components/Power/Chargers/BaseCharger.cs +++ b/Content.Server/GameObjects/Components/Power/Chargers/BaseCharger.cs @@ -13,7 +13,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers public abstract class BaseCharger : Component { - protected IEntity _heldItem; + public IEntity HeldItem { get; protected set; } protected ContainerSlot _container; protected PowerDeviceComponent _powerDevice; public CellChargerStatus Status => _status; @@ -87,7 +87,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers return; } - _container.Remove(_heldItem); + _container.Remove(HeldItem); + HeldItem = null; UpdateStatus(); } diff --git a/Content.Server/GameObjects/Components/Power/Chargers/PowerCellChargerComponent.cs b/Content.Server/GameObjects/Components/Power/Chargers/PowerCellChargerComponent.cs index 57ae2f41f7..198e9ed5ab 100644 --- a/Content.Server/GameObjects/Components/Power/Chargers/PowerCellChargerComponent.cs +++ b/Content.Server/GameObjects/Components/Power/Chargers/PowerCellChargerComponent.cs @@ -123,8 +123,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers return false; } - _heldItem = entity; - if (!_container.Insert(_heldItem)) + HeldItem = entity; + if (!_container.Insert(HeldItem)) { return false; } diff --git a/Content.Server/GameObjects/Components/Power/Chargers/WeaponCapacitorChargerComponent.cs b/Content.Server/GameObjects/Components/Power/Chargers/WeaponCapacitorChargerComponent.cs index e51b75b20b..d90105e855 100644 --- a/Content.Server/GameObjects/Components/Power/Chargers/WeaponCapacitorChargerComponent.cs +++ b/Content.Server/GameObjects/Components/Power/Chargers/WeaponCapacitorChargerComponent.cs @@ -118,8 +118,9 @@ namespace Content.Server.GameObjects.Components.Power.Chargers return false; } - _heldItem = entity; - if (!_container.Insert(_heldItem)) + HeldItem = entity; + + if (!_container.Insert(HeldItem)) { return false; } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs index 5dce4c1738..78fae35837 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -38,7 +38,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee private float _arcWidth; private string _arc; private string _hitSound; - private float _cooldownTime; + public float CooldownTime => _cooldownTime; + private float _cooldownTime = 1f; [ViewVariables(VVAccess.ReadWrite)] public string Arc diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs index 33e140e7a6..3cb73b7272 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs @@ -38,6 +38,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile [ViewVariables] private string _defaultMagazine; + public ContainerSlot MagazineSlot => _magazineSlot; [ViewVariables] private ContainerSlot _magazineSlot; private List _magazineTypes; diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs index 076d217196..df94536d9e 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs @@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile FireAtCoord(user, clickLocation, ammo.ProjectileID, total_stdev, ammo.ProjectilesFired, final_evenspread, final_velocity); } - protected IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity; + public IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity; /// /// Loads the next ammo casing into the chamber. diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs index 44207ba68c..1b83393469 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs @@ -1,6 +1,7 @@ using System; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.EntitySystems; +using Content.Server.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Weapons.Ranged; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; @@ -60,6 +61,18 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged } } + // Probably shouldn't be a separate method but don't want anything except NPCs calling this, + // and currently ranged combat is handled via player only messages + public void AiFire(IEntity entity, GridCoordinates coordinates) + { + if (!entity.HasComponent()) + { + throw new InvalidOperationException("Only AIs should call AiFire"); + } + + _tryFire(entity, coordinates); + } + private void _tryFire(IEntity user, GridCoordinates coordinates) { if (!user.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand?.Owner != Owner) @@ -75,11 +88,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged { return; } - - // Firing delays are quite complicated. - // Sometimes the client's fire messages come in just too early. - // Generally this is a frame or two of being early. - // In that case we try them a few times the next frames to avoid having to drop them. + var curTime = IoCManager.Resolve().CurTime; var span = curTime - _lastFireTime; if (span.TotalSeconds < 1 / FireRate) diff --git a/Content.Server/GameObjects/EntitySystems/AiSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs similarity index 83% rename from Content.Server/GameObjects/EntitySystems/AiSystem.cs rename to Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs index 2aadeb7e46..b02225e97d 100644 --- a/Content.Server/GameObjects/EntitySystems/AiSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using Content.Server.AI.Utility.AiLogic; using Content.Server.GameObjects.Components.Movement; using Content.Server.Interfaces.GameObjects.Components.Movement; +using JetBrains.Annotations; using Robust.Server.AI; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -13,8 +15,9 @@ using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; using Robust.Shared.Utility; -namespace Content.Server.GameObjects.EntitySystems +namespace Content.Server.GameObjects.EntitySystems.AI { + [UsedImplicitly] internal class AiSystem : EntitySystem { #pragma warning disable 649 @@ -47,6 +50,7 @@ namespace Content.Server.GameObjects.EntitySystems /// public override void Update(float frameTime) { + var entities = EntityManager.GetEntities(EntityQuery); foreach (var entity in entities) { @@ -56,12 +60,7 @@ namespace Content.Server.GameObjects.EntitySystems } var aiComp = entity.GetComponent(); - if (aiComp.Processor == null) - { - aiComp.Processor = CreateProcessor(aiComp.LogicName); - aiComp.Processor.SelfEntity = entity; - aiComp.Processor.VisionRadius = aiComp.VisionRadius; - } + ProcessorInitialize(aiComp); var processor = aiComp.Processor; @@ -69,11 +68,24 @@ namespace Content.Server.GameObjects.EntitySystems } } - private AiLogicProcessor CreateProcessor(string name) + /// + /// Will start up the controller's processor if not already done so + /// + /// + public void ProcessorInitialize(AiControllerComponent controller) + { + if (controller.Processor != null) return; + controller.Processor = CreateProcessor(controller.LogicName); + controller.Processor.SelfEntity = controller.Owner; + controller.Processor.VisionRadius = controller.VisionRadius; + controller.Processor.Setup(); + } + + private UtilityAi CreateProcessor(string name) { if (_processorTypes.TryGetValue(name, out var type)) { - return (AiLogicProcessor)_typeFactory.CreateInstance(type); + return (UtilityAi)_typeFactory.CreateInstance(type); } // processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name diff --git a/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequest.cs b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequest.cs new file mode 100644 index 0000000000..cb9b641fea --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequest.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.WorldState; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer +{ + public class AiActionRequest + { + public EntityUid EntityUid { get; } + public Blackboard Context { get; } + public IEnumerable Actions { get; } + + public AiActionRequest(EntityUid uid, Blackboard context, IEnumerable actions) + { + EntityUid = uid; + Context = context; + Actions = actions; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequestJob.cs b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequestJob.cs new file mode 100644 index 0000000000..bd4041bf76 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionRequestJob.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.AI.Utility.Actions; +using Content.Server.AI.Utility.ExpandableActions; +using Content.Server.AI.WorldState.States; +using Content.Server.AI.WorldState.States.Utility; +using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Content.Shared.AI; + +namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer +{ + public class AiActionRequestJob : Job + { +#if DEBUG + public static event Action FoundAction; +#endif + private readonly AiActionRequest _request; + + public AiActionRequestJob( + double maxTime, + AiActionRequest request, + CancellationToken cancellationToken = default) : base(maxTime, cancellationToken) + { + _request = request; + } + + protected override async Task Process() + { + if (_request.Context == null) + { + return null; + } + + var entity = _request.Context.GetState().GetValue(); + + if (entity == null || !entity.HasComponent()) + { + return null; + } + + if (_request.Actions == null || _request.Context == null) + { + return null; + } + + var consideredTaskCount = 0; + // Actions are pre-sorted + var actions = new Stack(_request.Actions); + + // So essentially we go through and once we have a valid score that score becomes the cutoff; + // once the bonus of new tasks is below the cutoff we can stop evaluating. + + // Use last action as the basis for the cutoff + var cutoff = _request.Context.GetState().GetValue(); + UtilityAction foundAction = null; + + // To see what I was trying to do watch these 2 videos about Infinite Axis Utility System (IAUS): + // Architecture Tricks: Managing Behaviors in Time, Space, and Depth + // Building a Better Centaur + + // We'll want to cap the considered entities at some point, e.g. if 500 guns are in a stack cap it at 256 or whatever + while (actions.Count > 0) + { + if (consideredTaskCount > 0 && consideredTaskCount % 5 == 0) + { + await SuspendIfOutOfTime(); + + // If this happens then that means something changed when we resumed so ABORT + if (actions.Count == 0 || _request.Context == null) + { + return null; + } + } + + var action = actions.Pop(); + switch (action) + { + case ExpandableUtilityAction expandableUtilityAction: + foreach (var expanded in expandableUtilityAction.GetActions(_request.Context)) + { + actions.Push(expanded); + } + break; + case UtilityAction utilityAction: + consideredTaskCount++; + var bonus = (float) utilityAction.Bonus; + + if (bonus < cutoff) + { + // We know none of the other actions can beat this as they're pre-sorted + actions.Clear(); + break; + } + + var score = utilityAction.GetScore(_request.Context, cutoff); + if (score > cutoff) + { + foundAction = utilityAction; + cutoff = score; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + _request.Context.GetState().SetValue(cutoff); +#if DEBUG + if (foundAction != null) + { + FoundAction?.Invoke(new SharedAiDebug.UtilityAiDebugMessage( + _request.Context.GetState().GetValue().Uid, + DebugTime, + cutoff, + foundAction.GetType().Name, + consideredTaskCount)); + } + +#endif + _request.Context.ResetPlanning(); + + return foundAction; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionSystem.cs new file mode 100644 index 0000000000..55fb3af735 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/LoadBalancer/AiActionSystem.cs @@ -0,0 +1,28 @@ +using System.Threading; +using Content.Server.GameObjects.EntitySystems.JobQueues.Queues; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer +{ + /// + /// This will queue up an AI's request for an action and give it one when possible + /// + public class AiActionSystem : EntitySystem + { + private readonly AiActionJobQueue _aiRequestQueue = new AiActionJobQueue(); + + public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken) + { + var job = new AiActionRequestJob(0.002, request, cancellationToken.Token); + // AI should already know if it shouldn't request again + _aiRequestQueue.EnqueueJob(job); + return job; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + _aiRequestQueue.Process(); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs new file mode 100644 index 0000000000..e5df90943d --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollidableMove.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameObjects.Components.Transform; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates +{ + public struct CollidableMove : IPathfindingGraphUpdate + { + public MoveEvent MoveEvent { get; } + + public CollidableMove(MoveEvent moveEvent) + { + MoveEvent = moveEvent; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs new file mode 100644 index 0000000000..0d7dde253a --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/CollisionChange.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates +{ + public class CollisionChange : IPathfindingGraphUpdate + { + public IEntity Owner { get; } + public bool Value { get; } + + public CollisionChange(IEntity owner, bool value) + { + Owner = owner; + Value = value; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs new file mode 100644 index 0000000000..30ee86f2aa --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/GridRemoval.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Map; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates +{ + public struct GridRemoval : IPathfindingGraphUpdate + { + public GridId GridId { get; } + + public GridRemoval(GridId gridId) + { + GridId = gridId; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs new file mode 100644 index 0000000000..69aa5c1eac --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/IPathfindingGraphUpdate.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates +{ + public interface IPathfindingGraphUpdate + { + + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs new file mode 100644 index 0000000000..501e4dabb8 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/GraphUpdates/TileUpdate.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Map; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates +{ + public struct TileUpdate : IPathfindingGraphUpdate + { + public TileUpdate(TileRef tile) + { + Tile = tile; + } + + public TileRef Tile { get; } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs new file mode 100644 index 0000000000..1ac13372ad --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/AStarPathfindingJob.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Content.Shared.AI; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders +{ + public class AStarPathfindingJob : Job> + { + public static event Action DebugRoute; + + private PathfindingNode _startNode; + private PathfindingNode _endNode; + private PathfindingArgs _pathfindingArgs; + + public AStarPathfindingJob( + double maxTime, + PathfindingNode startNode, + PathfindingNode endNode, + PathfindingArgs pathfindingArgs, + CancellationToken cancellationToken) : base(maxTime, cancellationToken) + { + _startNode = startNode; + _endNode = endNode; + _pathfindingArgs = pathfindingArgs; + } + + protected override async Task> Process() + { + if (_startNode == null || + _endNode == null || + Status == JobStatus.Finished) + { + return null; + } + + // If we couldn't get a nearby node that's good enough + if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs)) + { + return null; + } + + var openTiles = new PriorityQueue>(new PathfindingComparer()); + var gScores = new Dictionary(); + var cameFrom = new Dictionary(); + var closedTiles = new HashSet(); + + PathfindingNode currentNode = null; + openTiles.Add((0.0f, _startNode)); + gScores[_startNode] = 0.0f; + var routeFound = false; + var count = 0; + + while (openTiles.Count > 0) + { + count++; + + if (count % 20 == 0 && count > 0) + { + await SuspendIfOutOfTime(); + } + + if (_startNode == null || _endNode == null) + { + return null; + } + + (_, currentNode) = openTiles.Take(); + if (currentNode.Equals(_endNode)) + { + routeFound = true; + break; + } + + closedTiles.Add(currentNode); + + foreach (var (direction, nextNode) in currentNode.Neighbors) + { + if (closedTiles.Contains(nextNode)) + { + continue; + } + + // If tile is untraversable it'll be null + var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, nextNode); + + if (tileCost == null || !Utils.DirectionTraversable(_pathfindingArgs.CollisionMask, currentNode, direction)) + { + continue; + } + + var gScore = gScores[currentNode] + tileCost.Value; + + if (gScores.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue) + { + continue; + } + + cameFrom[nextNode] = currentNode; + gScores[nextNode] = gScore; + // pFactor is tie-breaker where the fscore is otherwise equal. + // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties + // There's other ways to do it but future consideration + var fScore = gScores[nextNode] + Utils.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f); + openTiles.Add((fScore, nextNode)); + } + } + + if (!routeFound) + { + return null; + } + + var route = Utils.ReconstructPath(cameFrom, currentNode); + + if (route.Count == 1) + { + return null; + } + +#if DEBUG + // Need to get data into an easier format to send to the relevant clients + if (DebugRoute != null && route.Count > 0) + { + var debugCameFrom = new Dictionary(cameFrom.Count); + var debugGScores = new Dictionary(gScores.Count); + var debugClosedTiles = new HashSet(closedTiles.Count); + + foreach (var (node, parent) in cameFrom) + { + debugCameFrom.Add(node.TileRef, parent.TileRef); + } + + foreach (var (node, score) in gScores) + { + debugGScores.Add(node.TileRef, score); + } + + foreach (var node in closedTiles) + { + debugClosedTiles.Add(node.TileRef); + } + + var debugRoute = new SharedAiDebug.AStarRouteDebug( + _pathfindingArgs.Uid, + route, + debugCameFrom, + debugGScores, + debugClosedTiles, + DebugTime); + + DebugRoute.Invoke(debugRoute); + } +#endif + + return route; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs new file mode 100644 index 0000000000..2b12b1b456 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/JpsPathfindingJob.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Content.Shared.AI; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders +{ + public class JpsPathfindingJob : Job> + { + public static event Action DebugRoute; + + private PathfindingNode _startNode; + private PathfindingNode _endNode; + private PathfindingArgs _pathfindingArgs; + + public JpsPathfindingJob(double maxTime, + PathfindingNode startNode, + PathfindingNode endNode, + PathfindingArgs pathfindingArgs, + CancellationToken cancellationToken) : base(maxTime, cancellationToken) + { + _startNode = startNode; + _endNode = endNode; + _pathfindingArgs = pathfindingArgs; + } + + protected override async Task> Process() + { + // VERY similar to A*; main difference is with the neighbor tiles you look for jump nodes instead + if (_startNode == null || + _endNode == null) + { + return null; + } + + // If we couldn't get a nearby node that's good enough + if (!Utils.TryEndNode(ref _endNode, _pathfindingArgs)) + { + return null; + } + + var openTiles = new PriorityQueue>(new PathfindingComparer()); + var gScores = new Dictionary(); + var cameFrom = new Dictionary(); + var closedTiles = new HashSet(); + +#if DEBUG + var jumpNodes = new HashSet(); +#endif + + PathfindingNode currentNode = null; + openTiles.Add((0, _startNode)); + gScores[_startNode] = 0.0f; + var routeFound = false; + var count = 0; + + while (openTiles.Count > 0) + { + count++; + + // JPS probably getting a lot fewer nodes than A* is + if (count % 5 == 0 && count > 0) + { + await SuspendIfOutOfTime(); + } + + (_, currentNode) = openTiles.Take(); + if (currentNode.Equals(_endNode)) + { + routeFound = true; + break; + } + + foreach (var (direction, _) in currentNode.Neighbors) + { + var jumpNode = GetJumpPoint(currentNode, direction, _endNode); + + if (jumpNode != null && !closedTiles.Contains(jumpNode)) + { + closedTiles.Add(jumpNode); +#if DEBUG + jumpNodes.Add(jumpNode); +#endif + // GetJumpPoint should already check if we can traverse to the node + var tileCost = Utils.GetTileCost(_pathfindingArgs, currentNode, jumpNode); + + if (tileCost == null) + { + throw new InvalidOperationException(); + } + + var gScore = gScores[currentNode] + tileCost.Value; + + if (gScores.TryGetValue(jumpNode, out var nextValue) && gScore >= nextValue) + { + continue; + } + + cameFrom[jumpNode] = currentNode; + gScores[jumpNode] = gScore; + // pFactor is tie-breaker where the fscore is otherwise equal. + // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties + // There's other ways to do it but future consideration + var fScore = gScores[jumpNode] + Utils.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f); + openTiles.Add((fScore, jumpNode)); + } + } + } + + if (!routeFound) + { + return null; + } + + var route = Utils.ReconstructJumpPath(cameFrom, currentNode); + if (route.Count == 1) + { + return null; + } + +#if DEBUG + // Need to get data into an easier format to send to the relevant clients + if (DebugRoute != null && route.Count > 0) + { + var debugJumpNodes = new HashSet(jumpNodes.Count); + + foreach (var node in jumpNodes) + { + debugJumpNodes.Add(node.TileRef); + } + + var debugRoute = new SharedAiDebug.JpsRouteDebug( + _pathfindingArgs.Uid, + route, + debugJumpNodes, + DebugTime); + + DebugRoute.Invoke(debugRoute); + } +#endif + + return route; + } + + private PathfindingNode GetJumpPoint(PathfindingNode currentNode, Direction direction, PathfindingNode endNode) + { + var count = 0; + + while (count < 1000) + { + count++; + var nextNode = currentNode.GetNeighbor(direction); + + // We'll do opposite DirectionTraversable just because of how the method's setup + // Nodes should be 2-way anyway. + if (nextNode == null || + Utils.GetTileCost(_pathfindingArgs, currentNode, nextNode) == null) + { + return null; + } + + if (nextNode == endNode) + { + return endNode; + } + + // Horizontal and vertical are treated the same i.e. + // They only check in their specific direction + // (So Going North means you check NorthWest and NorthEast to see if we're a jump point) + + // Diagonals also check the cardinal directions at the same time at the same time + + // See https://harablog.wordpress.com/2011/09/07/jump-point-search/ for original description + switch (direction) + { + case Direction.East: + if (IsCardinalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + break; + case Direction.NorthEast: + if (IsDiagonalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null) + { + return nextNode; + } + + break; + case Direction.North: + if (IsCardinalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + break; + case Direction.NorthWest: + if (IsDiagonalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null) + { + return nextNode; + } + + break; + case Direction.West: + if (IsCardinalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + break; + case Direction.SouthWest: + if (IsDiagonalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null) + { + return nextNode; + } + + break; + case Direction.South: + if (IsCardinalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + break; + case Direction.SouthEast: + if (IsDiagonalJumpPoint(direction, nextNode)) + { + return nextNode; + } + + if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null) + { + return nextNode; + } + + break; + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, null); + } + + currentNode = nextNode; + } + + Logger.WarningS("pathfinding", "Recursion found in JPS pathfinder"); + return null; + } + + private bool IsDiagonalJumpPoint(Direction direction, PathfindingNode currentNode) + { + // If we're going diagonally need to check all cardinals. + // I just just using casts int casts and offset to make it smaller but brain no workyand it wasn't working. + // From NorthEast we check (Closed / Open) S - SE, W - NW + + PathfindingNode openNeighborOne; + PathfindingNode closedNeighborOne; + PathfindingNode openNeighborTwo; + PathfindingNode closedNeighborTwo; + + switch (direction) + { + case Direction.NorthEast: + openNeighborOne = currentNode.GetNeighbor(Direction.SouthEast); + closedNeighborOne = currentNode.GetNeighbor(Direction.South); + + openNeighborTwo = currentNode.GetNeighbor(Direction.NorthWest); + closedNeighborTwo = currentNode.GetNeighbor(Direction.West); + break; + case Direction.SouthEast: + openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast); + closedNeighborOne = currentNode.GetNeighbor(Direction.North); + + openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest); + closedNeighborTwo = currentNode.GetNeighbor(Direction.West); + break; + case Direction.SouthWest: + openNeighborOne = currentNode.GetNeighbor(Direction.NorthWest); + closedNeighborOne = currentNode.GetNeighbor(Direction.North); + + openNeighborTwo = currentNode.GetNeighbor(Direction.SouthEast); + closedNeighborTwo = currentNode.GetNeighbor(Direction.East); + break; + case Direction.NorthWest: + openNeighborOne = currentNode.GetNeighbor(Direction.SouthWest); + closedNeighborOne = currentNode.GetNeighbor(Direction.South); + + openNeighborTwo = currentNode.GetNeighbor(Direction.NorthEast); + closedNeighborTwo = currentNode.GetNeighbor(Direction.East); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if ((closedNeighborOne == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null) + && openNeighborOne != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null) + { + return true; + } + + if ((closedNeighborTwo == null || Utils.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null) + && openNeighborTwo != null && Utils.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null) + { + return true; + } + + return false; + } + + /// + /// Check to see if the node is a jump point (only works for cardinal directions) + /// + private bool IsCardinalJumpPoint(Direction direction, PathfindingNode currentNode) + { + PathfindingNode openNeighborOne; + PathfindingNode closedNeighborOne; + PathfindingNode openNeighborTwo; + PathfindingNode closedNeighborTwo; + + switch (direction) + { + case Direction.North: + openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast); + closedNeighborOne = currentNode.GetNeighbor(Direction.East); + + openNeighborTwo = currentNode.GetNeighbor(Direction.NorthWest); + closedNeighborTwo = currentNode.GetNeighbor(Direction.West); + break; + case Direction.East: + openNeighborOne = currentNode.GetNeighbor(Direction.NorthEast); + closedNeighborOne = currentNode.GetNeighbor(Direction.North); + + openNeighborTwo = currentNode.GetNeighbor(Direction.SouthEast); + closedNeighborTwo = currentNode.GetNeighbor(Direction.South); + break; + case Direction.South: + openNeighborOne = currentNode.GetNeighbor(Direction.SouthEast); + closedNeighborOne = currentNode.GetNeighbor(Direction.East); + + openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest); + closedNeighborTwo = currentNode.GetNeighbor(Direction.West); + break; + case Direction.West: + openNeighborOne = currentNode.GetNeighbor(Direction.NorthWest); + closedNeighborOne = currentNode.GetNeighbor(Direction.North); + + openNeighborTwo = currentNode.GetNeighbor(Direction.SouthWest); + closedNeighborTwo = currentNode.GetNeighbor(Direction.South); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if ((closedNeighborOne == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborOne.CollisionMask)) && + (openNeighborOne != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborOne.CollisionMask))) + { + return true; + } + + if ((closedNeighborTwo == null || !Utils.Traversable(_pathfindingArgs.CollisionMask, closedNeighborTwo.CollisionMask)) && + (openNeighborTwo != null && Utils.Traversable(_pathfindingArgs.CollisionMask, openNeighborTwo.CollisionMask))) + { + return true; + } + + return false; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs new file mode 100644 index 0000000000..d2a01ea1ce --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingArgs.cs @@ -0,0 +1,41 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders +{ + public struct PathfindingArgs + { + public EntityUid Uid { get; } + public int CollisionMask { get; } + public TileRef Start { get; } + public TileRef End { get; } + // How close we need to get to the endpoint to be 'done' + public float Proximity { get; } + // Whether we use cardinal only or not + public bool AllowDiagonals { get; } + // Can we go through walls + public bool NoClip { get; } + // Can we traverse space tiles + public bool AllowSpace { get; } + + public PathfindingArgs( + EntityUid entityUid, + int collisionMask, + TileRef start, + TileRef end, + float proximity = 0.0f, + bool allowDiagonals = true, + bool noClip = false, + bool allowSpace = false) + { + Uid = entityUid; + CollisionMask = collisionMask; + Start = start; + End = end; + Proximity = proximity; + AllowDiagonals = allowDiagonals; + NoClip = noClip; + AllowSpace = allowSpace; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingComparer.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingComparer.cs new file mode 100644 index 0000000000..7188542f5e --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Pathfinders/PathfindingComparer.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.Pathfinding; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders +{ + public class PathfindingComparer : IComparer> + { + public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y) + { + return y.Item1.CompareTo(x.Item1); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs new file mode 100644 index 0000000000..74b58fd5b8 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingChunk.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding +{ + public class PathfindingChunk + { + public GridId GridId { get; } + + public MapIndices Indices => _indices; + private readonly MapIndices _indices; + + // Nodes per chunk row + public static int ChunkSize => 16; + public PathfindingNode[,] Nodes => _nodes; + private PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize]; + public Dictionary Neighbors { get; } = new Dictionary(8); + + public PathfindingChunk(GridId gridId, MapIndices indices) + { + GridId = gridId; + _indices = indices; + } + + public void Initialize() + { + var grid = IoCManager.Resolve().GetGrid(GridId); + for (var x = 0; x < ChunkSize; x++) + { + for (var y = 0; y < ChunkSize; y++) + { + var tileRef = grid.GetTileRef(new MapIndices(x + _indices.X, y + _indices.Y)); + CreateNode(tileRef); + } + } + + RefreshNodeNeighbors(); + } + + /// + /// Updates all internal nodes with references to every other internal node + /// + private void RefreshNodeNeighbors() + { + for (var x = 0; x < ChunkSize; x++) + { + for (var y = 0; y < ChunkSize; y++) + { + var node = _nodes[x, y]; + // West + if (x != 0) + { + if (y != ChunkSize - 1) + { + node.AddNeighbor(Direction.NorthWest, _nodes[x - 1, y + 1]); + } + node.AddNeighbor(Direction.West, _nodes[x - 1, y]); + if (y != 0) + { + node.AddNeighbor(Direction.SouthWest, _nodes[x - 1, y - 1]); + } + } + + // Same column + if (y != ChunkSize - 1) + { + node.AddNeighbor(Direction.North, _nodes[x, y + 1]); + } + + if (y != 0) + { + node.AddNeighbor(Direction.South, _nodes[x, y - 1]); + } + + // East + if (x != ChunkSize - 1) + { + if (y != ChunkSize - 1) + { + node.AddNeighbor(Direction.NorthEast, _nodes[x + 1, y + 1]); + } + node.AddNeighbor(Direction.East, _nodes[x + 1, y]); + if (y != 0) + { + node.AddNeighbor(Direction.SouthEast, _nodes[x + 1, y - 1]); + } + } + } + } + } + + /// + /// This will work both ways + /// + /// + /// + public void AddNeighbor(PathfindingChunk chunk) + { + if (chunk == this) return; + if (Neighbors.ContainsValue(chunk)) + { + return; + } + + Direction direction; + if (chunk.Indices.X < _indices.X) + { + if (chunk.Indices.Y > _indices.Y) + { + direction = Direction.NorthWest; + } else if (chunk.Indices.Y < _indices.Y) + { + direction = Direction.SouthWest; + } + else + { + direction = Direction.West; + } + } + else if (chunk.Indices.X > _indices.X) + { + if (chunk.Indices.Y > _indices.Y) + { + direction = Direction.NorthEast; + } else if (chunk.Indices.Y < _indices.Y) + { + direction = Direction.SouthEast; + } + else + { + direction = Direction.East; + } + } + else + { + if (chunk.Indices.Y > _indices.Y) + { + direction = Direction.North; + } else if (chunk.Indices.Y < _indices.Y) + { + direction = Direction.South; + } + else + { + throw new InvalidOperationException(); + } + } + + Neighbors.TryAdd(direction, chunk); + + foreach (var node in GetBorderNodes(direction)) + { + foreach (var counter in chunk.GetCounterpartNodes(direction)) + { + var xDiff = node.TileRef.X - counter.TileRef.X; + var yDiff = node.TileRef.Y - counter.TileRef.Y; + + if (Math.Abs(xDiff) <= 1 && Math.Abs(yDiff) <= 1) + { + node.AddNeighbor(counter); + counter.AddNeighbor(node); + } + } + } + + chunk.Neighbors.TryAdd(OppositeDirection(direction), this); + + if (Neighbors.Count > 8) + { + throw new InvalidOperationException(); + } + } + + private Direction OppositeDirection(Direction direction) + { + return (Direction) (((int) direction + 4) % 8); + } + + // TODO I was too tired to think of an easier system. Could probably just google an array wraparound + private IEnumerable GetCounterpartNodes(Direction direction) + { + switch (direction) + { + case Direction.West: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[ChunkSize - 1, i]; + } + break; + case Direction.SouthWest: + yield return _nodes[ChunkSize - 1, ChunkSize - 1]; + break; + case Direction.South: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[i, ChunkSize - 1]; + } + break; + case Direction.SouthEast: + yield return _nodes[0, ChunkSize - 1]; + break; + case Direction.East: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[0, i]; + } + break; + case Direction.NorthEast: + yield return _nodes[0, 0]; + break; + case Direction.North: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[i, 0]; + } + break; + case Direction.NorthWest: + yield return _nodes[ChunkSize - 1, 0]; + break; + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, null); + } + } + + public IEnumerable GetBorderNodes(Direction direction) + { + switch (direction) + { + case Direction.East: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[ChunkSize - 1, i]; + } + break; + case Direction.NorthEast: + yield return _nodes[ChunkSize - 1, ChunkSize - 1]; + break; + case Direction.North: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[i, ChunkSize - 1]; + } + break; + case Direction.NorthWest: + yield return _nodes[0, ChunkSize - 1]; + break; + case Direction.West: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[0, i]; + } + break; + case Direction.SouthWest: + yield return _nodes[0, 0]; + break; + case Direction.South: + for (var i = 0; i < ChunkSize; i++) + { + yield return _nodes[i, 0]; + } + break; + case Direction.SouthEast: + yield return _nodes[ChunkSize - 1, 0]; + break; + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, null); + } + } + + public bool InBounds(TileRef tile) + { + if (tile.X < _indices.X || tile.Y < _indices.Y) return false; + if (tile.X >= _indices.X + ChunkSize || tile.Y >= _indices.Y + ChunkSize) return false; + return true; + } + + /// + /// Returns true if the tile is on the outer edge + /// + /// + /// + public bool OnEdge(PathfindingNode node) + { + if (node.TileRef.X == _indices.X) return true; + if (node.TileRef.Y == _indices.Y) return true; + if (node.TileRef.X == _indices.X + ChunkSize - 1) return true; + if (node.TileRef.Y == _indices.Y + ChunkSize - 1) return true; + return false; + } + + public PathfindingNode GetNode(TileRef tile) + { + var chunkX = tile.X - _indices.X; + var chunkY = tile.Y - _indices.Y; + + return _nodes[chunkX, chunkY]; + } + + public void UpdateNode(TileRef tile) + { + var node = GetNode(tile); + node.UpdateTile(tile); + } + + private void CreateNode(TileRef tile, PathfindingChunk parent = null) + { + if (parent == null) + { + parent = this; + } + + var node = new PathfindingNode(parent, tile); + var offsetX = tile.X - Indices.X; + var offsetY = tile.Y - Indices.Y; + _nodes[offsetX, offsetY] = node; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs new file mode 100644 index 0000000000..b9b110a566 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingNode.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.GameObjects.EntitySystems.Pathfinding +{ + public class PathfindingNode + { + // TODO: Add access ID here + public PathfindingChunk ParentChunk => _parentChunk; + private readonly PathfindingChunk _parentChunk; + public TileRef TileRef { get; private set; } + public List CollisionLayers { get; } + public int CollisionMask { get; private set; } + public Dictionary Neighbors => _neighbors; + private Dictionary _neighbors = new Dictionary(); + + public PathfindingNode(PathfindingChunk parent, TileRef tileRef, List collisionLayers = null) + { + _parentChunk = parent; + TileRef = tileRef; + if (collisionLayers == null) + { + CollisionLayers = new List(); + } + else + { + CollisionLayers = collisionLayers; + } + GenerateMask(); + } + + public void AddNeighbor(Direction direction, PathfindingNode node) + { + _neighbors.Add(direction, node); + } + + public void AddNeighbor(PathfindingNode node) + { + if (node.TileRef.GridIndex != TileRef.GridIndex) + { + throw new InvalidOperationException(); + } + + Direction direction; + if (node.TileRef.X < TileRef.X) + { + if (node.TileRef.Y > TileRef.Y) + { + direction = Direction.NorthWest; + } else if (node.TileRef.Y < TileRef.Y) + { + direction = Direction.SouthWest; + } + else + { + direction = Direction.West; + } + } + else if (node.TileRef.X > TileRef.X) + { + if (node.TileRef.Y > TileRef.Y) + { + direction = Direction.NorthEast; + } else if (node.TileRef.Y < TileRef.Y) + { + direction = Direction.SouthEast; + } + else + { + direction = Direction.East; + } + } + else + { + if (node.TileRef.Y > TileRef.Y) + { + direction = Direction.North; + } + else + { + direction = Direction.South; + } + } + + if (_neighbors.ContainsKey(direction)) + { + // Should we verify that they align? + return; + } + + _neighbors.Add(direction, node); + } + + public PathfindingNode GetNeighbor(Direction direction) + { + _neighbors.TryGetValue(direction, out var node); + return node; + } + + public void UpdateTile(TileRef newTile) + { + TileRef = newTile; + } + + public void AddCollisionLayer(int layer) + { + CollisionLayers.Add(layer); + GenerateMask(); + } + + public void RemoveCollisionLayer(int layer) + { + CollisionLayers.Remove(layer); + GenerateMask(); + } + + private void GenerateMask() + { + CollisionMask = 0x0; + + foreach (var layer in CollisionLayers) + { + CollisionMask |= layer; + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs new file mode 100644 index 0000000000..490f909f7d --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/PathfindingSystem.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Content.Server.GameObjects.Components.Doors; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.GraphUpdates; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Content.Server.GameObjects.EntitySystems.JobQueues.Queues; +using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Components.Transform; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding +{ + /* + // TODO: IMO use rectangular symmetry reduction on the nodes with collision at all., or + alternatively store all rooms and have an alternative graph for humanoid mobs (same collision mask, needs access etc). You could also just path from room to room as needed. + // TODO: Longer term -> Handle collision layer changes? + */ + /// + /// This system handles pathfinding graph updates as well as dispatches to the pathfinder + /// (90% of what it's doing is graph updates so not much point splitting the 2 roles) + /// + public class PathfindingSystem : EntitySystem + { +#pragma warning disable 649 + [Dependency] private readonly IMapManager _mapManager; +#pragma warning restore 649 + + public IReadOnlyDictionary> Graph => _graph; + private readonly Dictionary> _graph = new Dictionary>(); + // Every tick we queue up all the changes and do them at once + private readonly Queue _queuedGraphUpdates = new Queue(); + private readonly PathfindingJobQueue _pathfindingQueue = new PathfindingJobQueue(); + + // Need to store previously known entity positions for collidables for when they move + private readonly Dictionary _lastKnownPositions = new Dictionary(); + + /// + /// Ask for the pathfinder to gimme somethin + /// + /// + /// + /// + public Job> RequestPath(PathfindingArgs pathfindingArgs, CancellationToken cancellationToken) + { + var startNode = GetNode(pathfindingArgs.Start); + var endNode = GetNode(pathfindingArgs.End); + var job = new AStarPathfindingJob(0.003, startNode, endNode, pathfindingArgs, cancellationToken); + _pathfindingQueue.EnqueueJob(job); + + return job; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Make sure graph is updated, then get pathfinders + ProcessGraphUpdates(); + _pathfindingQueue.Process(); + } + + private void ProcessGraphUpdates() + { + for (var i = 0; i < Math.Min(50, _queuedGraphUpdates.Count); i++) + { + var update = _queuedGraphUpdates.Dequeue(); + switch (update) + { + case CollidableMove move: + HandleCollidableMove(move); + break; + case CollisionChange change: + if (change.Value) + { + HandleCollidableAdd(change.Owner); + } + else + { + HandleCollidableRemove(change.Owner); + } + + break; + case GridRemoval removal: + HandleGridRemoval(removal); + break; + case TileUpdate tile: + HandleTileUpdate(tile); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private void HandleGridRemoval(GridRemoval removal) + { + if (!_graph.ContainsKey(removal.GridId)) + { + throw new InvalidOperationException(); + } + + _graph.Remove(removal.GridId); + } + + private void HandleTileUpdate(TileUpdate tile) + { + var chunk = GetChunk(tile.Tile); + chunk.UpdateNode(tile.Tile); + } + + public PathfindingChunk GetChunk(TileRef tile) + { + var chunkX = (int) (Math.Floor((float) tile.X / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize); + var chunkY = (int) (Math.Floor((float) tile.Y / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize); + var mapIndices = new MapIndices(chunkX, chunkY); + + if (_graph.TryGetValue(tile.GridIndex, out var chunks)) + { + if (!chunks.ContainsKey(mapIndices)) + { + CreateChunk(tile.GridIndex, mapIndices); + } + + return chunks[mapIndices]; + } + + var newChunk = CreateChunk(tile.GridIndex, mapIndices); + + return newChunk; + } + + private PathfindingChunk CreateChunk(GridId gridId, MapIndices indices) + { + var newChunk = new PathfindingChunk(gridId, indices); + newChunk.Initialize(); + if (_graph.TryGetValue(gridId, out var chunks)) + { + for (var x = -1; x < 2; x++) + { + for (var y = -1; y < 2; y++) + { + if (x == 0 && y == 0) continue; + + var neighborIndices = new MapIndices( + indices.X + x * PathfindingChunk.ChunkSize, + indices.Y + y * PathfindingChunk.ChunkSize); + + if (chunks.TryGetValue(neighborIndices, out var neighborChunk)) + { + neighborChunk.AddNeighbor(newChunk); + } + } + } + } + else + { + _graph.Add(gridId, new Dictionary()); + } + + _graph[gridId].Add(indices, newChunk); + + return newChunk; + } + + public PathfindingNode GetNode(TileRef tile) + { + var chunk = GetChunk(tile); + var node = chunk.GetNode(tile); + + return node; + } + + public override void Initialize() + { + IoCManager.InjectDependencies(this); + SubscribeLocalEvent(QueueCollisionEnabledEvent); + SubscribeLocalEvent(QueueCollidableMove); + + // Handle all the base grid changes + // Anything that affects traversal (i.e. collision layer) is handled separately. + _mapManager.OnGridRemoved += QueueGridRemoval; + _mapManager.GridChanged += QueueGridChange; + _mapManager.TileChanged += QueueTileChange; + } + + public override void Shutdown() + { + base.Shutdown(); + _mapManager.OnGridRemoved -= QueueGridRemoval; + _mapManager.GridChanged -= QueueGridChange; + _mapManager.TileChanged -= QueueTileChange; + } + + private void QueueGridRemoval(GridId gridId) + { + _queuedGraphUpdates.Enqueue(new GridRemoval(gridId)); + } + + private void QueueGridChange(object sender, GridChangedEventArgs eventArgs) + { + foreach (var (position, _) in eventArgs.Modified) + { + _queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.Grid.GetTileRef(position))); + } + } + + private void QueueTileChange(object sender, TileChangedEventArgs eventArgs) + { + _queuedGraphUpdates.Enqueue(new TileUpdate(eventArgs.NewTile)); + } + + #region collidable + /// + /// If an entity's collision gets turned on then we need to update its current position + /// + /// + private void HandleCollidableAdd(IEntity entity) + { + // It's a grid / gone / a door / we already have it (which probably shouldn't happen) + if (entity.Prototype == null || + entity.Deleted || + entity.HasComponent() || + entity.HasComponent() || + _lastKnownPositions.ContainsKey(entity)) + { + return; + } + + var grid = _mapManager.GetGrid(entity.Transform.GridID); + var tileRef = grid.GetTileRef(entity.Transform.GridPosition); + + var collisionLayer = entity.GetComponent().CollisionLayer; + + var chunk = GetChunk(tileRef); + var node = chunk.GetNode(tileRef); + node.AddCollisionLayer(collisionLayer); + + _lastKnownPositions.Add(entity, tileRef); + } + + /// + /// If an entity's collision is removed then stop tracking it from the graph + /// + /// + private void HandleCollidableRemove(IEntity entity) + { + if (entity.Prototype == null || + entity.Deleted || + entity.HasComponent() || + entity.HasComponent() || + !_lastKnownPositions.ContainsKey(entity)) + { + return; + } + + _lastKnownPositions.Remove(entity); + + var grid = _mapManager.GetGrid(entity.Transform.GridID); + var tileRef = grid.GetTileRef(entity.Transform.GridPosition); + + if (!entity.TryGetComponent(out CollidableComponent collidableComponent)) + { + return; + } + + var collisionLayer = collidableComponent.CollisionLayer; + + var chunk = GetChunk(tileRef); + var node = chunk.GetNode(tileRef); + node.RemoveCollisionLayer(collisionLayer); + } + + private void QueueCollidableMove(MoveEvent moveEvent) + { + _queuedGraphUpdates.Enqueue(new CollidableMove(moveEvent)); + } + + private void HandleCollidableMove(CollidableMove move) + { + if (!_lastKnownPositions.ContainsKey(move.MoveEvent.Sender)) + { + return; + } + + // The pathfinding graph is tile-based so first we'll check if they're on a different tile and if we need to update. + // If you get entities bigger than 1 tile wide you'll need some other system so god help you. + var moveEvent = move.MoveEvent; + + if (moveEvent.Sender.Deleted) + { + HandleCollidableRemove(moveEvent.Sender); + return; + } + + _lastKnownPositions.TryGetValue(moveEvent.Sender, out var oldTile); + var newTile = _mapManager.GetGrid(moveEvent.NewPosition.GridID).GetTileRef(moveEvent.NewPosition); + + if (oldTile == newTile) + { + return; + } + + _lastKnownPositions[moveEvent.Sender] = newTile; + + if (!moveEvent.Sender.TryGetComponent(out CollidableComponent collidableComponent)) + { + HandleCollidableRemove(moveEvent.Sender); + return; + } + + var collisionLayer = collidableComponent.CollisionLayer; + + var gridIds = new HashSet(2) {oldTile.GridIndex, newTile.GridIndex}; + + foreach (var gridId in gridIds) + { + if (oldTile.GridIndex == gridId) + { + var oldChunk = GetChunk(oldTile); + var oldNode = oldChunk.GetNode(oldTile); + oldNode.RemoveCollisionLayer(collisionLayer); + } + + if (newTile.GridIndex == gridId) + { + var newChunk = GetChunk(newTile); + var newNode = newChunk.GetNode(newTile); + newNode.RemoveCollisionLayer(collisionLayer); + } + } + } + + private void QueueCollisionEnabledEvent(CollisionChangeEvent collisionEvent) + { + // TODO: Handle containers + var entityManager = IoCManager.Resolve(); + var entity = entityManager.GetEntity(collisionEvent.Owner); + switch (collisionEvent.CanCollide) + { + case true: + _queuedGraphUpdates.Enqueue(new CollisionChange(entity, true)); + break; + case false: + _queuedGraphUpdates.Enqueue(new CollisionChange(entity, false)); + break; + } + } + #endregion + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/ServerPathfindingDebugSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/ServerPathfindingDebugSystem.cs new file mode 100644 index 0000000000..f67b441ff5 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/ServerPathfindingDebugSystem.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; +using Content.Shared.AI; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Maths; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding +{ +#if DEBUG + [UsedImplicitly] + public class ServerPathfindingDebugSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + AStarPathfindingJob.DebugRoute += DispatchAStarDebug; + JpsPathfindingJob.DebugRoute += DispatchJpsDebug; + SubscribeNetworkEvent(DispatchGraph); + } + + public override void Shutdown() + { + base.Shutdown(); + AStarPathfindingJob.DebugRoute -= DispatchAStarDebug; + JpsPathfindingJob.DebugRoute -= DispatchJpsDebug; + } + + private void DispatchAStarDebug(SharedAiDebug.AStarRouteDebug routeDebug) + { + var mapManager = IoCManager.Resolve(); + var route = new List(); + foreach (var tile in routeDebug.Route) + { + var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + route.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position); + } + + var cameFrom = new Dictionary(); + foreach (var (from, to) in routeDebug.CameFrom) + { + var tileOneGrid = mapManager.GetGrid(from.GridIndex).GridTileToLocal(from.GridIndices); + var tileOneWorld = mapManager.GetGrid(from.GridIndex).LocalToWorld(tileOneGrid).Position; + var tileTwoGrid = mapManager.GetGrid(to.GridIndex).GridTileToLocal(to.GridIndices); + var tileTwoWorld = mapManager.GetGrid(to.GridIndex).LocalToWorld(tileTwoGrid).Position; + cameFrom.Add(tileOneWorld, tileTwoWorld); + } + + var gScores = new Dictionary(); + foreach (var (tile, score) in routeDebug.GScores) + { + var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + gScores.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position, score); + } + + var closedTiles = new List(); + foreach (var tile in routeDebug.ClosedTiles) + { + var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + closedTiles.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position); + } + + var systemMessage = new SharedAiDebug.AStarRouteMessage( + routeDebug.EntityUid, + route, + cameFrom, + gScores, + closedTiles, + routeDebug.TimeTaken + ); + + EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage); + } + + private void DispatchJpsDebug(SharedAiDebug.JpsRouteDebug routeDebug) + { + var mapManager = IoCManager.Resolve(); + var route = new List(); + foreach (var tile in routeDebug.Route) + { + var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + route.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position); + } + + var jumpNodes = new List(); + foreach (var tile in routeDebug.JumpNodes) + { + var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices); + jumpNodes.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position); + } + + var systemMessage = new SharedAiDebug.JpsRouteMessage( + routeDebug.EntityUid, + route, + jumpNodes, + routeDebug.TimeTaken + ); + + EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage); + } + + private void DispatchGraph(SharedAiDebug.RequestPathfindingGraphMessage message) + { + var pathfindingSystem = EntitySystemManager.GetEntitySystem(); + var mapManager = IoCManager.Resolve(); + var result = new Dictionary>(); + + var idx = 0; + + foreach (var (gridId, chunks) in pathfindingSystem.Graph) + { + var gridManager = mapManager.GetGrid(gridId); + + foreach (var chunk in chunks.Values) + { + var nodes = new List(); + foreach (var node in chunk.Nodes) + { + var worldTile = gridManager.GridTileToWorldPos(node.TileRef.GridIndices); + + nodes.Add(worldTile); + } + + result.Add(idx, nodes); + idx++; + } + } + + var systemMessage = new SharedAiDebug.PathfindingGraphMessage(result); + EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage); + } + } +#endif +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs new file mode 100644 index 0000000000..a72869db78 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/Pathfinding/Utils.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders; +using Content.Server.GameObjects.EntitySystems.Pathfinding; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding +{ + public static class Utils + { + public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs) + { + if (!Traversable(pathfindingArgs.CollisionMask, endNode.CollisionMask)) + { + if (pathfindingArgs.Proximity > 0.0f) + { + // TODO: Should make this account for proximities, + // probably some kind of breadth-first search to find a valid one + foreach (var (direction, node) in endNode.Neighbors) + { + if (Traversable(pathfindingArgs.CollisionMask, node.CollisionMask)) + { + endNode = node; + return true; + } + } + } + + return false; + } + + return true; + } + + public static bool DirectionTraversable(int collisionMask, PathfindingNode currentNode, Direction direction) + { + // If it's a diagonal we need to check NSEW to see if we can get to it and stop corner cutting, NE needs N and E etc. + // Given there's different collision layers stored for each node in the graph it's probably not worth it to cache this + // Also this will help with corner-cutting + + currentNode.Neighbors.TryGetValue(Direction.North, out var northNeighbor); + currentNode.Neighbors.TryGetValue(Direction.South, out var southNeighbor); + currentNode.Neighbors.TryGetValue(Direction.East, out var eastNeighbor); + currentNode.Neighbors.TryGetValue(Direction.West, out var westNeighbor); + + switch (direction) + { + case Direction.NorthEast: + if (northNeighbor == null || eastNeighbor == null) return false; + if (!Traversable(collisionMask, northNeighbor.CollisionMask) || + !Traversable(collisionMask, eastNeighbor.CollisionMask)) + { + return false; + } + break; + case Direction.NorthWest: + if (northNeighbor == null || westNeighbor == null) return false; + if (!Traversable(collisionMask, northNeighbor.CollisionMask) || + !Traversable(collisionMask, westNeighbor.CollisionMask)) + { + return false; + } + break; + case Direction.SouthWest: + if (southNeighbor == null || westNeighbor == null) return false; + if (!Traversable(collisionMask, southNeighbor.CollisionMask) || + !Traversable(collisionMask, westNeighbor.CollisionMask)) + { + return false; + } + break; + case Direction.SouthEast: + if (southNeighbor == null || eastNeighbor == null) return false; + if (!Traversable(collisionMask, southNeighbor.CollisionMask) || + !Traversable(collisionMask, eastNeighbor.CollisionMask)) + { + return false; + } + break; + } + + return true; + } + + public static bool Traversable(int collisionMask, int nodeMask) + { + return (collisionMask & nodeMask) == 0; + } + + public static Queue ReconstructPath(Dictionary cameFrom, PathfindingNode current) + { + var running = new Stack(); + running.Push(current.TileRef); + while (cameFrom.ContainsKey(current)) + { + var previousCurrent = current; + current = cameFrom[current]; + cameFrom.Remove(previousCurrent); + running.Push(current.TileRef); + } + + var result = new Queue(running); + + return result; + } + + /// + /// This will reconstruct the path and fill in the tile holes as well + /// + /// + /// + /// + public static Queue ReconstructJumpPath(Dictionary cameFrom, PathfindingNode current) + { + var running = new Stack(); + running.Push(current.TileRef); + while (cameFrom.ContainsKey(current)) + { + var previousCurrent = current; + current = cameFrom[current]; + var intermediate = previousCurrent; + cameFrom.Remove(previousCurrent); + var pathfindingSystem = IoCManager.Resolve().GetEntitySystem(); + var mapManager = IoCManager.Resolve(); + var grid = mapManager.GetGrid(current.TileRef.GridIndex); + + // Get all the intermediate nodes + while (true) + { + var xOffset = 0; + var yOffset = 0; + + if (intermediate.TileRef.X < current.TileRef.X) + { + xOffset += 1; + } + else if (intermediate.TileRef.X > current.TileRef.X) + { + xOffset -= 1; + } + else + { + xOffset = 0; + } + + if (intermediate.TileRef.Y < current.TileRef.Y) + { + yOffset += 1; + } + else if (intermediate.TileRef.Y > current.TileRef.Y) + { + yOffset -= 1; + } + else + { + yOffset = 0; + } + + intermediate = pathfindingSystem.GetNode(grid.GetTileRef( + new MapIndices(intermediate.TileRef.X + xOffset, intermediate.TileRef.Y + yOffset))); + + if (intermediate.TileRef != current.TileRef) + { + // Hacky corner cut fix + + running.Push(intermediate.TileRef); + continue; + } + + break; + } + running.Push(current.TileRef); + } + + var result = new Queue(running); + + return result; + } + + public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode) + { + // "Fast Euclidean" / octile. + // This implementation is written down in a few sources; it just saves doing sqrt. + int dstX = Math.Abs(currentNode.TileRef.X - endNode.TileRef.X); + int dstY = Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y); + if (dstX > dstY) + { + return 1.4f * dstY + (dstX - dstY); + } + + return 1.4f * dstX + (dstY - dstX); + } + + public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode) + { + return Math.Abs(currentNode.TileRef.X - endNode.TileRef.X) + Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y); + } + + public static float? GetTileCost(PathfindingArgs pathfindingArgs, PathfindingNode start, PathfindingNode end) + { + if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, end.CollisionMask)) + { + return null; + } + + if (!pathfindingArgs.AllowSpace && end.TileRef.Tile.IsEmpty) + { + return null; + } + + var cost = 1.0f; + + switch (pathfindingArgs.AllowDiagonals) + { + case true: + cost *= OctileDistance(end, start); + break; + // Manhattan distance + case false: + cost *= ManhattanDistance(end, start); + break; + } + + return cost; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/ServerAiDebugSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/ServerAiDebugSystem.cs new file mode 100644 index 0000000000..95faa68447 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/ServerAiDebugSystem.cs @@ -0,0 +1,30 @@ +using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer; +using Content.Shared.AI; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems.AI +{ +#if DEBUG + [UsedImplicitly] + public class ServerAiDebugSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + AiActionRequestJob.FoundAction += NotifyActionJob; + } + + public override void Shutdown() + { + base.Shutdown(); + AiActionRequestJob.FoundAction -= NotifyActionJob; + } + + private void NotifyActionJob(SharedAiDebug.UtilityAiDebugMessage message) + { + EntityManager.EntityNetManager.SendSystemNetworkMessage(message); + } + } +#endif +} diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index 2ef0da742e..66692a5543 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Input; using JetBrains.Annotations; +using Robust.Server.GameObjects; using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -319,7 +320,7 @@ namespace Content.Server.GameObjects.EntitySystems { CommandBinds.Builder .Bind(EngineKeyFunctions.Use, - new PointerInputCmdHandler(HandleUseItemInHand)) + new PointerInputCmdHandler(HandleClientUseItemInHand)) .Bind(ContentKeyFunctions.WideAttack, new PointerInputCmdHandler(HandleWideAttack)) .Bind(ContentKeyFunctions.ActivateItemInWorld, @@ -421,7 +422,31 @@ namespace Content.Server.GameObjects.EntitySystems return true; } - private bool HandleUseItemInHand(ICommonSession session, GridCoordinates coords, EntityUid uid) + /// + /// Entity will try and use their active hand at the target location. + /// Don't use for players + /// + /// + /// + /// + internal void UseItemInHand(IEntity entity, GridCoordinates coords, EntityUid uid) + { + if (entity.HasComponent()) + { + throw new InvalidOperationException(); + } + + if (entity.TryGetComponent(out CombatModeComponent combatMode) && combatMode.IsInCombatMode) + { + DoAttack(entity, coords); + } + else + { + UserInteraction(entity, coords, uid); + } + } + + private bool HandleClientUseItemInHand(ICommonSession session, GridCoordinates coords, EntityUid uid) { // client sanitization if (!_mapManager.GridExists(coords.GridID)) diff --git a/Content.Server/GameObjects/EntitySystems/JobQueues/IJob.cs b/Content.Server/GameObjects/EntitySystems/JobQueues/IJob.cs new file mode 100644 index 0000000000..e23429ae14 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/JobQueues/IJob.cs @@ -0,0 +1,10 @@ +using System.Collections; + +namespace Content.Server.GameObjects.EntitySystems.JobQueues +{ + public interface IJob + { + JobStatus Status { get; } + void Run(); + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/EntitySystems/JobQueues/Job.cs b/Content.Server/GameObjects/EntitySystems/JobQueues/Job.cs new file mode 100644 index 0000000000..014d35575c --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/JobQueues/Job.cs @@ -0,0 +1,232 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Robust.Shared.Log; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.EntitySystems.JobQueues +{ + /// + /// CPU-intensive job that can be suspended and resumed on the main thread + /// + /// + /// Implementations should overload . + /// Inside , implementations should only await on , + /// , or . + /// + /// The type of result this job generates + public abstract class Job : IJob + { + public JobStatus Status { get; private set; } = JobStatus.Pending; + + /// + /// Represents the status of this job as a regular task. + /// + public Task AsTask { get; } + + public T Result { get; private set; } + public Exception Exception { get; private set; } + protected CancellationToken Cancellation { get; } + + public double DebugTime { get; private set; } + private readonly double _maxTime; + protected readonly IStopwatch StopWatch; + + // TCS for the Task property. + private readonly TaskCompletionSource _taskTcs; + + // TCS to call to resume the suspended job. + private TaskCompletionSource _resume; + private Task _workInProgress; + + protected Job(double maxTime, CancellationToken cancellation = default) + : this(maxTime, new Stopwatch(), cancellation) + { + } + + protected Job(double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) + { + _maxTime = maxTime; + StopWatch = stopwatch; + Cancellation = cancellation; + + _taskTcs = new TaskCompletionSource(); + AsTask = _taskTcs.Task; + } + + /// + /// Suspends the current task immediately, yielding to other running jobs. + /// + /// + /// This does not stop the job queue from un-suspending the current task immediately again, + /// if there is still time left over. + /// + protected Task SuspendNow() + { + DebugTools.AssertNull(_resume); + + _resume = new TaskCompletionSource(); + Status = JobStatus.Paused; + DebugTime += StopWatch.Elapsed.TotalSeconds; + return _resume.Task; + } + + protected ValueTask SuspendIfOutOfTime() + { + DebugTools.AssertNull(_resume); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (StopWatch.Elapsed.TotalSeconds <= _maxTime || _maxTime == 0.0) + { + return new ValueTask(); + } + + return new ValueTask(SuspendNow()); + } + + /// + /// Wrapper to await on an external task. + /// + protected async Task WaitAsyncTask(Task task) + { + DebugTools.AssertNull(_resume); + + Status = JobStatus.Waiting; + DebugTime += StopWatch.Elapsed.TotalSeconds; + + var result = await task; + + // Immediately block on resume so that everything stays correct. + Status = JobStatus.Paused; + _resume = new TaskCompletionSource(); + + await _resume.Task; + + return result; + } + + /// + /// Wrapper to safely await on an external task. + /// + protected async Task WaitAsyncTask(Task task) + { + DebugTools.AssertNull(_resume); + + Status = JobStatus.Waiting; + DebugTime += StopWatch.Elapsed.TotalSeconds; + + await task; + + // Immediately block on resume so that everything stays correct. + _resume = new TaskCompletionSource(); + Status = JobStatus.Paused; + + await _resume.Task; + } + + public void Run() + { + _workInProgress ??= ProcessWrap(); + + if (Status == JobStatus.Finished) + { + return; + } + + DebugTools.Assert(_resume != null, + "Run() called without resume. Was this called while the job is in Waiting state?"); + var resume = _resume; + _resume = null; + + Status = JobStatus.Running; + StopWatch.Restart(); + + if (Cancellation.IsCancellationRequested) + { + resume.TrySetCanceled(); + } + else + { + resume.SetResult(null); + } + + if (Status != JobStatus.Finished && Status != JobStatus.Waiting) + { + DebugTools.Assert(_resume != null, + "Job suspended without _resume set. Did you await on an external task without using WaitAsyncTask?"); + } + } + + protected abstract Task Process(); + + private async Task ProcessWrap() + { + try + { + Cancellation.ThrowIfCancellationRequested(); + + // Making sure that the task starts inside the Running block, + // where the stopwatch is correctly set and such. + await SuspendNow(); + Result = await Process(); + + // TODO: not sure if it makes sense to connect Task directly up + // to the return value of this method/Process. + // Maybe? + _taskTcs.TrySetResult(Result); + } + catch (TaskCanceledException) + { + _taskTcs.TrySetCanceled(); + } + catch (Exception e) + { + // TODO: Should this be exposed differently? + // I feel that people might forget to check whether the job failed. + Logger.ErrorS("job", "Job failed on exception:\n{0}", e); + Exception = e; + _taskTcs.TrySetException(e); + } + finally + { + if (Status != JobStatus.Waiting) + { + // If we're blocked on waiting and the waiting task goes cancel/exception, + // this timing info would not be correct. + DebugTime += StopWatch.Elapsed.TotalSeconds; + } + Status = JobStatus.Finished; + } + } + } + + public enum JobStatus + { + /// + /// Job has been created and has not been ran yet. + /// + Pending, + + /// + /// Job is currently (yes, right now!) executing. + /// + Running, + + /// + /// Job is paused due to CPU limits. + /// + Paused, + + /// + /// Job is paused because of waiting on external task. + /// + Waiting, + + /// + /// Job is done. + /// + // TODO: Maybe have a different status code for cancelled/failed on exception? + Finished, + } +} diff --git a/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/AiActionJobQueue.cs b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/AiActionJobQueue.cs new file mode 100644 index 0000000000..731185477a --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/AiActionJobQueue.cs @@ -0,0 +1,4 @@ +namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues +{ + public sealed class AiActionJobQueue : JobQueue {} +} diff --git a/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/JobQueue.cs b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/JobQueue.cs new file mode 100644 index 0000000000..08c04739c0 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/JobQueue.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Robust.Shared.Timing; + +namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues +{ + public class JobQueue + { + private readonly IStopwatch _stopwatch; + + public JobQueue() : this(new Stopwatch()) + { + } + + public JobQueue(IStopwatch stopwatch) + { + _stopwatch = stopwatch; + } + + /// + /// How long the job's allowed to run for before suspending + /// + public virtual double MaxTime => 0.002; + + private readonly Queue _pendingQueue = new Queue(); + private readonly List _waitingJobs = new List(); + + public void EnqueueJob(IJob job) + { + _pendingQueue.Enqueue(job); + } + + public void Process() + { + // Move all finished waiting jobs back into the regular queue. + foreach (var waitingJob in _waitingJobs) + { + if (waitingJob.Status != JobStatus.Waiting) + { + _pendingQueue.Enqueue(waitingJob); + } + } + + _waitingJobs.RemoveAll(p => p.Status != JobStatus.Waiting); + + // At one point I tried making the pathfinding queue multi-threaded but ehhh didn't go great + // Could probably try it again at some point + // it just seemed slow af but I was probably doing something dumb with semaphores + _stopwatch.Restart(); + + // Although the jobs can stop themselves we might be able to squeeze more of them in the allotted time + while (_stopwatch.Elapsed.TotalSeconds < MaxTime && _pendingQueue.TryDequeue(out var job)) + { + // Deque and re-enqueue these to cycle them through to avoid starvation if we've got a lot of jobs. + + job.Run(); + + switch (job.Status) + { + case JobStatus.Finished: + continue; + case JobStatus.Waiting: + // If this job goes into waiting we have to move it into a separate list. + // Otherwise we'd just be spinning like mad here for external IO or such. + _waitingJobs.Add(job); + break; + default: + _pendingQueue.Enqueue(job); + break; + } + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/PathfindingJobQueue.cs b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/PathfindingJobQueue.cs new file mode 100644 index 0000000000..60ca114cdd --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/JobQueues/Queues/PathfindingJobQueue.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues +{ + public sealed class PathfindingJobQueue : JobQueue + { + public override double MaxTime => 0.003; + } +} diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index e9f4f9215c..e2e73daafc 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,4 +1,5 @@ -using Content.Server.Cargo; +using Content.Server.AI.WorldState; +using Content.Server.Cargo; using Content.Server.Chat; using Content.Server.GameTicking; using Content.Server.Interfaces; @@ -30,6 +31,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/AI/SharedAiDebug.cs b/Content.Shared/AI/SharedAiDebug.cs new file mode 100644 index 0000000000..23c838315d --- /dev/null +++ b/Content.Shared/AI/SharedAiDebug.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace Content.Shared.AI +{ + public static class SharedAiDebug + { + #region Mob Debug + [Serializable, NetSerializable] + public class UtilityAiDebugMessage : EntitySystemMessage + { + public EntityUid EntityUid { get; } + public double PlanningTime { get; } + public float ActionScore { get; } + public string FoundTask { get; } + public int ConsideredTaskCount { get; } + + public UtilityAiDebugMessage( + EntityUid entityUid, + double planningTime, + float actionScore, + string foundTask, + int consideredTaskCount) + { + EntityUid = entityUid; + PlanningTime = planningTime; + ActionScore = actionScore; + FoundTask = foundTask; + ConsideredTaskCount = consideredTaskCount; + } + } + #endregion + #region Pathfinder Debug + /// + /// Client asks the server for the pathfinding graph details + /// + [Serializable, NetSerializable] + public class RequestPathfindingGraphMessage : EntitySystemMessage {} + + [Serializable, NetSerializable] + public class PathfindingGraphMessage : EntitySystemMessage + { + public Dictionary> Graph { get; } + + public PathfindingGraphMessage(Dictionary> graph) + { + Graph = graph; + } + } + + public class AStarRouteDebug + { + public EntityUid EntityUid { get; } + public Queue Route { get; } + public Dictionary CameFrom { get; } + public Dictionary GScores { get; } + public HashSet ClosedTiles { get; } + public double TimeTaken { get; } + + public AStarRouteDebug( + EntityUid uid, + Queue route, + Dictionary cameFrom, + Dictionary gScores, + HashSet closedTiles, + double timeTaken) + { + EntityUid = uid; + Route = route; + CameFrom = cameFrom; + GScores = gScores; + ClosedTiles = closedTiles; + TimeTaken = timeTaken; + } + } + + public class JpsRouteDebug + { + public EntityUid EntityUid { get; } + public Queue Route { get; } + public HashSet JumpNodes { get; } + public double TimeTaken { get; } + + public JpsRouteDebug( + EntityUid uid, + Queue route, + HashSet jumpNodes, + double timeTaken) + { + EntityUid = uid; + Route = route; + JumpNodes = jumpNodes; + TimeTaken = timeTaken; + } + } + + [Serializable, NetSerializable] + public class AStarRouteMessage : EntitySystemMessage + { + public readonly EntityUid EntityUid; + public readonly IEnumerable Route; + public readonly Dictionary CameFrom; + public readonly Dictionary GScores; + public readonly List ClosedTiles; + public double TimeTaken; + + public AStarRouteMessage( + EntityUid uid, + IEnumerable route, + Dictionary cameFrom, + Dictionary gScores, + List closedTiles, + double timeTaken) + { + EntityUid = uid; + Route = route; + CameFrom = cameFrom; + GScores = gScores; + ClosedTiles = closedTiles; + TimeTaken = timeTaken; + } + } + + [Serializable, NetSerializable] + public class JpsRouteMessage : EntitySystemMessage + { + public readonly EntityUid EntityUid; + public readonly IEnumerable Route; + public readonly List JumpNodes; + public double TimeTaken; + + public JpsRouteMessage( + EntityUid uid, + IEnumerable route, + List jumpNodes, + double timeTaken) + { + EntityUid = uid; + Route = route; + JumpNodes = jumpNodes; + TimeTaken = timeTaken; + } + } + #endregion + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 5c37c53bb1..fc4e92a82a 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -48,6 +48,8 @@ public const uint SURGERY = 1042; public const uint MULTITOOLS = 1043; public const uint PDA = 1044; + public const uint PATHFINDER_DEBUG = 1045; + public const uint AI_DEBUG = 1046; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Content.Tests/Server/Jobs/JobQueueTest.cs b/Content.Tests/Server/Jobs/JobQueueTest.cs new file mode 100644 index 0000000000..02bc5c3aba --- /dev/null +++ b/Content.Tests/Server/Jobs/JobQueueTest.cs @@ -0,0 +1,216 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameObjects.EntitySystems.JobQueues; +using Content.Server.GameObjects.EntitySystems.JobQueues.Queues; +using NUnit.Framework; +using Robust.Shared.Timing; +using Robust.UnitTesting; + +namespace Content.Tests.Server.Jobs +{ + [TestFixture] + [TestOf(typeof(Job<>))] + [TestOf(typeof(JobQueue))] + public class JobQueueTest : RobustUnitTest + { + /// + /// Test a job that immediately exits with a value. + /// + [Test] + public void TestImmediateJob() + { + // Pass debug stopwatch so time doesn't advance. + var sw = new DebugStopwatch(); + var queue = new JobQueue(sw); + + var job = new ImmediateJob(); + + queue.EnqueueJob(job); + + queue.Process(); + + Assert.That(job.Status, Is.EqualTo(JobStatus.Finished)); + Assert.That(job.Result, Is.EqualTo("honk!")); + } + + [Test] + public void TestLongJob() + { + var swA = new DebugStopwatch(); + var swB = new DebugStopwatch(); + var queue = new LongJobQueue(swB); + + var job = new LongJob(swA, swB); + + queue.EnqueueJob(job); + + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Paused)); + Assert.That((float)job.DebugTime, new ApproxEqualityConstraint(1f)); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Paused)); + Assert.That((float)job.DebugTime, new ApproxEqualityConstraint(2f)); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Finished)); + + Assert.That(job.Result, Is.EqualTo("foo!")); + Assert.That((float)job.DebugTime, new ApproxEqualityConstraint(2.4f)); + } + + [Test] + public void TestLongJobCancel() + { + var swA = new DebugStopwatch(); + var swB = new DebugStopwatch(); + var queue = new LongJobQueue(swB); + + var cts = new CancellationTokenSource(); + var job = new LongJob(swA, swB, cts.Token); + + queue.EnqueueJob(job); + + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Paused)); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Paused)); + cts.Cancel(); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Finished)); + Assert.That((float)job.DebugTime, new ApproxEqualityConstraint(2.0f)); + + Assert.That(job.Result, Is.Null); + } + + [Test] + public void TestWaitingJob() + { + var sw = new DebugStopwatch(); + var queue = new LongJobQueue(sw); + + var tcs = new TaskCompletionSource(); + + var job = new WaitingJob(tcs.Task); + + queue.EnqueueJob(job); + + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Waiting)); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Waiting)); + tcs.SetResult(1); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Finished)); + + Assert.That(job.Result, Is.EqualTo("oof!")); + } + + [Test] + public void TestWaitingJobCancel() + { + var sw = new DebugStopwatch(); + var queue = new LongJobQueue(sw); + + var tcs = new TaskCompletionSource(); + + var job = new WaitingJob(tcs.Task); + + queue.EnqueueJob(job); + + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Waiting)); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Waiting)); + tcs.SetCanceled(); + queue.Process(); + Assert.That(job.Status, Is.EqualTo(JobStatus.Finished)); + + Assert.That(job.Result, Is.Null); + } + + private class DebugStopwatch : IStopwatch + { + public TimeSpan Elapsed { get; set; } + + public void Restart() + { + Elapsed = TimeSpan.Zero; + } + + public void Start() + { + Elapsed = TimeSpan.Zero; + } + } + + private class ImmediateJob : Job + { + public ImmediateJob() : base(0) + { + } + + protected override Task Process() + { + return Task.FromResult("honk!"); + } + } + + private class LongJob : Job + { + private readonly DebugStopwatch _stopwatch; + private readonly DebugStopwatch _stopwatchB; + + public LongJob(DebugStopwatch stopwatchA, DebugStopwatch stopwatchB, CancellationToken cancel = default) : + base(0.95, stopwatchA, cancel) + { + _stopwatch = stopwatchA; + _stopwatchB = stopwatchB; + } + + protected override async Task Process() + { + for (var i = 0; i < 12; i++) + { + // Increment time by 0.2 seconds. + IncrementTime(); + await SuspendIfOutOfTime(); + } + + return "foo!"; + } + + private void IncrementTime() + { + var diff = TimeSpan.FromSeconds(0.2); + _stopwatch.Elapsed += diff; + _stopwatchB.Elapsed += diff; + } + } + + private class LongJobQueue : JobQueue + { + public LongJobQueue(IStopwatch swB) : base(swB) + { + } + + public override double MaxTime => 0.9; + } + + private class WaitingJob : Job + { + private readonly Task _t; + + public WaitingJob(Task t) : base(0) + { + _t = t; + } + + protected override async Task Process() + { + await WaitAsyncTask(_t); + + return "oof!"; + } + } + } +} diff --git a/Resources/Maps/Pathfinding/simple.yml b/Resources/Maps/Pathfinding/simple.yml new file mode 100644 index 0000000000..cc6b54039f --- /dev/null +++ b/Resources/Maps/Pathfinding/simple.yml @@ -0,0 +1,277 @@ +meta: + format: 2 + name: DemoStation + author: Space-Wizards + postmapinit: true +tilemap: + 0: space + 1: floor_asteroid_coarse_sand0 + 2: floor_asteroid_coarse_sand1 + 3: floor_asteroid_coarse_sand2 + 4: floor_asteroid_coarse_sand_dug + 5: floor_asteroid_sand + 6: floor_asteroid_tile + 7: floor_carpet + 8: floor_dark + 9: floor_elevator_shaft + 10: floor_freezer + 11: floor_green_circuit + 12: floor_hull_center0 + 13: floor_hull_center1 + 14: floor_hull_center10 + 15: floor_hull_center11 + 16: floor_hull_center12 + 17: floor_hull_center13 + 18: floor_hull_center14 + 19: floor_hull_center15 + 20: floor_hull_center16 + 21: floor_hull_center17 + 22: floor_hull_center18 + 23: floor_hull_center19 + 24: floor_hull_center2 + 25: floor_hull_center20 + 26: floor_hull_center21 + 27: floor_hull_center22 + 28: floor_hull_center23 + 29: floor_hull_center24 + 30: floor_hull_center25 + 31: floor_hull_center26 + 32: floor_hull_center27 + 33: floor_hull_center28 + 34: floor_hull_center29 + 35: floor_hull_center3 + 36: floor_hull_center30 + 37: floor_hull_center31 + 38: floor_hull_center32 + 39: floor_hull_center33 + 40: floor_hull_center34 + 41: floor_hull_center35 + 42: floor_hull_center4 + 43: floor_hull_center5 + 44: floor_hull_center6 + 45: floor_hull_center7 + 46: floor_hull_center8 + 47: floor_hull_center9 + 48: floor_hydro + 49: floor_lino + 50: floor_mono + 51: floor_reinforced + 52: floor_rock_vault + 53: floor_showroom + 54: floor_steel + 55: floor_steel_dirty + 56: floor_techmaint + 57: floor_white + 58: plating + 59: underplating +grids: +- settings: + chunksize: 16 + tilesize: 1 + snapsize: 1 + chunks: + - ind: "-1,-1" + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAA== + - ind: "-1,0" + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + - ind: "0,-1" + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAAA== + - ind: "0,0" + tiles: NgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + - ind: "1,-1" + tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + - ind: "1,0" + tiles: NgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAANgAAADYAAAA2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== +entities: +- uid: 0 + components: + - parent: null + pos: 0.8203125,-0.4765625 + type: Transform + - index: 0 + type: MapGrid + - shapes: + - !type:PhysShapeGrid + grid: 0 + type: Collidable +- uid: 1 + type: wall_light + components: + - parent: 0 + pos: 24.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 2 + type: wall_light + components: + - parent: 0 + pos: -0.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 3 + type: wall_light + components: + - parent: 0 + pos: 24.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 4 + type: wall_light + components: + - parent: 0 + pos: 24.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 5 + type: wall_light + components: + - parent: 0 + pos: 4.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 6 + type: wall_light + components: + - parent: 0 + pos: 9.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 7 + type: wall_light + components: + - parent: 0 + pos: 14.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 8 + type: wall_light + components: + - parent: 0 + pos: 19.5,-0.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 9 + type: wall_light + components: + - parent: 0 + pos: 19.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 10 + type: wall_light + components: + - parent: 0 + pos: 19.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 11 + type: wall_light + components: + - parent: 0 + pos: 14.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 12 + type: wall_light + components: + - parent: 0 + pos: 14.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 13 + type: wall_light + components: + - parent: 0 + pos: 9.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 14 + type: wall_light + components: + - parent: 0 + pos: 9.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 15 + type: wall_light + components: + - parent: 0 + pos: 4.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 16 + type: wall_light + components: + - parent: 0 + pos: 4.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 17 + type: wall_light + components: + - parent: 0 + pos: -0.5,4.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +- uid: 18 + type: wall_light + components: + - parent: 0 + pos: -0.5,9.5 + rot: -1.5707963267948966 rad + type: Transform + - shapes: + - !type:PhysShapeAabb {} + type: Collidable +... diff --git a/Resources/Prototypes/Entities/Mobs/dummy_npcs.yml b/Resources/Prototypes/Entities/Mobs/dummy_npcs.yml new file mode 100644 index 0000000000..60dab85b6f --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/dummy_npcs.yml @@ -0,0 +1,10 @@ +- type: entity + save: false + name: Pathfinding Dummy + parent: BaseHumanMob_Content + id: HumanMob_PathDummy + description: A miserable pile of secrets + drawdepth: Mobs + components: + - type: AiController + logic: PathingDummy diff --git a/Resources/Prototypes/Entities/Mobs/human.yml b/Resources/Prototypes/Entities/Mobs/human.yml index 5683331652..903ca8f432 100644 --- a/Resources/Prototypes/Entities/Mobs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/human.yml @@ -1,12 +1,13 @@ +# Both humans and NPCs inherit from this. +# Anything human specific (e.g. UI, input) goes under HumanMob_Content - type: entity save: false name: Urist McHands - id: HumanMob_Content + id: BaseHumanMob_Content description: A miserable pile of secrets drawdepth: Mobs + abstract: true components: - - type: Mind - show_examine_info: true - type: Hands hands: - left @@ -110,39 +111,50 @@ layer: - Opaque - MobImpassable - - type: Input - context: "human" - type: Species Template: Human HeatResistance: 323 - type: BodyManager BaseTemplate: bodyTemplate.Humanoid BasePreset: bodyPreset.BasicHuman - - - type: StatusEffectsUI - - type: OverlayEffectsUI - type: HeatResistance - type: Damageable - - type: Eye - zoom: 0.5, 0.5 - - type: CameraRecoil - type: Appearance visuals: - type: SpeciesVisualizer2D - type: CombatMode - type: Teleportable - - type: Examiner - type: CharacterInfo - type: FootstepSound - type: HumanoidAppearance - - type: HumanInventoryController - type: Stunnable - type: AnimationPlayer - type: entity save: false name: Urist McHands + parent: BaseHumanMob_Content abstract: true + id: HumanMob_Content + description: A miserable pile of secrets + drawdepth: Mobs + components: + - type: Mind + show_examine_info: true + - type: Constructor + - type: Input + context: "human" + - type: StatusEffectsUI + - type: OverlayEffectsUI + - type: Eye + zoom: 0.5, 0.5 + - type: CameraRecoil + - type: Examiner + - type: HumanInventoryController + +- type: entity + save: false + name: Urist McHands id: HumanMob_Dummy description: A dummy human meant to be used in character setup components: diff --git a/Resources/Prototypes/Entities/Mobs/npcs.yml b/Resources/Prototypes/Entities/Mobs/npcs.yml new file mode 100644 index 0000000000..cd5cdbc22e --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/npcs.yml @@ -0,0 +1,20 @@ +- type: entity + save: false + name: Civilian + parent: BaseHumanMob_Content + id: HumanMob_Civilian + description: A miserable pile of secrets + drawdepth: Mobs + components: + - type: AiController + logic: Civilian + +- type: entity + save: false + name: Spirate + parent: BaseHumanMob_Content + id: HumanMob_Spirate + description: Yarr + components: + - type: AiController + logic: Spirate