Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs
T
NotAlwaysTrue e7e444e9b2 Fixed multiple LINQ using shared resources and cause crashes
Added an null check in AIObjectiveManager.cs to avoid accessing removed resources
Use shuffledGaps instead of gapList to ensure update order requirement(already in master)
Updated parallelism count
2026-01-09 18:09:49 +08:00

842 lines
37 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking; // used by the server
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveManager
{
public enum ObjectiveType
{
None = 0,
Order = 1,
Objective = 2,
MinValue = 0,
MaxValue = 2
}
/// <summary>
/// Highest possible priority for any objective. Used in certain cases where the character needs to react immediately to survive,
/// such as finding a suit when under pressure or getting out of a burning room.
/// </summary>
public const float MaxObjectivePriority = 100;
/// <summary>
/// Priority of objectives such as finding safety, rescuing someone in a critical state or defending against an attacker
/// (= objectives that are critical for saving the character's or someone else's life)
/// </summary>
public const float EmergencyObjectivePriority = 90;
/// <summary>
/// Maximum priority of an order given to the character (forced order, or the leftmost order in the crew list)
/// </summary>
public const float HighestOrderPriority = 70;
/// <summary>
/// Minimum priority of an order given to the character (rightmost order in the crew list)
/// </summary>
public const float LowestOrderPriority = 60;
/// <summary>
/// Objectives with a priority equal to or higher than this make the character run.
/// </summary>
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
/// <summary>
/// Excluding the current order.
/// </summary>
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private readonly Character character;
public HumanAIController HumanAIController => character.AIController as HumanAIController;
private float _waitTimer;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
/// </summary>
public float WaitTimer
{
get { return _waitTimer; }
set
{
_waitTimer = IsAllowedToWait() ? value : 0;
}
}
public List<Order> CurrentOrders { get; } = new List<Order>();
/// <summary>
/// The AIObjective in <see cref="CurrentOrders"/> with the highest <see cref="AIObjective.Priority"/>
/// </summary>
public AIObjective CurrentOrder
{
get
{
return ForcedOrder ?? currentOrder;
}
private set
{
currentOrder = value;
}
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
/// <summary>
/// Includes orders.
/// </summary>
public AIObjective CurrentObjective { get; private set; }
public AIObjectiveManager(Character character)
{
this.character = character;
CreateAutonomousObjectives();
}
public void AddObjective(AIObjective objective)
{
AddObjective<AIObjective>(objective);
}
public void AddObjective<T>(T objective) where T : AIObjective
{
var result = GameMain.LuaCs.Hook.Call<bool?>("AI.addObjective", this, objective);
if (result != null && result.Value) return;
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
// Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective.
// We need to get the highest type.
var type = objective.GetType();
if (objective.AllowMultipleInstances)
{
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
{
Objectives.Remove(existingObjective);
}
}
else
{
Objectives.RemoveAll(o => o?.GetType() == type);
}
Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public bool FailedAutonomousObjectives { get; private set; }
public bool FailedToFindDivingGearForDepth;
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
{
humanAi.UnreachableHulls.Clear();
humanAi.IgnoredItems.Clear();
}
}
public void CreateAutonomousObjectives()
{
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to create autonomous orders for a dead character");
#else
return;
#endif
}
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
DelayedObjectives.Clear();
Objectives.Clear();
FailedAutonomousObjectives = false;
AddObjective(new AIObjectiveFindSafety(character, this));
var newIdleObjective = new AIObjectiveIdle(character, this);
if (prevIdleObjective != null)
{
newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
newIdleObjective.Behavior = prevIdleObjective.Behavior;
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
if (character.Info?.Job != null)
{
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier] ?? throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'");
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
{
continue;
}
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
else
{
string warningMsg = character.Info == null ?
$"The character {character.DisplayName} has been set to use human ai, but has no {nameof(CharacterInfo)}. This may cause issues with the AI. Consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config." :
$"The character {character.DisplayName} has been set to use human ai, but has no job. This may cause issues with the AI. Consider configuring some jobs for the character type.";
DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage);
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
{
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null && GameMain.GameSession.GameMode is not TestGameMode) { return; }
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
}, delay);
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
private AIObjective GetCurrentObjective()
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
CurrentObjective = currentObjectiveIsOrder ? CurrentOrder : firstObjective;
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
if (CurrentObjective != null)
{
CurrentObjective.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character,
new Character.ObjectiveManagerStateEventData(currentObjectiveIsOrder ? ObjectiveType.Order : ObjectiveType.Objective));
}
return CurrentObjective;
}
/// <summary>
/// Returns the highest priority of the current objective and its subobjectives.
/// </summary>
public float GetCurrentPriority()
{
if (CurrentObjective == null) { return 0; }
float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0;
return Math.Max(CurrentObjective.Priority, subObjectivePriority);
}
public void UpdateObjectives(float deltaTime)
{
UpdateOrderObjective(ForcedOrder);
if (CurrentOrders.Any())
{
foreach (var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
}
}
void UpdateOrderObjective(AIObjective orderObjective)
{
if (orderObjective == null) { return; }
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
orderObjective.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
return;
}
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightBlue);
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
FailedAutonomousObjectives = true;
}
else
{
objective.Update(deltaTime);
}
}
GetCurrentObjective();
}
public void SortObjectives()
{
ForcedOrder?.CalculatePriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
if (CurrentOrders.Count <= i) { break; }
var orderObjective = CurrentOrders[i].Objective;
if (orderObjective == null) { continue; }
orderObjective.CalculatePriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
{
orderWithHighestPriority = orderObjective;
highestPriority = orderObjective.Priority;
}
}
CurrentOrder = orderWithHighestPriority;
for (int i = Objectives.Count - 1; i >= 0; i--)
{
Objectives[i].CalculatePriority();
}
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
{
if (WaitTimer <= 0)
{
CurrentObjective?.TryComplete(deltaTime);
}
else
{
character.AIController.SteeringManager.Reset();
}
}
public void SetForcedOrder(AIObjective objective)
{
ForcedOrder = objective;
}
public void ClearForcedOrder()
{
ForcedOrder = null;
SortObjectives();
}
public void SetOrder(Order order, bool speak)
{
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to set an order for a dead character");
#else
return;
#endif
}
ClearIgnored();
if (order == null || order.IsDismissal)
{
if (order.Option != Identifier.Empty)
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(order.Option)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(order.Option));
CurrentOrders.Remove(dismissedOrderInfo);
}
}
else
{
CurrentOrders.Clear();
}
}
// Make sure the order priorities reflect those set by the player
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
if (CurrentOrders.Count <= i) { break; }
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order))
{
CurrentOrders.RemoveAt(i);
continue;
}
if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo)
{
int currentPriority = currentOrderInfo.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
{
CurrentOrders[i] = currentOrder.WithManualPriority(currentPriority);
}
}
else
{
CurrentOrders.RemoveAt(i);
}
}
//reset this here so the bots can retry finding a better suit if it's needed for the new order
FailedToFindDivingGearForDepth = false;
var newCurrentObjective = CreateObjective(order);
if (newCurrentObjective != null)
{
newCurrentObjective.Abandoned += () => DismissSelf(order);
CurrentOrders.Add(order.WithObjective(newCurrentObjective));
}
else if (!order.IsDismissal)
{
DebugConsole.ThrowError($"Failed to create an objective for the order: {order.Name}");
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else if (newCurrentObjective != null)
{
if (speak && character.IsOnPlayerTeam)
{
LocalizedString msg = newCurrentObjective.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg.Value, delay: 1.0f);
}
}
}
/// <summary>
/// Creates an AI objective based on the order. Note that the method can return null in the case of e.g. Dismissal orders or orders that erroneously target something non-interactable.
/// </summary>
public AIObjective CreateObjective(Order order, float priorityModifier = 1)
{
if (order == null || order.IsDismissal) { return null; }
AIObjective newObjective;
switch (order.Identifier.Value.ToLowerInvariant())
{
case "follow":
if (order.OrderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
IsFollowOrder = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = true,
IsWaitOrder = true,
DebugLogWhenFails = false,
SpeakIfFails = false,
CloseEnough = 100
};
break;
case "return":
newObjective = new AIObjectiveReturn(character, order.OrderGiver, this, priorityModifier: priorityModifier);
newObjective.Completed += () => DismissSelf(order);
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull);
break;
case "chargebatteries":
newObjective = new AIObjectiveChargeBatteries(character, this, order.Option, priorityModifier);
break;
case "rescue":
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
case "repairmechanical":
case "repairelectrical":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
{
RelevantSkill = order.AppropriateSkill,
};
break;
case "pumpwater":
if (order.TargetItemComponent is Pump targetPump)
{
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier)
{
Override = order.OrderGiver is { IsCommanding: true }
};
newObjective.Completed += () => DismissSelf(order);
}
else
{
newObjective = new AIObjectivePumpWater(character, this, order.Option, priorityModifier: priorityModifier);
}
break;
case "extinguishfires":
newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
break;
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break;
case "assaultenemy":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier)
{
TargetCharactersInOtherSubs = true
};
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
break;
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier)
{
Override = !character.IsDismissed,
completionCondition = () =>
{
if (float.TryParse(order.Option.Value, out float pct))
{
var targetRatio = Math.Clamp(pct, 0f, 1f);
var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio;
return Math.Abs(targetRatio - currentRatio) < 0.05f;
}
return true;
}
};
break;
case "getitem":
newObjective = new AIObjectiveGetItem(character, order.TargetEntity as Item ?? order.TargetItemComponent?.Item, this, false, priorityModifier: priorityModifier)
{
MustBeSpecificItem = true
};
break;
case "cleanupitems":
if (order.TargetEntity is Item targetItem)
{
if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
{
// Target all items inside the container
newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier);
}
else
{
newObjective = new AIObjectiveCleanupItems(character, this, targetItem, priorityModifier);
}
}
else
{
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
}
break;
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "findthieves":
newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true,
RequireNonEmpty = false
};
break;
case "findweapon":
AIObjectivePrepare prepareObjective;
if (order.TargetEntity is Item tItem)
{
prepareObjective = new AIObjectivePrepare(character, this, targetItem: tItem);
}
else
{
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false,
RequireNonEmpty = true
};
}
prepareObjective.KeepActiveWhenReady = false;
prepareObjective.Equip = true;
newObjective = prepareObjective;
newObjective.Completed += () => DismissSelf(order);
break;
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier);
break;
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
case "inspectnoises":
newObjective = new AIObjectiveInspectNoises(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
if (newObjective.Abandon) { return null; }
break;
}
if (newObjective != null)
{
newObjective.Identifier = order.Identifier;
}
newObjective.IgnoreAtOutpost = order.IgnoreAtOutpost;
return newObjective;
}
/// <summary>
/// Sets the order as dismissed, and enables the option to reissue the order on the crew list.
/// Note that this is not the same thing as just removing the order entirely!
/// </summary>
/// <param name="order"></param>
private void DismissSelf(Order order)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option));
if (currentOrder == null)
{
#if DEBUG
DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found");
#endif
return;
}
Order dismissOrder = currentOrder.GetDismissal();
#if CLIENT
if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
{
character.SetOrder(dismissOrder, isNewOrder: true, speak: false);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
SetOrder(dismissOrder, speak: false);
#endif
}
private bool IsAllowedToWait()
{
if (!character.IsOnPlayerTeam) { return false; }
if (HasOrders()) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (humanAI.UnsafeHulls.Contains(character.CurrentHull)) { return false; }
}
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
/// <summary>
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
/// <summary>
/// Checks if any objectives or orders are of the specified type. Regardless of whether the objective is active or inactive.
/// </summary>
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
/// <summary>
/// Returns the current objective or its currently active subobjective (first in chain), regadless of the type.
/// Note: Not recursive, and thus doesn't work for deeper hierarchy!
/// For seeking objectives of specific type and in a deep hierarchy, use <see cref="GetLastActiveObjective{T}"/> or with looping objectives <see cref="GetFirstActiveObjective{T}"/>
/// </summary>
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Return the first order whose objective is of the given type. Can return null.
/// </summary>
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
/// <summary>
/// Returns the last active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
/// </summary>
/// <returns>
/// The last active objective of the specified type if found.
/// </returns>
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns the first active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that sort their subobjectives by priority, such as those that inherit <see cref="AIObjectiveLoop"/>.
/// </summary>
/// <returns>
/// The first active objective of the specified type if found.
/// </returns>
public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
{
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
}
/// <summary>
/// Is the current objective or any of its subobjectives of the given type?
/// Useful for checking whether the bot has a certain type of objective active in the hierarchy.
/// </summary>
/// <returns>False for objectives and orders that are not currently active.</returns>
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public bool IsOrder(AIObjective objective)
{
if (objective == ForcedOrder) { return true; }
foreach (var order in CurrentOrders)
{
if (order.Objective == objective) { return true; }
}
return false;
}
public bool HasOrders()
{
return ForcedOrder != null || CurrentOrders.Any();
}
public bool HasOrder<T>(Func<T, bool> predicate = null) where T : AIObjective =>
ForcedOrder is T forcedOrder && (predicate == null || predicate(forcedOrder)) ||
CurrentOrders.Any(o => o.Objective is T order && (predicate == null || predicate(order)));
public float GetOrderPriority(AIObjective objective)
{
if (objective == ForcedOrder)
{
return HighestOrderPriority;
}
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
if (currentOrder.Objective == null)
{
return HighestOrderPriority;
}
else if (currentOrder.ManualPriority > 0)
{
if (objective.ForceHighestPriority)
{
return HighestOrderPriority;
}
if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any())
{
return HighestOrderPriority;
}
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority - 1, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
}
#if DEBUG
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
#endif
return 0;
}
public Order GetCurrentOrderInfo()
{
if (currentOrder == null) { return null; }
return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder);
}
}
}