Client LuaCsForBarotrauma
HintManager.cs
2 using Barotrauma.IO;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
12  static class HintManager
13  {
14  private const string HintManagerFile = "hintmanager.xml";
15 
16  public static bool Enabled => !GameSettings.CurrentConfig.DisableInGameHints;
17  private static HashSet<Identifier> HintIdentifiers { get; set; }
18  private static Dictionary<Identifier, HashSet<Identifier>> HintTags { get; } = new Dictionary<Identifier, HashSet<Identifier>>();
19  private static Dictionary<Identifier, (Identifier identifier, Identifier option)> HintOrders { get; } = new Dictionary<Identifier, (Identifier orderIdentifier, Identifier orderOption)>();
23  private static HashSet<Identifier> HintsIgnoredThisRound { get; } = new HashSet<Identifier>();
24  private static GUIMessageBox ActiveHintMessageBox { get; set; }
25  private static Action OnUpdate { get; set; }
26  private static double TimeStoppedInteracting { get; set; }
27  private static double TimeRoundStarted { get; set; }
31  private static int TimeBeforeReminders { get; set; }
35  private static int ReminderCooldown { get; set; }
36  private static double TimeReminderLastDisplayed { get; set; }
37  private static HashSet<Hull> BallastHulls { get; } = new HashSet<Hull>();
38 
39  public static void Init()
40  {
41  if (File.Exists(HintManagerFile))
42  {
43  var doc = XMLExtensions.TryLoadXml(HintManagerFile);
44  if (doc?.Root != null)
45  {
46  HintIdentifiers = new HashSet<Identifier>();
47  foreach (var element in doc.Root.Elements())
48  {
49  GetHintsRecursive(element, element.NameAsIdentifier());
50  }
51  }
52  else
53  {
54  DebugConsole.ThrowError($"File \"{HintManagerFile}\" is empty - cannot initialize the HintManager!");
55  }
56  }
57  else
58  {
59  DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!");
60  }
61 
62  static void GetHintsRecursive(XElement element, Identifier identifier)
63  {
64  if (!element.HasElements)
65  {
66  HintIdentifiers.Add(identifier);
67  if (element.GetAttributeIdentifierArray("tags", null) is Identifier[] tags)
68  {
69  HintTags.TryAdd(identifier, tags.ToHashSet());
70  }
71  if (element.GetAttributeIdentifier("order", Identifier.Empty) is Identifier orderIdentifier && orderIdentifier != Identifier.Empty)
72  {
73  Identifier orderOption = element.GetAttributeIdentifier("orderoption", Identifier.Empty);
74  HintOrders.Add(identifier, (orderIdentifier, orderOption));
75  }
76  return;
77  }
78  else if (element.Name.ToString().Equals("reminder"))
79  {
80  TimeBeforeReminders = element.GetAttributeInt("timebeforereminders", TimeBeforeReminders);
81  ReminderCooldown = element.GetAttributeInt("remindercooldown", ReminderCooldown);
82  }
83  foreach (var childElement in element.Elements())
84  {
85  GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}".ToIdentifier());
86  }
87  }
88  }
89 
90  public static void Update()
91  {
92  if (HintIdentifiers == null || GameSettings.CurrentConfig.DisableInGameHints) { return; }
93  if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; }
94 
95  if (ActiveHintMessageBox != null)
96  {
97  if (ActiveHintMessageBox.Closed)
98  {
99  ActiveHintMessageBox = null;
100  OnUpdate = null;
101  }
102  else
103  {
104  OnUpdate?.Invoke();
105  return;
106  }
107  }
108 
109  CheckIsInteracting();
110  CheckIfDivingGearOutOfOxygen();
111  CheckHulls();
112  CheckReminders();
113  }
114 
115  public static void OnSetSelectedItem(Character character, Item oldItem, Item newItem)
116  {
117  if (oldItem == newItem) { return; }
118 
119  if (Character.Controlled != null && Character.Controlled == character && oldItem != null && !oldItem.IsLadder)
120  {
121  TimeStoppedInteracting = Timing.TotalTime;
122  }
123 
124  if (newItem == null) { return; }
125  if (newItem.IsLadder) { return; }
126  if (newItem.GetComponent<ConnectionPanel>() is ConnectionPanel cp && cp.User == character) { return; }
127  OnStartedInteracting(character, newItem);
128  }
129 
130  private static void OnStartedInteracting(Character character, Item item)
131  {
132  if (!CanDisplayHints()) { return; }
133  if (character != Character.Controlled || item == null) { return; }
134 
135  string hintIdentifierBase = "onstartedinteracting";
136 
137  // onstartedinteracting.brokenitem
138  if (item.Repairables.Any(r => r.IsBelowRepairThreshold))
139  {
140  if (DisplayHint($"{hintIdentifierBase}.brokenitem".ToIdentifier())) { return; }
141  }
142 
143  // Don't display other item-related hints if the repair interface is displayed
144  if (item.Repairables.Any(r => r.ShouldDrawHUD(character))) { return; }
145 
146  // onstartedinteracting.lootingisstealing
147  if (item.Submarine?.Info?.Type == SubmarineType.Outpost &&
148  item.ContainedItems.Any(i => !i.AllowStealing))
149  {
150  if (DisplayHint($"{hintIdentifierBase}.lootingisstealing".ToIdentifier())) { return; }
151  }
152 
153  // onstartedinteracting.turretperiscope
154  if (item.HasTag(Tags.Periscope) &&
155  item.GetConnectedComponents<Turret>().FirstOrDefault(t => t.Item.HasTag(Tags.Turret)) is Turret)
156  {
157  if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(),
158  variables: new[]
159  {
160  ("[shootkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)),
161  ("[deselectkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect))
162  }))
163  { return; }
164  }
165 
166  // onstartedinteracting.item...
167  hintIdentifierBase += ".item";
168  foreach (Identifier hintIdentifier in HintIdentifiers)
169  {
170  if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; }
171  if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; }
172  if (!item.HasTag(hintTags)) { continue; }
173  if (DisplayHint(hintIdentifier)) { return; }
174  }
175  }
176 
177  public static void OnStartRepairing(Character character, Repairable repairable)
178  {
179  if (repairable.ForceDeteriorationTimer > 0.0f && !character.IsTraitor)
180  {
181  CoroutineManager.Invoke(() =>
182  {
183  DisplayHint($"repairingsabotageditem".ToIdentifier());
184  }, delay: 5.0f);
185  }
186  }
187 
188  public static void OnItemMarkedForRelocation()
189  {
190  DisplayHint($"onitemmarkedforrelocation".ToIdentifier());
191  }
192 
193  public static void OnItemMarkedForDeconstruction(Character character)
194  {
195  if (character == Character.Controlled)
196  {
197  DisplayHint($"onitemmarkedfordeconstruction".ToIdentifier());
198  }
199  }
200 
201  private static void CheckIsInteracting()
202  {
203  if (!CanDisplayHints()) { return; }
204  if (Character.Controlled?.SelectedItem == null) { return; }
205 
206  if (Character.Controlled.SelectedItem.GetComponent<Reactor>() is Reactor reactor && reactor.PowerOn &&
207  Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable<Item> containedItems &&
208  containedItems.Count(i => i.HasTag(Tags.ReactorFuel)) > 1)
209  {
210  if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; }
211  }
212  }
213 
214  public static void OnRoundStarted()
215  {
216  // Make sure everything's been reset properly, OnRoundEnded() isn't always called when exiting a game
217  Reset();
218  TimeRoundStarted = GameMain.GameScreen.GameTime;
219 
220  var initRoundHandle = CoroutineManager.StartCoroutine(InitRound(), "HintManager.InitRound");
221  if (!CanDisplayHints(requireGameScreen: false, requireControllingCharacter: false)) { return; }
222  CoroutineManager.StartCoroutine(DisplayRoundStartedHints(initRoundHandle), "HintManager.DisplayRoundStartedHints");
223 
224  static IEnumerable<CoroutineStatus> InitRound()
225  {
226  while (Character.Controlled == null) { yield return CoroutineStatus.Running; }
227  // Get the ballast hulls on round start not to find them again and again later
228  BallastHulls.Clear();
229  var sub = Submarine.MainSubs.FirstOrDefault(s => s != null && s.TeamID == Character.Controlled.TeamID);
230  if (sub != null)
231  {
232  foreach (var item in sub.GetItems(true))
233  {
234  if (item.CurrentHull == null) { continue; }
235  if (item.GetComponent<Pump>() == null) { continue; }
236  if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
237  BallastHulls.Add(item.CurrentHull);
238  }
239  }
240  yield return CoroutineStatus.Success;
241  }
242 
243  static IEnumerable<CoroutineStatus> DisplayRoundStartedHints(CoroutineHandle initRoundHandle)
244  {
245  while (GameMain.Instance.LoadingScreenOpen || Screen.Selected != GameMain.GameScreen ||
246  CoroutineManager.IsCoroutineRunning(initRoundHandle) ||
247  CoroutineManager.IsCoroutineRunning("LevelTransition") ||
248  CoroutineManager.IsCoroutineRunning("SinglePlayerCampaign.DoInitialCameraTransition") ||
249  CoroutineManager.IsCoroutineRunning("MultiPlayerCampaign.DoInitialCameraTransition") ||
250  GUIMessageBox.VisibleBox != null || Character.Controlled == null)
251  {
252  yield return CoroutineStatus.Running;
253  }
254 
255  OnStartedControlling();
256 
257  while (ActiveHintMessageBox != null)
258  {
259  yield return CoroutineStatus.Running;
260  }
261 
262  if (!GameMain.GameSession.GameMode.IsSinglePlayer &&
263  GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled)
264  {
265  DisplayHint("onroundstarted.voipdisabled".ToIdentifier(), onUpdate: () =>
266  {
267  if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { return; }
268  ActiveHintMessageBox.Close();
269  });
270  }
271 
272  if (GameMain.GameSession is { TraitorsEnabled: true })
273  {
274  DisplayHint("traitorsonboard".ToIdentifier());
275  DisplayHint("traitorsonboard2".ToIdentifier());
276  }
277  yield return CoroutineStatus.Success;
278  }
279 
280  }
281 
282  public static void OnRoundEnded()
283  {
284  Reset();
285  }
286 
287  private static void Reset()
288  {
289  CoroutineManager.StopCoroutines("HintManager.InitRound");
290  CoroutineManager.StopCoroutines("HintManager.DisplayRoundStartedHints");
291  if (ActiveHintMessageBox != null)
292  {
293  GUIMessageBox.MessageBoxes.Remove(ActiveHintMessageBox);
294  ActiveHintMessageBox = null;
295  }
296  OnUpdate = null;
297  HintsIgnoredThisRound.Clear();
298  }
299 
300  public static void OnSonarSpottedCharacter(Item sonar, Character spottedCharacter)
301  {
302  if (!CanDisplayHints()) { return; }
303  if (sonar == null || sonar.Removed) { return; }
304  if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; }
305  if (Character.Controlled.SelectedItem != sonar) { return; }
306  if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; }
307  DisplayHint("onsonarspottedenemy".ToIdentifier());
308  }
309 
310  public static void OnAfflictionDisplayed(Character character, List<Affliction> displayedAfflictions)
311  {
312  if (!CanDisplayHints()) { return; }
313  if (character != Character.Controlled || displayedAfflictions == null) { return; }
314  foreach (var affliction in displayedAfflictions)
315  {
316  if (affliction?.Prefab == null) { continue; }
317  if (affliction.Prefab.IsBuff) { continue; }
318  if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; }
319  if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.IsEntityRadiated(character) ?? false)) { continue; }
320  if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; }
321  DisplayHint("onafflictiondisplayed".ToIdentifier(),
322  variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)) },
323  icon: affliction.Prefab.Icon,
324  iconColor: CharacterHealth.GetAfflictionIconColor(affliction),
325  onUpdate: () =>
326  {
327  if (CharacterHealth.OpenHealthWindow == null) { return; }
328  ActiveHintMessageBox.Close();
329  });
330  return;
331  }
332  }
333 
334  public static void OnShootWithoutAiming(Character character, Item item)
335  {
336  if (!CanDisplayHints()) { return; }
337  if (character != Character.Controlled) { return; }
338  if (character.HasSelectedAnyItem || character.FocusedItem != null) { return; }
339  if (item == null || !item.IsShootable || !item.RequireAimToUse) { return; }
340  if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; }
341  if (GUI.MouseOn != null) { return; }
342  if (Character.Controlled.Inventory?.visualSlots != null && Character.Controlled.Inventory.visualSlots.Any(s => s.InteractRect.Contains(PlayerInput.MousePosition))) { return; }
343  Identifier hintIdentifier = "onshootwithoutaiming".ToIdentifier();
344  if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; }
345  if (!item.HasTag(tags)) { return; }
346  DisplayHint(hintIdentifier,
347  variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim)) },
348  onUpdate: () =>
349  {
350  if (character.SelectedItem == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim))
351  {
352  ActiveHintMessageBox.Close();
353  }
354  });
355  }
356 
357  public static void OnWeldingDoor(Character character, Door door)
358  {
359  if (!CanDisplayHints()) { return; }
360  if (character != Character.Controlled) { return; }
361  if (door == null || door.Stuck < 20.0f) { return; }
362  DisplayHint("onweldingdoor".ToIdentifier());
363  }
364 
365  public static void OnTryOpenStuckDoor(Character character)
366  {
367  if (!CanDisplayHints()) { return; }
368  if (character != Character.Controlled) { return; }
369  DisplayHint("ontryopenstuckdoor".ToIdentifier());
370  }
371 
372  public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType)
373  {
374  if (!CanDisplayHints()) { return; }
375  if (interactionType == CampaignMode.InteractionType.None) { return; }
376  Identifier hintIdentifier = $"onshowcampaigninterface.{interactionType}".ToIdentifier();
377  DisplayHint(hintIdentifier, onUpdate: () =>
378  {
379 
380  if (!(GameMain.GameSession?.Campaign is CampaignMode campaign) ||
381  (!campaign.ShowCampaignUI && !campaign.ForceMapUI) ||
382  campaign.CampaignUI?.SelectedTab != CampaignMode.InteractionType.Map)
383  {
384  ActiveHintMessageBox.Close();
385  }
386  });
387  }
388 
389  public static void OnShowCommandInterface()
390  {
391  IgnoreReminder("commandinterface");
392  if (!CanDisplayHints()) { return; }
393  DisplayHint("onshowcommandinterface".ToIdentifier(), onUpdate: () =>
394  {
395  if (CrewManager.IsCommandInterfaceOpen) { return; }
396  ActiveHintMessageBox.Close();
397  });
398  }
399 
400  public static void OnShowHealthInterface()
401  {
402  if (!CanDisplayHints()) { return; }
403  if (CharacterHealth.OpenHealthWindow == null) { return; }
404  DisplayHint("onshowhealthinterface".ToIdentifier(), onUpdate: () =>
405  {
406  if (CharacterHealth.OpenHealthWindow != null) { return; }
407  ActiveHintMessageBox.Close();
408  });
409  }
410 
411  public static void OnShowTabMenu()
412  {
413  IgnoreReminder("tabmenu");
414  }
415 
416  public static void OnObtainedItem(Character character, Item item)
417  {
418  if (!CanDisplayHints()) { return; }
419  if (character != Character.Controlled || item == null) { return; }
420 
421  if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; }
422  foreach (Identifier tag in item.GetTags())
423  {
424  if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; }
425  }
426 
427  if ((item.HasTag(Tags.GeneticMaterial) && character.Inventory.FindItemByTag(Tags.GeneticMaterial, recursive: true) != null) ||
428  (item.HasTag(Tags.GeneticDevice) && character.Inventory.FindItemByTag(Tags.GeneticDevice, recursive: true) != null))
429  {
430  if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; }
431  }
432  }
433 
434  public static void OnStartDeconstructing(Character character, Deconstructor deconstructor)
435  {
436  if (!CanDisplayHints()) { return; }
437  if (character != Character.Controlled || deconstructor == null) { return; }
438  if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent<GeneticMaterial>() is not null))
439  {
440  DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier());
441  }
442  }
443 
444  public static void OnStoleItem(Character character, Item item)
445  {
446  if (!CanDisplayHints()) { return; }
447  if (character != Character.Controlled) { return; }
448  if (item == null || item.AllowStealing || !item.StolenDuringRound) { return; }
449  DisplayHint("onstoleitem".ToIdentifier(), onUpdate: () =>
450  {
451  if (item == null || item.Removed || item.GetRootInventoryOwner() != character)
452  {
453  ActiveHintMessageBox.Close();
454  }
455  });
456  }
457 
458  public static void OnHandcuffed(Character character)
459  {
460  if (!CanDisplayHints()) { return; }
461  if (character != Character.Controlled || !character.LockHands) { return; }
462  DisplayHint("onhandcuffed".ToIdentifier(), onUpdate: () =>
463  {
464  if (character != null && !character.Removed && character.LockHands) { return; }
465  ActiveHintMessageBox.Close();
466  });
467  }
468 
469  public static void OnRadioJammed(Item radioItem)
470  {
471  if (!CanDisplayHints()) { return; }
472  if (radioItem?.ParentInventory is not CharacterInventory characterInventory) { return; }
473  if (characterInventory.Owner != Character.Controlled) { return; }
474  DisplayHint("radiojammed".ToIdentifier());
475  }
476 
477  public static void OnReactorOutOfFuel(Reactor reactor)
478  {
479  if (!CanDisplayHints()) { return; }
480  if (reactor == null) { return; }
481  if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; }
482  if (!HasValidJob("engineer")) { return; }
483  DisplayHint("onreactoroutoffuel".ToIdentifier(), onUpdate: () =>
484  {
485  if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; }
486  ActiveHintMessageBox.Close();
487  });
488  }
489 
490  public static void OnAssignedAsTraitor()
491  {
492  if (!CanDisplayHints()) { return; }
493  DisplayHint("assignedastraitor".ToIdentifier());
494  DisplayHint("assignedastraitor2".ToIdentifier());
495  }
496 
497  public static void OnAvailableTransition(CampaignMode.TransitionType transitionType)
498  {
499  if (!CanDisplayHints()) { return; }
500  if (transitionType == CampaignMode.TransitionType.None) { return; }
501  DisplayHint($"onavailabletransition.{transitionType}".ToIdentifier());
502  }
503 
504  public static void OnShowSubInventory(Item item)
505  {
506  if (item?.Prefab == null) { return; }
507  if (item.Prefab.Identifier == "toolbelt")
508  {
509  IgnoreReminder("toolbelt");
510  }
511  }
512 
513  public static void OnChangeCharacter()
514  {
515  IgnoreReminder("characterchange");
516  }
517 
518  public static void OnCharacterUnconscious(Character character)
519  {
520  if (!CanDisplayHints()) { return; }
521  if (character != Character.Controlled) { return; }
522  if (character.IsDead) { return; }
523  if (character.CharacterHealth != null && character.Vitality < character.CharacterHealth.MinVitality) { return; }
524  DisplayHint("oncharacterunconscious".ToIdentifier());
525  }
526 
527  public static void OnCharacterKilled(Character character)
528  {
529  if (!CanDisplayHints()) { return; }
530  if (character != Character.Controlled) { return; }
531  if (GameMain.IsMultiplayer) { return; }
532  if (GameMain.GameSession?.CrewManager == null) { return; }
533  if (GameMain.GameSession.CrewManager.GetCharacters().None(c => !c.IsDead)) { return; }
534  DisplayHint("oncharacterkilled".ToIdentifier());
535  }
536 
537  private static void OnStartedControlling()
538  {
539  if (Level.IsLoadedOutpost) { return; }
540  if (Character.Controlled?.Info?.Job?.Prefab == null) { return; }
541  Identifier hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}".ToIdentifier();
542  DisplayHint(hintIdentifier,
543  icon: Character.Controlled.Info.Job.Prefab.Icon,
544  iconColor: Character.Controlled.Info.Job.Prefab.UIColor,
545  onDisplay: () =>
546  {
547  if (!HintOrders.TryGetValue(hintIdentifier, out var orderInfo)) { return; }
548  var orderPrefab = OrderPrefab.Prefabs[orderInfo.identifier];
549  if (orderPrefab == null) { return; }
550  Item targetEntity = null;
551  ItemComponent targetItem = null;
552  if (orderPrefab.MustSetTarget)
553  {
554  targetEntity = orderPrefab.GetMatchingItems(true, interactableFor: Character.Controlled, orderOption: orderInfo.option).FirstOrDefault();
555  if (targetEntity == null) { return; }
556  targetItem = orderPrefab.GetTargetItemComponent(targetEntity);
557  }
558  var order = new Order(orderPrefab, orderInfo.option, targetEntity, targetItem, orderGiver: Character.Controlled).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
559  GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order);
560  });
561  }
562 
563  public static void OnAutoPilotPathUpdated(Steering steering)
564  {
565  if (!CanDisplayHints()) { return; }
566  if (!HasValidJob("captain")) { return; }
567  if (steering?.Item?.Submarine?.Info == null) { return; }
568  if (steering.Item.Submarine.Info.Type != SubmarineType.Player) { return; }
569  if (steering.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; }
570  if (!steering.AutoPilot || steering.MaintainPos) { return; }
571  if (steering.SteeringPath?.CurrentNode?.Tunnel?.Type != Level.TunnelType.MainPath) { return; }
572  if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; }
573  if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartExit)) { return; }
574  if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndExit)) { return; }
575  DisplayHint("onautopilotreachedoutpost".ToIdentifier());
576  }
577 
578  public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character)
579  {
580  if (!CanDisplayHints()) { return; }
581  if (character != Character.Controlled) { return; }
582  // Could make this more generic if there will ever be any other status effect related hints
583  if (component is not Repairable || actionType != ActionType.OnFailure) { return; }
584  DisplayHint("onrepairfailed".ToIdentifier());
585  }
586 
587  public static void OnActiveOrderAdded(Order order)
588  {
589  if (!CanDisplayHints()) { return; }
590  if (order == null) { return; }
591 
592  if (order.Identifier == "reportballastflora" &&
593  order.TargetEntity is Hull h &&
594  h.Submarine?.TeamID == Character.Controlled.TeamID)
595  {
596  DisplayHint("onballastflorainfected".ToIdentifier());
597  }
598  if (order.Identifier == "deconstructitems" &&
599  Item.DeconstructItems.None())
600  {
601  DisplayHint("ondeconstructorder".ToIdentifier());
602  }
603  }
604 
605  public static void OnSetOrder(Character character, Order order)
606  {
607  if (!CanDisplayHints()) { return; }
608  if (character == null || order == null) { return; }
609 
610  if (order.OrderGiver == Character.Controlled &&
611  order.Identifier == "deconstructitems" &&
612  Item.DeconstructItems.None())
613  {
614  DisplayHint("ondeconstructorder".ToIdentifier());
615  }
616  }
617 
618  private static void CheckIfDivingGearOutOfOxygen()
619  {
620  if (!CanDisplayHints()) { return; }
621  var divingGear = Character.Controlled.GetEquippedItem(Tags.DivingGear, InvSlotType.OuterClothes);
622  if (divingGear?.OwnInventory == null) { return; }
623  if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; }
624  DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () =>
625  {
626  if (divingGear == null || divingGear.Removed ||
627  Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) ||
628  divingGear.GetContainedItemConditionPercentage() > 0.0f)
629  {
630  ActiveHintMessageBox.Close();
631  }
632  });
633  }
634 
635  private static void CheckHulls()
636  {
637  if (!CanDisplayHints()) { return; }
638  if (Character.Controlled.CurrentHull == null) { return; }
639  if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull))
640  {
641  if (IsOnFriendlySub() && DisplayHint("onballastflorainfected".ToIdentifier())) { return; }
642  }
643  foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps)
644  {
645  if (gap.ConnectedDoor == null || gap.ConnectedDoor.Impassable) { continue; }
646  if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, gap.ConnectedDoor.Item.WorldPosition) > 400 * 400) { continue; }
647  if (!gap.IsRoomToRoom)
648  {
649  if (!IsWearingDivingSuit()) { continue; }
650  if (Character.Controlled.IsProtectedFromPressure) { continue; }
651  if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; }
652  continue;
653  }
654  foreach (var me in gap.linkedTo)
655  {
656  if (me == Character.Controlled.CurrentHull) { continue; }
657  if (me is not Hull adjacentHull) { continue; }
658  if (!IsOnFriendlySub()) { continue; }
659  if (IsWearingDivingSuit()) { continue; }
660  if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; }
661  if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; }
662  }
663 
664  static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem(Tags.HeavyDivingGear, InvSlotType.OuterClothes) is Item;
665  }
666 
667  static bool IsOnFriendlySub() => Character.Controlled.Submarine is Submarine sub && (sub.TeamID == Character.Controlled.TeamID || sub.TeamID == CharacterTeamType.FriendlyNPC);
668  }
669 
670  private static void CheckReminders()
671  {
672  if (!CanDisplayHints()) { return; }
673  if (Level.Loaded == null) { return; }
674  if (GameMain.GameScreen.GameTime < TimeRoundStarted + TimeBeforeReminders) { return; }
675  if (GameMain.GameScreen.GameTime < TimeReminderLastDisplayed + ReminderCooldown) { return; }
676 
677  string hintIdentifierBase = "reminder";
678 
679  if (GameMain.GameSession.GameMode.IsSinglePlayer)
680  {
681  if (DisplayHint($"{hintIdentifierBase}.characterchange".ToIdentifier()))
682  {
683  TimeReminderLastDisplayed = GameMain.GameScreen.GameTime;
684  return;
685  }
686  }
687 
688  if (Level.Loaded.Type != LevelData.LevelType.Outpost)
689  {
690  if (DisplayHint($"{hintIdentifierBase}.commandinterface".ToIdentifier(),
691  variables: new[] { ("[commandkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)) },
692  onUpdate: () =>
693  {
694  if (!CrewManager.IsCommandInterfaceOpen) { return; }
695  ActiveHintMessageBox.Close();
696  }))
697  {
698  TimeReminderLastDisplayed = GameMain.GameScreen.GameTime;
699  return;
700  }
701  }
702 
703  if (DisplayHint($"{hintIdentifierBase}.tabmenu".ToIdentifier(),
704  variables: new[] { ("[infotabkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)) },
705  onUpdate: () =>
706  {
707  if (!GameSession.IsTabMenuOpen) { return; }
708  ActiveHintMessageBox.Close();
709  }))
710  {
711  TimeReminderLastDisplayed = GameMain.GameScreen.GameTime;
712  return;
713  }
714 
715  if (Character.Controlled.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt")
716  {
717  if (DisplayHint($"{hintIdentifierBase}.toolbelt".ToIdentifier()))
718  {
719  TimeReminderLastDisplayed = GameMain.GameScreen.GameTime;
720  return;
721  }
722  }
723  }
724 
725  private static bool DisplayHint(Identifier hintIdentifier, bool extendTextTag = true, (Identifier Tag, LocalizedString Value)[] variables = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null)
726  {
727  if (hintIdentifier == Identifier.Empty) { return false; }
728  if (!HintIdentifiers.Contains(hintIdentifier)) { return false; }
729  if (IgnoredHints.Instance.Contains(hintIdentifier)) { return false; }
730  if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; }
731 
732  LocalizedString text;
733  Identifier textTag = extendTextTag ? $"hint.{hintIdentifier}".ToIdentifier() : hintIdentifier;
734  if (variables != null && variables.Length > 0)
735  {
736  text = TextManager.GetWithVariables(textTag, variables);
737  }
738  else
739  {
740  text = TextManager.Get(textTag);
741  }
742 
743  if (text.IsNullOrEmpty())
744  {
745 #if DEBUG
746  DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\"");
747 #endif
748  return false;
749  }
750 
751  HintsIgnoredThisRound.Add(hintIdentifier);
752 
753  ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, TextManager.ParseInputTypes(text), icon);
754  if (iconColor.HasValue) { ActiveHintMessageBox.IconColor = iconColor.Value; }
755  OnUpdate = onUpdate;
756 
757  SoundPlayer.PlayUISound(GUISoundType.UIMessage);
758  ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f);
759  onDisplay?.Invoke();
760 
761  GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none".ToIdentifier()}:HintDisplayed:{hintIdentifier}");
762 
763  return true;
764  }
765 
766  public static bool OnDontShowAgain(GUITickBox tickBox)
767  {
768  IgnoreHint((Identifier)tickBox.UserData, ignore: tickBox.Selected);
769  return true;
770  }
771 
772  private static void IgnoreHint(Identifier hintIdentifier, bool ignore = true)
773  {
774  if (hintIdentifier.IsEmpty) { return; }
775  if (!HintIdentifiers.Contains(hintIdentifier))
776  {
777 #if DEBUG
778  DebugConsole.ThrowError($"Tried to ignore a hint not defined in {HintManagerFile}: {hintIdentifier}");
779 #endif
780  return;
781  }
782  if (ignore)
783  {
784  IgnoredHints.Instance.Add(hintIdentifier);
785  }
786  else
787  {
788  IgnoredHints.Instance.Remove(hintIdentifier);
789  }
790  }
791 
792  private static void IgnoreReminder(string reminderIdentifier)
793  {
794  HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}".ToIdentifier());
795  }
796 
797  public static bool OnDisableHints(GUITickBox tickBox)
798  {
799  var config = GameSettings.CurrentConfig;
800  config.DisableInGameHints = tickBox.Selected;
801  GameSettings.SetCurrentConfig(config);
802  GameSettings.SaveCurrentConfig();
803  return true;
804  }
805 
806  private static bool CanDisplayHints(bool requireGameScreen = true, bool requireControllingCharacter = true)
807  {
808  if (HintIdentifiers == null) { return false; }
809  if (GameSettings.CurrentConfig.DisableInGameHints) { return false; }
810  if (ActiveHintMessageBox != null) { return false; }
811  if (requireControllingCharacter && Character.Controlled == null) { return false; }
812  var gameMode = GameMain.GameSession?.GameMode;
813  if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; }
814  if (ObjectiveManager.AnyObjectives) { return false; }
815  if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; }
816  return true;
817  }
818 
819  private static bool HasValidJob(string jobIdentifier)
820  {
821  // In singleplayer, we can control all character so we don't care about job restrictions
822  if (GameMain.GameSession.GameMode.IsSinglePlayer) { return true; }
823  if (Character.Controlled.HasJob(jobIdentifier)) { return true; }
824  // In multiplayer, if there are players with the job, display the hint to all players
825  foreach (var c in GameMain.GameSession.CrewManager.GetCharacters())
826  {
827  if (c == null || !c.IsRemotePlayer) { continue; }
828  if (c.IsUnconscious || c.IsDead || c.Removed) { continue; }
829  if (!c.HasJob(jobIdentifier)) { continue; }
830  return false;
831  }
832  return true;
833  }
834  }
835 }
Submarine Submarine
Definition: Entity.cs:53
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
The base class for components holding the different functionalities of the item
GUISoundType
Definition: GUI.cs:21
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
@ Character
Characters only