4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
51 public List<AIObjective>
Objectives {
get;
private set; } =
new List<AIObjective>();
57 private float _waitTimer;
63 get {
return _waitTimer; }
66 _waitTimer = IsAllowedToWait() ? value : 0;
95 this.character = character;
101 AddObjective<AIObjective>(objective);
108 if (result !=
null && result.Value)
return;
110 if (objective ==
null)
113 DebugConsole.ThrowError(
"Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
119 var type = objective.GetType();
120 if (objective.AllowMultipleInstances)
122 if (
Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
129 Objectives.RemoveAll(o => o.GetType() == type);
134 public Dictionary<AIObjective, CoroutineHandle>
DelayedObjectives {
get;
private set; } =
new Dictionary<AIObjective, CoroutineHandle>();
139 private void ClearIgnored()
143 humanAi.UnreachableHulls.Clear();
144 humanAi.IgnoredItems.Clear();
150 if (character.IsDead)
153 DebugConsole.ThrowError(
"Attempted to create autonomous orders for a dead character");
161 CoroutineManager.StopCoroutines(delayedObjective.Value);
164 var prevIdleObjective = GetObjective<AIObjectiveIdle>();
171 if (prevIdleObjective !=
null)
173 newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
174 newIdleObjective.Behavior = prevIdleObjective.Behavior;
175 prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
180 if (character.Info?.Job !=
null)
182 foreach (var autonomousObjective
in character.Info.Job.Prefab.AutonomousObjectives)
184 var orderPrefab =
OrderPrefab.
Prefabs[autonomousObjective.Identifier] ??
throw new Exception($
"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'");
186 if (orderPrefab.MustSetTarget)
188 item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub:
false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
190 var order =
new Order(orderPrefab, autonomousObjective.Option, item ?? character.
CurrentHull as
Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
191 if (order ==
null) {
continue; }
192 if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
204 var objective =
CreateObjective(order, autonomousObjective.PriorityModifier);
205 if (objective !=
null && objective.CanBeCompleted)
214 string warningMsg = character.Info ==
null ?
215 $
"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." :
216 $
"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.";
217 DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage);
220 _waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
225 if (objective ==
null)
228 DebugConsole.ThrowError($
"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
234 CoroutineManager.StopCoroutines(coroutine);
237 coroutine = CoroutineManager.Invoke(() =>
253 var firstObjective =
Objectives.FirstOrDefault();
293 var orderObjective = order.Objective;
294 UpdateOrderObjective(orderObjective);
298 void UpdateOrderObjective(
AIObjective orderObjective)
300 if (orderObjective ==
null) {
return; }
305 DebugConsole.NewMessage($
"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
308 orderObjective.
Update(deltaTime);
319 if (objective.IsCompleted)
322 DebugConsole.NewMessage($
"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightBlue);
326 else if (!objective.CanBeCompleted)
329 DebugConsole.NewMessage($
"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
336 objective.Update(deltaTime);
339 GetCurrentObjective();
346 float highestPriority = 0;
351 if (orderObjective ==
null) {
continue; }
352 orderObjective.CalculatePriority();
353 if (orderWithHighestPriority ==
null || orderObjective.
Priority > highestPriority)
355 orderWithHighestPriority = orderObjective;
356 highestPriority = orderObjective.
Priority;
360 for (
int i =
Objectives.Count - 1; i >= 0; i--)
366 Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
379 character.AIController.SteeringManager.Reset();
396 if (character.IsDead)
399 DebugConsole.ThrowError(
"Attempted to set an order for a dead character");
408 if (order.
Option != Identifier.Empty)
427 if (currentOrder.Objective ==
null || currentOrder.MatchesOrder(order))
432 if (character.GetCurrentOrder(currentOrder) is
Order currentOrderInfo)
434 int currentPriority = currentOrderInfo.ManualPriority;
435 if (currentOrder.ManualPriority != currentPriority)
437 CurrentOrders[i] = currentOrder.WithManualPriority(currentPriority);
450 if (newCurrentObjective !=
null)
452 newCurrentObjective.Abandoned += () => DismissSelf(order);
460 else if (newCurrentObjective !=
null)
462 if (speak && character.IsOnPlayerTeam)
464 LocalizedString msg = newCurrentObjective.IsAllowed ? TextManager.Get(
"DialogAffirmative") : TextManager.Get(
"DialogNegative");
465 character.Speak(msg.
Value, delay: 1.0f);
472 if (order ==
null || order.
IsDismissal) {
return null; }
474 switch (order.
Identifier.Value.ToLowerInvariant())
477 if (order.
OrderGiver ==
null) {
return null; }
480 CloseEnough = Rand.Range(80f, 100f),
482 ExtraDistanceOutsideSub = 100,
483 ExtraDistanceWhileSwimming = 100,
484 AllowGoingOutside =
true,
485 IgnoreIfTargetDead =
true,
486 IsFollowOrder =
true,
487 Mimic = character.IsOnPlayerTeam,
488 DialogueIdentifier =
"dialogcannotreachplace".ToIdentifier()
494 AllowGoingOutside =
true,
496 DebugLogWhenFails =
false,
497 SpeakIfFails =
false,
503 newObjective.
Completed += () => DismissSelf(order);
508 case "chargebatteries":
514 case "repairsystems":
515 case "repairmechanical":
516 case "repairelectrical":
528 Override = order.
OrderGiver is { IsCommanding:
true }
530 newObjective.
Completed += () => DismissSelf(order);
537 case "extinguishfires":
540 case "fightintruders":
546 TargetCharactersInOtherSubs =
true
551 if (steering !=
null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
565 Override = !character.IsDismissed,
566 completionCondition = () =>
568 if (
float.TryParse(order.
Option.Value, out
float pct))
570 var targetRatio = Math.Clamp(pct, 0f, 1f);
572 return Math.Abs(targetRatio - currentRatio) < 0.05f;
581 MustBeSpecificItem =
true
587 if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory ==
null && targetItem.OwnInventory !=
null)
590 newObjective =
new AIObjectiveCleanupItems(character,
this, targetItem.OwnInventory.AllItems, priorityModifier);
602 case "escapehandcuffs":
608 case "prepareforexpedition":
611 KeepActiveWhenReady =
true,
612 CheckInventory =
true,
615 RequireNonEmpty =
false
628 CheckInventory =
false,
629 EvaluateCombatPriority =
true,
630 FindAllItems =
false,
631 RequireNonEmpty =
true
635 prepareObjective.
Equip =
true;
636 newObjective = prepareObjective;
637 newObjective.
Completed += () => DismissSelf(order);
642 case "deconstructitems":
645 case "inspectnoises":
658 if (newObjective.
Abandon) {
return null; }
661 if (newObjective !=
null)
674 private void DismissSelf(
Order order)
677 if (currentOrder ==
null)
680 DebugConsole.ThrowError(
"Tried to self-dismiss an order, but no matching current order was found");
685 Order dismissOrder = currentOrder.GetDismissal();
687 if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
689 character.SetOrder(dismissOrder, isNewOrder:
true, speak:
false);
692 GameMain.Server?.SendOrderChatMessage(
new OrderChatMessage(dismissOrder, character, character));
693 SetOrder(dismissOrder, speak:
false);
697 private bool IsAllowedToWait()
699 if (!character.IsOnPlayerTeam) {
return false; }
702 if (character.AnimController.InWater) {
return false; }
703 if (character.IsClimbing) {
return false; }
706 if (humanAI.UnsafeHulls.Contains(character.CurrentHull)) {
return false; }
708 if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) {
return false; }
746 =>
CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
756 =>
CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
761 public IEnumerable<T> GetActiveObjectives<T>() where T :
AIObjective
774 if (order.Objective == objective) {
return true; }
785 ForcedOrder is T forcedOrder && (predicate == null || predicate(forcedOrder)) ||
786 CurrentOrders.Any(o => o.Objective is T order && (predicate == null || predicate(order)));
794 var currentOrder =
CurrentOrders.FirstOrDefault(o => o.Objective == objective);
795 if (currentOrder.Objective ==
null)
799 else if (currentOrder.ManualPriority > 0)
801 if (objective.ForceHighestPriority)
805 if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any())
812 DebugConsole.AddWarning(
"Error in order priority: shouldn't return 0!");
819 if (currentOrder ==
null) {
return null; }
AIController(Character c)
IEnumerable< AIObjective > SubObjectives
float CalculatePriority()
Call this only when the priority needs to be recalculated. Use the cached Priority property when you ...
virtual bool CanBeCompleted
virtual void Update(float deltaTime)
IEnumerable< AIObjective > GetSubObjectivesRecursive(bool includingSelf=false)
float Priority
Final priority value after all calculations.
void TryComplete(float deltaTime)
Makes the character act according to the objective, or according to any subobjectives that need to be...
Action Completed
A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (...
virtual void OnSelected()
virtual void OnDeselected()
abstract Identifier Identifier
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
void SetOrder(Order order, bool speak)
bool IsCurrentOrder< T >()
Only checks the current order. Deprecated, use pattern matching instead.
AIObjective GetActiveObjective()
bool FailedAutonomousObjectives
const float RunPriority
Objectives with a priority equal to or higher than this make the character run.
Dictionary< AIObjective, CoroutineHandle > DelayedObjectives
bool HasActiveObjective< T >()
AIObjective CreateObjective(Order order, float priorityModifier=1)
float GetCurrentPriority()
Returns the highest priority of the current objective and its subobjectives.
AIObjectiveManager(Character character)
List< AIObjective > Objectives
Excluding the current order.
void SetForcedOrder(AIObjective objective)
void AddObjective(AIObjective objective)
HumanAIController HumanAIController
void AddObjective< T >(T objective)
Order GetCurrentOrderInfo()
bool FailedToFindDivingGearForDepth
const float LowestOrderPriority
Maximum priority of an order given to the character (rightmost order in the crew list)
void UpdateObjectives(float deltaTime)
AIObjective CurrentObjective
Includes orders.
AIObjective?? CurrentOrder
The AIObjective in CurrentOrders with the highest AIObjective.Priority
List< Order > CurrentOrders
void DoCurrentObjective(float deltaTime)
Order GetOrder(AIObjective objective)
Return the first order with the specified objective. Can return null.
float GetOrderPriority(AIObjective objective)
void CreateAutonomousObjectives()
bool IsOrder(AIObjective objective)
bool HasOrder< T >(Func< T, bool > predicate=null)
const float MaxObjectivePriority
Highest possible priority for any objective. Used in certain cases where the character needs to react...
float? WaitTimer
When set above zero, the character will stand still doing nothing until the timer runs out....
const float HighestOrderPriority
Maximum priority of an order given to the character (forced order, or the leftmost order in the crew ...
bool IsCommanding
Is the character player or does it have an active ship command manager (an AI controlled sub)?...
Stores information about the Character that is needed between rounds in the menu etc....
static int HighestManualOrderPriority
static GameSession?? GameSession
static NetworkMember NetworkMember
static int CountBotsInTheCrew(Character character, Func< HumanAIController, bool > predicate=null)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
object Call(string name, params object[] args)
readonly Entity TargetEntity
readonly Character OrderGiver
ISpatialEntity??? TargetSpatialEntity
Note this property doesn't return the follow target of the Follow objective, as expected!
readonly Identifier Option
readonly bool UseController
Identifier AppropriateSkill
readonly Controller ConnectedController
ref readonly ImmutableArray< Identifier > RequireItems
ImmutableArray< Identifier > GetTargetItems(Identifier option=default)
Order WithObjective(AIObjective objective)
readonly ItemComponent TargetItemComponent
static readonly PrefabCollection< OrderPrefab > Prefabs
IEnumerable< Submarine > DockedTo
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.