Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using Microsoft.Xna.Framework;
6 using System.Collections.Immutable;
7 
8 namespace Barotrauma
9 {
10  abstract partial class AIObjective
11  {
12  public virtual float Devotion => AIObjectiveManager.baseDevotion;
13 
14  public abstract Identifier Identifier { get; set; }
15  public virtual string DebugTag => Identifier.Value;
16  public virtual bool ForceRun => false;
17  public virtual bool IgnoreUnsafeHulls => false;
18  public virtual bool AbandonWhenCannotCompleteSubObjectives => true;
22  public virtual bool AllowSubObjectiveSorting => false;
23  public virtual bool PrioritizeIfSubObjectivesActive => false;
24 
28  public virtual bool AllowMultipleInstances => false;
29 
34  protected virtual bool ConcurrentObjectives => false;
35  public virtual bool KeepDivingGearOn => false;
36  public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
37 
41  public virtual bool AllowAutomaticItemUnequipping => false;
42 
43  // These booleans are used for defining whether the objective is allowed in different contexts. E.g. AllowOutsideSubmarine needs to be true or the objective cannot be active when the bot is swimming outside.
44  protected virtual bool AllowOutsideSubmarine => false;
49  protected virtual bool AllowInFriendlySubs => false;
50  protected virtual bool AllowInAnySub => false;
51  protected virtual bool AllowWhileHandcuffed => true;
52 
58  protected virtual bool AbandonIfDisallowed => true;
59 
60  public virtual bool CanBeCompleted => !Abandon;
61 
62  protected virtual float MaxDevotion => 10;
63 
72 
73  protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
74  private float _cumulatedDevotion;
75  protected float CumulatedDevotion
76  {
77  get { return _cumulatedDevotion; }
78  set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
79  }
80 
84  public float Priority { get; set; }
85  public float BasePriority { get; set; }
86  public float PriorityModifier { get; private set; } = 1;
87 
88  private float resetPriorityTimer;
89  private readonly float resetPriorityTime = 1;
90  private bool _forceHighestPriority;
91  // For forcing the highest priority temporarily. Will reset automatically after one second, unless kept alive by something.
93  {
94  get { return _forceHighestPriority; }
95  set
96  {
97  if (_forceHighestPriority == value) { return; }
98  _forceHighestPriority = value;
99  if (_forceHighestPriority)
100  {
101  resetPriorityTimer = resetPriorityTime;
102  }
103  }
104  }
105 
106  // For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
107  // The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
108  public bool ForceWalk { get; set; }
109 
110  public bool IgnoreAtOutpost { get; set; }
111 
112  public readonly Character character;
114  public readonly Identifier Option;
115 
116  private bool _abandon;
117  public bool Abandon
118  {
119  get { return _abandon; }
120  set
121  {
122  _abandon = value;
123  if (_abandon)
124  {
125  OnAbandon();
126  }
127  }
128  }
129 
130  public IEnumerable<AIObjective> SubObjectives => subObjectives;
131 
132  public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
133 
134  private readonly List<AIObjective> all = new List<AIObjective>();
135 
136  public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
137  {
138  all.Clear();
139  if (includingSelf)
140  {
141  all.Add(this);
142  }
143  foreach (var subObjective in subObjectives)
144  {
145  all.AddRange(subObjective.GetSubObjectivesRecursive(true));
146  }
147  return all;
148  }
149 
153  public Func<AIObjective, bool> AbortCondition;
154 
158  public event Action Completed;
162  public event Action Abandoned;
166  public event Action Selected;
170  public event Action Deselected;
171 
175 
177  {
178  var subObjective = CurrentSubObjective;
179  return subObjective == null ? this : subObjective.GetActiveObjective();
180  }
181 
182  public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default)
183  {
184  this.objectiveManager = objectiveManager;
185  this.character = character;
186  Option = option;
187  PriorityModifier = priorityModifier;
188  }
189 
193  public void TryComplete(float deltaTime)
194  {
195  if (isCompleted) { return; }
196  if (CheckState()) { return; }
197  // Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
198  for (int i = 0; i < subObjectives.Count; i++)
199  {
200  subObjectives[i].TryComplete(deltaTime);
201  if (!ConcurrentObjectives) { return; }
202  }
203  Act(deltaTime);
204  }
205 
206  public void AddSubObjective(AIObjective objective, bool addFirst = false)
207  {
208  var type = objective.GetType();
209  objective.SourceObjective = this;
210  subObjectives.RemoveAll(o => o.GetType() == type);
211  if (addFirst)
212  {
213  subObjectives.Insert(0, objective);
214  }
215  else
216  {
217  subObjectives.Add(objective);
218  }
219  }
220 
221  public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
222  {
223  if (objective != null)
224  {
225  if (subObjectives.Contains(objective))
226  {
227  subObjectives.Remove(objective);
228  }
229  objective = null;
230  }
231  }
232 
233  public void SortSubObjectives()
234  {
235  if (!AllowSubObjectiveSorting) { return; }
236  if (subObjectives.None()) { return; }
237  var previousSubObjective = subObjectives.First();
238  subObjectives.ForEach(so => so.GetPriority());
239  subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
241  {
242  subObjectives.ForEach(so => so.SortSubObjectives());
243  }
244  else
245  {
246  var currentSubObjective = subObjectives.First();
247  if (previousSubObjective != currentSubObjective)
248  {
249  previousSubObjective.OnDeselected();
250  currentSubObjective.OnSelected();
251  }
252  currentSubObjective.SortSubObjectives();
253  }
254  }
255 
256  public bool IsAllowed
257  {
258  get
259  {
260  if (!AllowWhileHandcuffed && character.LockHands) { return false; }
261  if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
262  // Evaluate ignored at outpost first, because it has higher priority than AllowInAnySub or AllowInFriendlySubs.
263  if (IsIgnoredAtOutpost()) { return false; }
264  if (AllowInAnySub) { return true; }
265  if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
267  }
268  }
269 
274  public bool IsIgnoredAtOutpost()
275  {
276  if (!IgnoreAtOutpost) { return false; }
277  if (!Level.IsLoadedFriendlyOutpost) { return false; }
279  if (character.Submarine?.Info == null) { return false; }
281  }
282 
283  protected void HandleDisallowed()
284  {
285  Priority = 0;
287  {
288  // Never abandon objectives inside a friendly outpost, because otherwise we'd have to reassign most orders every round.
289  Abandon = true;
290  }
291  }
292 
293  protected virtual float GetPriority()
294  {
295  if (!IsAllowed)
296  {
298  return Priority;
299  }
300  if (objectiveManager.IsOrder(this))
301  {
303  }
304  else
305  {
307  }
308  return Priority;
309  }
310 
314  public float CalculatePriority()
315  {
316  ForceWalk = false;
317  Priority = GetPriority();
318  ForceHighestPriority = false;
319  return Priority;
320  }
321 
331  public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
332  {
333  float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y);
334  yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0;
335  float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist;
336  float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance));
337  return
338  factorAtMinDistance > factorAtMaxDistance ?
339  MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) :
340  MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance);
341  }
342 
352  protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
353  {
354  return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance);
355  }
356 
357  private void UpdateDevotion(float deltaTime)
358  {
359  var currentObjective = objectiveManager.CurrentObjective;
360  if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.FirstOrDefault() == this))
361  {
362  CumulatedDevotion += Devotion * deltaTime;
363  }
364  }
365 
366  public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
367 
368  public virtual void Update(float deltaTime)
369  {
370  if (resetPriorityTimer > 0)
371  {
372  resetPriorityTimer -= deltaTime;
373  }
374  else
375  {
376  ForceHighestPriority = false;
377  }
379  {
380  UpdateDevotion(deltaTime);
381  }
382  subObjectives.ForEach(so => so.Update(deltaTime));
383  }
384 
388  protected virtual void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
389  {
390  foreach (T1 key in collection)
391  {
392  if (dictionary.TryGetValue(key, out T2 objective))
393  {
394  if (!subObjectives.Contains(objective))
395  {
396  dictionary.Remove(key);
397  }
398  }
399  }
400  }
401 
407  protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
408  {
409  if (objective != null)
410  {
411  // Sub objective already found, no need to do anything if it remains in the subobjectives
412  // If the sub objective is removed -> it's either completed or impossible to complete.
413  if (!subObjectives.Contains(objective))
414  {
415  objective = null;
416  }
417  return false;
418  }
419  else
420  {
421  objective = constructor();
422  if (!subObjectives.Contains(objective))
423  {
424  if (objective.AllowMultipleInstances)
425  {
426  objective.SourceObjective = this;
427  subObjectives.Add(objective);
428  }
429  else
430  {
431  AddSubObjective(objective);
432  }
433  if (onCompleted != null)
434  {
435  objective.Completed += onCompleted;
436  }
437  if (onAbandon != null)
438  {
439  objective.Abandoned += onAbandon;
440  }
441  return true;
442  }
443 #if DEBUG
444  DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace.CleanupStackTrace());
445 #endif
446  return false;
447  }
448  }
449 
450  public virtual void OnSelected()
451  {
452  Reset();
453  Selected?.Invoke();
454  Selected = null;
455  }
456 
457  public virtual void OnDeselected()
458  {
459  CumulatedDevotion = 0;
460  Deselected?.Invoke();
461  Deselected = null;
462  }
463 
464  protected virtual void OnCompleted()
465  {
466  Completed?.Invoke();
467  Completed = null;
468  }
469 
470  protected virtual void OnAbandon()
471  {
472  Abandoned?.Invoke();
473  Abandoned = null;
474  }
475 
476  public virtual void Reset()
477  {
478  subObjectives.Clear();
479  isCompleted = false;
480  hasBeenChecked = false;
481  _abandon = false;
482  CumulatedDevotion = 0;
483  }
484 
485  protected abstract void Act(float deltaTime);
486 
487  private bool isCompleted;
488  private bool hasBeenChecked;
489 
490  public bool IsCompleted
491  {
492  get
493  {
494  if (!hasBeenChecked)
495  {
496  CheckState();
497  }
498  return isCompleted;
499  }
500  protected set
501  {
502  if (isCompleted == value) { return; }
503  isCompleted = value;
504  if (isCompleted)
505  {
506  OnCompleted();
507  }
508  }
509  }
510 
514  private bool Check()
515  {
516  if (AbortCondition != null && AbortCondition(this))
517  {
518  Abandon = true;
519  return false;
520  }
521  return CheckObjectiveSpecific();
522  }
523 
527  protected abstract bool CheckObjectiveSpecific();
528 
529  private bool CheckState()
530  {
531  hasBeenChecked = true;
532  CheckSubObjectives();
533  if (subObjectives.None() || ConcurrentObjectives)
534  {
535  if (Check())
536  {
537  IsCompleted = true;
538  }
539  }
540  return isCompleted;
541  }
542 
543  private void CheckSubObjectives()
544  {
545  for (int i = 0; i < subObjectives.Count; i++)
546  {
547  var subObjective = subObjectives[i];
548  subObjective.CheckState();
549  if (subObjective.IsCompleted)
550  {
551 #if DEBUG
552  DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen);
553 #endif
554  subObjectives.Remove(subObjective);
555  }
556  else if (!subObjective.CanBeCompleted)
557  {
558 #if DEBUG
559  DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
560 #endif
561  subObjectives.Remove(subObjective);
563  {
564  if (objectiveManager.IsOrder(this))
565  {
566  Reset();
567  }
568  else
569  {
570  Abandon = true;
571  }
572  }
573  }
574  }
575  }
576 
577  public virtual void SpeakAfterOrderReceived() { }
578 
579  protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
580  {
581  if (item == null) { return false; }
582  bool canEquip = false;
583  if (item.AllowedSlots.Contains(InvSlotType.Any))
584  {
586  {
587  canEquip = true;
588  }
589  }
590  if (!canEquip)
591  {
592  var inv = character.Inventory;
593  foreach (var allowedSlot in item.AllowedSlots)
594  {
595  if (!allowWearing)
596  {
597  if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
598  {
599  continue;
600  }
601  }
602  foreach (var slotType in inv.SlotTypes)
603  {
604  if (!allowedSlot.HasFlag(slotType)) { continue; }
605  for (int i = 0; i < inv.Capacity; i++)
606  {
607  canEquip = true;
608  if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
609  {
610  canEquip = false;
611  break;
612  }
613  }
614  }
615  }
616  }
617  return canEquip && character.Inventory.CanBePut(item);
618  }
619 
620  protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing);
621  }
622 }
virtual bool IsDuplicate< T >(T otherObjective)
bool IsIgnoredAtOutpost()
Returns true only when at a friendly outpost and when the order is set to be ignored there....
virtual bool AbandonIfDisallowed
Should the objective abandon when it's not allowed in the current context or should it just stay inac...
float CalculatePriority()
Call this only when the priority needs to be recalculated. Use the cached Priority property when you ...
AIObjective SourceObjective
Which objective (if any) created this objective. When this is a subobjective, the parent objective is...
EventAction SourceEventAction
Which event action (if any) created this objective
virtual bool AllowMultipleInstances
Can there be multiple objective instaces of the same type?
IEnumerable< AIObjective > GetSubObjectivesRecursive(bool includingSelf=false)
float Priority
Final priority value after all calculations.
Action Selected
A single shot event. Automatically cleared after launching. Use OnSelected method for implementing (i...
abstract void Act(float deltaTime)
void AddSubObjective(AIObjective objective, bool addFirst=false)
Action Deselected
A single shot event. Automatically cleared after launching. Use OnDeselected method for implementing ...
Func< AIObjective, bool > AbortCondition
Aborts the objective when this condition is true.
void TryComplete(float deltaTime)
Makes the character act according to the objective, or according to any subobjectives that need to be...
bool TryAddSubObjective< T >(ref T objective, Func< T > constructor, Action onCompleted=null, Action onAbandon=null)
Checks if the objective already is created and added in subobjectives. If not, creates it....
bool CanEquip(Item item, bool allowWearing)
virtual void SyncRemovedObjectives< T1, T2 >(Dictionary< T1, T2 > dictionary, IEnumerable< T1 > collection)
Checks if the subobjectives in the given collection are removed from the subobjectives....
AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option=default)
Action Completed
A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (...
abstract bool CheckObjectiveSpecific()
Should return whether the objective is completed or not.
Action Abandoned
A single shot event. Automatically cleared after launching. Use OnAbandoned method for implementing (...
virtual bool AllowInFriendlySubs
When true, the objective is allowed in the player subs (when in the same team) and on friendly outpos...
static bool CanPutInInventory(Character character, Item item, bool allowWearing)
virtual bool AllowAutomaticItemUnequipping
There's a separate property for diving suit and mask: KeepDivingGearOn.
virtual bool AllowSubObjectiveSorting
Should subobjectives be sorted according to their priority?
float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier=3, float maxDistance=10000.0f, float factorAtMinDistance=1.0f)
Get a normalized value representing how close the target position is. The value is a rough estimation...
virtual bool ConcurrentObjectives
Run the main objective with all subobjectives concurrently? If false, the main objective will continu...
static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier=3, float maxDistance=10000.0f, float factorAtMinDistance=1.0f)
Get a normalized value representing how close the target position is. The value is a rough estimation...
AIObjective CurrentObjective
Includes orders.
float GetOrderPriority(AIObjective objective)
bool IsOrder(AIObjective objective)
float? WaitTimer
When set above zero, the character will stand still doing nothing until the timer runs out....
bool IsAnySlotAvailable(Item item)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
bool CanBePut(Item item)
Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be p...
IEnumerable< InvSlotType > AllowedSlots
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...