Client LuaCsForBarotrauma
AIObjectiveFindSafety.cs
2 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Diagnostics;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
12  {
13  public override Identifier Identifier { get; set; } = "find safety".ToIdentifier();
14  public override bool ForceRun => true;
15  public override bool KeepDivingGearOn => true;
16  public override bool IgnoreUnsafeHulls => true;
17  protected override bool ConcurrentObjectives => true;
18  protected override bool AllowOutsideSubmarine => true;
19  protected override bool AllowInAnySub => true;
20  public override bool AbandonWhenCannotCompleteSubObjectives => false;
21 
22  private const float PriorityIncrease = 100;
23  private const float PriorityDecrease = 10;
24  private const float SearchHullInterval = 3.0f;
25 
26  private float currentHullSafety;
27 
28  private float searchHullTimer;
29 
30  private AIObjectiveGoTo goToObjective;
31  private AIObjectiveFindDivingGear divingGearObjective;
32 
33  public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
34 
35  protected override bool CheckObjectiveState() => false;
36  public override bool CanBeCompleted => true;
37 
38  private bool resetPriority;
39 
40  protected override float GetPriority()
41  {
42  if (character.CurrentHull == null)
43  {
44  Priority = (
45  objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
46  objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
49  }
50  else
51  {
52  bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
53  static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
54 
55  if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
56  {
58  }
60  {
62  HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false))
63  {
64  //we have a suit that's not suitable for the pressure,
65  //but we've failed to find a better one
66  // shit, not much we can do here, let's just allow the bot to get on with their current objective
67  Priority = 0;
68  }
69  else
70  {
72  }
73  }
74  else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
76  {
77  // Ordered to follow, hold position, or return back to main sub inside a hostile sub
78  // -> ignore find safety unless we need to find a diving gear
79  Priority = 0;
80  }
81  else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
82  {
83  Priority = 0;
84  }
86  if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
87  {
88  // Boost the priority while seeking the diving gear
90  }
91  }
92  return Priority;
93  }
94 
95  public override void Update(float deltaTime)
96  {
97  if (retryTimer > 0)
98  {
99  retryTimer -= deltaTime;
100  if (retryTimer <= 0)
101  {
102  retryCounter = 0;
103  }
104  }
105  if (resetPriority)
106  {
107  Priority = 0;
108  resetPriority = false;
109  return;
110  }
111  if (character.CurrentHull == null)
112  {
113  currentHullSafety = 0;
114  }
115  else
116  {
117  currentHullSafety = HumanAIController.CurrentHullSafety;
118  if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
119  {
120  Priority -= PriorityDecrease * deltaTime;
121  if (currentHullSafety >= 100 && !character.IsLowInOxygen)
122  {
123  // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
124  Priority = 0;
125  }
126  }
127  else
128  {
129  float dangerFactor = (100 - currentHullSafety) / 100;
130  Priority += dangerFactor * PriorityIncrease * deltaTime;
131  }
133  }
134  }
135 
136  private Hull currentSafeHull;
137  private Hull previousSafeHull;
138  private bool cannotFindSafeHull;
139  private bool cannotFindDivingGear;
140  private readonly int findDivingGearAttempts = 2;
141  private int retryCounter;
142  private readonly float retryResetTime = 5;
143  private float retryTimer;
144  protected override void Act(float deltaTime)
145  {
146  if (resetPriority) { return; }
147  var currentHull = character.CurrentHull;
148  bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
149  bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
150  if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
151  {
152  bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
153  bool needsEquipment = shouldActOnSuffocation;
154  if (needsDivingSuit)
155  {
157  }
158  else if (needsDivingGear)
159  {
161  }
162  if (needsEquipment)
163  {
164  if (cannotFindDivingGear && retryCounter < findDivingGearAttempts)
165  {
166  retryTimer = retryResetTime;
167  retryCounter++;
168  needsDivingSuit = !needsDivingSuit;
169  RemoveSubObjective(ref divingGearObjective);
170  }
171  if (divingGearObjective == null)
172  {
173  cannotFindDivingGear = false;
174  RemoveSubObjective(ref goToObjective);
175  TryAddSubObjective(ref divingGearObjective,
176  constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
177  onAbandon: () =>
178  {
179  searchHullTimer = Math.Min(1, searchHullTimer);
180  cannotFindDivingGear = true;
181  // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
182  },
183  onCompleted: () =>
184  {
185  resetPriority = true;
186  searchHullTimer = Math.Min(1, searchHullTimer);
187  RemoveSubObjective(ref divingGearObjective);
188  });
189  }
190  }
191  }
192  if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
193  {
194  if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
195  {
196  searchHullTimer = Math.Min(1, searchHullTimer);
197  }
198  if (searchHullTimer > 0.0f)
199  {
200  searchHullTimer -= deltaTime;
201  }
202  else
203  {
204  HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
205  if (hullSearchStatus != HullSearchStatus.Finished)
206  {
207  UpdateSimpleEscape(deltaTime);
208  return;
209  }
210  searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
211  previousSafeHull = currentSafeHull;
212  currentSafeHull = potentialSafeHull;
213  cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
214  currentSafeHull ??= previousSafeHull;
215  if (currentSafeHull != null && currentSafeHull != currentHull)
216  {
217  if (goToObjective?.Target != currentSafeHull)
218  {
219  RemoveSubObjective(ref goToObjective);
220  }
221  TryAddSubObjective(ref goToObjective,
222  constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
223  {
224  SpeakIfFails = false,
225  AllowGoingOutside =
227  character.CurrentHull == null ||
230  },
231  onCompleted: () =>
232  {
233  if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
235  {
236  resetPriority = true;
237  searchHullTimer = Math.Min(1, searchHullTimer);
238  }
239  RemoveSubObjective(ref goToObjective);
240  if (cannotFindDivingGear)
241  {
242  // If diving gear objective failed, let's reset it here.
243  RemoveSubObjective(ref divingGearObjective);
244  }
245  },
246  onAbandon: () =>
247  {
248  // Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
249  // If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
250  // If the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts.
251  if (goToObjective != null)
252  {
253  if (goToObjective.Target is Hull hull)
254  {
255  if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine))
256  {
258  }
259  }
260  }
261  RemoveSubObjective(ref goToObjective);
262  });
263  }
264  else
265  {
266  RemoveSubObjective(ref goToObjective);
267  }
268  }
269  if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
270  UpdateSimpleEscape(deltaTime);
271 
272  bool inFriendlySub =
275  if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
276  {
277  if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
278  {
280  }
281  }
282  }
283  }
284 
285  public void UpdateSimpleEscape(float deltaTime)
286  {
287  Vector2 escapeVel = Vector2.Zero;
288  if (character.CurrentHull != null)
289  {
290  foreach (Hull hull in HumanAIController.VisibleHulls)
291  {
292  foreach (FireSource fireSource in hull.FireSources)
293  {
294  Vector2 dir = character.Position - fireSource.Position;
295  float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
296  escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
297  }
298  }
299  foreach (Character enemy in Character.CharacterList)
300  {
301  if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
302  if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
303  {
304  Vector2 dir = character.Position - enemy.Position;
305  float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
306  escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
307  }
308  }
309  }
310  if (escapeVel != Vector2.Zero)
311  {
312  float left = character.CurrentHull.Rect.X + 50;
313  float right = character.CurrentHull.Rect.Right - 50;
314  //only move if we haven't reached the edge of the room
315  if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
316  {
319  }
320  else
321  {
322  character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
324  }
325  }
326  else
327  {
328  objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
329  }
330  }
331 
332  public enum HullSearchStatus
333  {
334  Running,
335  Finished
336  }
337 
338  private readonly List<Hull> hulls = new List<Hull>();
339  private int hullSearchIndex = -1;
340  float bestHullValue = 0;
341  bool bestHullIsAirlock = false;
342  Hull potentialBestHull;
343 
344 #if DEBUG
345  private readonly Stopwatch stopWatch = new Stopwatch();
346 #endif
347 
352  public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable<Hull> ignoredHulls = null, bool allowChangingSubmarine = true)
353  {
354  if (hullSearchIndex == -1)
355  {
356  bestHullValue = 0;
357  potentialBestHull = null;
358  bestHullIsAirlock = false;
359  hulls.Clear();
360  var connectedSubs = character.Submarine?.GetConnectedSubs();
361 #if DEBUG
362  stopWatch.Restart();
363 #endif
364  foreach (Hull hull in Hull.HullList)
365  {
366  if (hull.Submarine == null) { continue; }
367  // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
368  if (hull.Submarine.Info.IsRuin) { continue; }
369  if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; }
370  if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
371  if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
372  if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
373  if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
374  if (hulls.None())
375  {
376  hulls.Add(hull);
377  }
378  else
379  {
380  //sort the hulls first based on distance and a rough suitability estimation
381  //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
382  //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
383  //path calculations, only to discard all of them when going through the hulls in the outpost)
384  bool addLast = true;
385  float hullSuitability = EstimateHullSuitability(hull);
386  for (int i = 0; i < hulls.Count; i++)
387  {
388  Hull otherHull = hulls[i];
389  float otherHullSuitability = EstimateHullSuitability(otherHull);
390  if (hullSuitability > otherHullSuitability)
391  {
392  hulls.Insert(i, hull);
393  addLast = false;
394  break;
395  }
396  }
397  if (addLast)
398  {
399  hulls.Add(hull);
400  }
401  }
402 
403  float EstimateHullSuitability(Hull h)
404  {
405  float distX = Math.Abs(h.WorldPosition.X - character.WorldPosition.X);
406  float distY = Math.Abs(h.WorldPosition.Y - character.WorldPosition.Y);
407  if (character.CurrentHull != null)
408  {
409  distY *= 3;
410  }
411  float dist = distX + distY;
412  float suitability = -dist;
413  const float suitabilityReduction = 10000.0f;
414  if (h.Submarine != character.Submarine)
415  {
416  suitability -= suitabilityReduction;
417  }
418  if (character.CurrentHull != null)
419  {
420  if (h.AvoidStaying)
421  {
422  suitability -= suitabilityReduction;
423  }
424  if (HumanAIController.UnsafeHulls.Contains(h))
425  {
426  suitability -= suitabilityReduction;
427  }
428  if (HumanAIController.NeedsDivingGear(h, out _))
429  {
430  suitability -= suitabilityReduction;
431  }
432  }
433  return suitability;
434  }
435  }
436  if (hulls.None())
437  {
438  bestHull = null;
439  return HullSearchStatus.Finished;
440  }
441  hullSearchIndex = 0;
442 #if DEBUG
443  stopWatch.Stop();
444  DebugConsole.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
445 #endif
446  }
447 
448  Hull potentialHull = hulls[hullSearchIndex];
449 
450  float hullSafety = 0;
451  bool hullIsAirlock = false;
452  bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
453  if (isCharacterInside)
454  {
455  hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
456  float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f);
457  hullSafety *= distanceFactor;
458  //skip the hull if the safety is already less than the best hull
459  //(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
460  if (hullSafety > bestHullValue)
461  {
462  //avoid airlock modules if not allowed to change the sub
463  if (allowChangingSubmarine || potentialHull.OutpostModuleTags.All(t => t != "airlock"))
464  {
465  // Don't allow to go outside if not already outside.
466  var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
467  if (path.Unreachable)
468  {
469  hullSafety = 0;
470  HumanAIController.UnreachableHulls.Add(potentialHull);
471  }
472  else
473  {
474  // Each unsafe node reduces the hull safety value.
475  // Ignore the current hull, because otherwise we couldn't find a path out.
476  int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
477  hullSafety /= 1 + unsafeNodes;
478  // If the target is not inside a friendly submarine, considerably reduce the hull safety.
479  if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
480  {
481  hullSafety /= 10;
482  }
483  }
484  }
485  else
486  {
487  hullSafety = 0;
488  }
489  }
490  }
491  else
492  {
493  if (potentialHull.IsAirlock)
494  {
495  hullSafety = 100;
496  hullIsAirlock = true;
497  }
498  else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character))
499  {
500  hullSafety = 100;
501  }
502  float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
503  // Huge preference for closer targets
504  float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f);
505  hullSafety *= distanceFactor;
506  // If the target is not inside a friendly submarine, considerably reduce the hull safety.
507  // Intentionally exclude wrecks from this check
508  if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
509  {
510  hullSafety /= 10;
511  }
512  }
513  if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock))
514  {
515  potentialBestHull = potentialHull;
516  bestHullValue = hullSafety;
517  bestHullIsAirlock = hullIsAirlock;
518  }
519 
520  bestHull = potentialBestHull;
521  hullSearchIndex++;
522 
523  if (hullSearchIndex >= hulls.Count)
524  {
525  hullSearchIndex = -1;
526  return HullSearchStatus.Finished;
527  }
528  return HullSearchStatus.Running;
529  }
530 
531  public override void Reset()
532  {
533  base.Reset();
534  goToObjective = null;
535  divingGearObjective = null;
536  currentSafeHull = null;
537  previousSafeHull = null;
538  retryCounter = 0;
539  cannotFindDivingGear = false;
540  cannotFindSafeHull = false;
541  }
542 
543  private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
544  {
545  if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
546  if (needsSuit)
547  {
548  return !HumanAIController.HasDivingSuit(character, minOxygen);
549  }
550  else
551  {
552  return !HumanAIController.HasDivingGear(character, minOxygen);
553  }
554  }
555  }
556 }
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
static float GetMinOxygen(Character character)
override void Update(float deltaTime)
HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable< Hull > ignoredHulls=null, bool allowChangingSubmarine=true)
Tries to find the best (safe, nearby) hull the character can find a path to. Checks one hull at a tim...
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier=1)
override void Act(float deltaTime)
float Priority
Final priority value after all calculations.
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...
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
List< AIObjective > Objectives
Excluding the current order.
void AddObjective(AIObjective objective)
const float MaxObjectivePriority
Highest possible priority for any objective. Used in certain cases where the character needs to react...
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
bool IsProtectedFromPressure
Is the character currently protected from the pressure by immunity/ability or a status effect (e....
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
IEnumerable< Identifier > OutpostModuleTags
Inherited flags from outpost generation.
bool LeadsOutside(Character character)
Does this hull have any doors leading outside?
static readonly List< Hull > HullList
IEnumerable< Hull > GetConnectedHulls(bool includingThis, int? searchDepth=null, bool ignoreClosedGaps=false)
static bool HasDivingSuit(Character character, float conditionPercentage=0, bool requireOxygenTank=true, bool requireSuitablePressureProtection=true)
Check whether the character has a diving suit in usable condition, suitable pressure protection for t...
static bool HasDivingGear(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
static bool IsActive(Character c)
float GetHullSafety(Hull hull, Character character, IEnumerable< Hull > visibleHulls=null)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
static bool HasDivingMask(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
Check whether the character has a diving mask in usable condition plus some oxygen.
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
float ColliderHeightFromFloor
In sim units. Joint scale applied.
void SteeringManual(float deltaTime, Vector2 velocity)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)