Add utility AI (#806)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
metalgearsloth
2020-06-18 22:52:44 +10:00
committed by GitHub
parent 9b8cedf6c6
commit 5391d3c72a
211 changed files with 10335 additions and 527 deletions

View File

@@ -0,0 +1,55 @@
using System;
namespace Content.Server.AI.Operators
{
public abstract class AiOperator
{
private bool _hasStartup = false;
private bool _hasShutdown = false;
/// <summary>
/// Called once when the AiLogicProcessor starts this action
/// </summary>
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;
}
/// <summary>
/// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails.
/// </summary>
public virtual void Shutdown(Outcome outcome)
{
if (_hasShutdown)
{
throw new InvalidOperationException("AiOperator has already shutdown");
}
_hasShutdown = true;
}
/// <summary>
/// Called every tick for the AI
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public abstract Outcome Execute(float frameTime);
}
public enum Outcome
{
Success,
Continuing,
Failed,
}
}

View File

@@ -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<AiControllerComponent>().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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _target.Transform.GridPosition, _target.Uid);
_elapsedTime += frameTime;
return Outcome.Continuing;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Close the last EntityStorage we opened
/// This will also update the State for it (which a regular InteractWith won't do)
/// </summary>
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<LastOpenedStorageState>().GetValue();
return _target != null;
}
public override void Shutdown(Outcome outcome)
{
base.Shutdown(outcome);
var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
blackboard?.GetState<LastOpenedStorageState>().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;
}
}
}

View File

@@ -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;
}
/// <summary>
/// Requires EquipEntityOperator to put it in the active hand first
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public override Outcome Execute(float frameTime)
{
if (!_owner.TryGetComponent(out HandsComponent handsComponent))
{
return Outcome.Failed;
}
return handsComponent.Drop(_entity) ? Outcome.Success : Outcome.Failed;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// A Generic interacter; if you need to check stuff then make your own
/// </summary>
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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _useTarget.Transform.GridPosition, _useTarget.Uid);
return Outcome.Success;
}
}
}

View File

@@ -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
{
/// <summary>
/// If the target is in EntityStorage will open its parent container
/// </summary>
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<LastOpenedStorageState>().SetValue(container.Owner);
return Outcome.Success;
}
}
}

View File

@@ -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<ItemComponent>() ||
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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.Interaction(_owner, _target);
return Outcome.Success;
}
}
}

View File

@@ -0,0 +1,49 @@
using Content.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// Will find the item in storage, put it in an active hand, then use it
/// </summary>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Invoked every time we move across a tile
/// </summary>
public event Action MovedATile;
/// <summary>
/// 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.
///
/// </summary>
public float PathfindingProximity { get; set; } = 1.42f;
protected Queue<TileRef> Route = new Queue<TileRef>();
/// <summary>
/// The final spot we're trying to get to
/// </summary>
protected GridCoordinates TargetGrid;
/// <summary>
/// As the pathfinder is tilebased we'll move to each tile's grid.
/// </summary>
protected GridCoordinates NextGrid;
private const float TileTolerance = 0.2f;
// Stuck checkers
/// <summary>
/// How long we're stuck in general before trying to unstuck
/// </summary>
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<Queue<TileRef>> RouteJob;
private IMapManager _mapManager;
private PathfindingSystem _pathfinder;
private AiControllerComponent _controller;
// Input
protected IEntity Owner;
protected void Setup(IEntity owner)
{
Owner = owner;
_mapManager = IoCManager.Resolve<IMapManager>();
_pathfinder = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
if (!Owner.TryGetComponent(out AiControllerComponent controllerComponent))
{
throw new InvalidOperationException();
}
_controller = controllerComponent;
}
protected void NextTile()
{
MovedATile?.Invoke();
}
/// <summary>
/// Will move the AI towards the next position
/// </summary>
/// <returns>true if movement to be done</returns>
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;
}
/// <summary>
/// Will try and get around obstacles if stuck
/// </summary>
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<IRobustRandom>().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<IRobustRandom>();
_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;
}
/// <summary>
/// Tells us we don't need to keep moving and resets everything
/// </summary>
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<TileRef>();
// 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
}
}

View File

@@ -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<IMapManager>();
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<TileRef>(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;
}
}
}

View File

@@ -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<IMapManager>();
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;
}
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(owner, target),
new OpenStorageOperator(owner, target),
new PickupEntityOperator(owner, target),
});
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace Content.Server.AI.Operators.Sequences
{
/// <summary>
/// Sequential chain of operators
/// Saves having to duplicate stuff like MoveTo and PickUp everywhere
/// </summary>
public abstract class SequenceOperator : AiOperator
{
public Queue<AiOperator> 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();
}
}
}
}