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:
55
Content.Server/AI/Operators/AiOperator.cs
Normal file
55
Content.Server/AI/Operators/AiOperator.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
Content.Server/AI/Operators/Generic/WaitOperator.cs
Normal file
24
Content.Server/AI/Operators/Generic/WaitOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
Normal file
32
Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
Normal file
38
Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
Normal file
55
Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
295
Content.Server/AI/Operators/Movement/BaseMover.cs
Normal file
295
Content.Server/AI/Operators/Movement/BaseMover.cs
Normal 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
|
||||
}
|
||||
}
|
||||
142
Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
Normal file
142
Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
Normal file
94
Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Content.Server/AI/Operators/Sequences/SequenceOperator.cs
Normal file
44
Content.Server/AI/Operators/Sequences/SequenceOperator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user