using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma { static class HintManager { private const string HintManagerFile = "hintmanager.xml"; public static bool Enabled => !GameSettings.CurrentConfig.DisableInGameHints; private static HashSet HintIdentifiers { get; set; } private static Dictionary> HintTags { get; } = new Dictionary>(); private static Dictionary HintOrders { get; } = new Dictionary(); /// /// Hints that have already been shown this round and shouldn't be shown shown again until the next round /// private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); private static GUIMessageBox ActiveHintMessageBox { get; set; } private static Action OnUpdate { get; set; } private static double TimeStoppedInteracting { get; set; } private static double TimeRoundStarted { get; set; } /// /// Seconds before any reminders can be shown /// private static int TimeBeforeReminders { get; set; } /// /// Seconds before another reminder can be shown /// private static int ReminderCooldown { get; set; } private static double TimeReminderLastDisplayed { get; set; } private static HashSet BallastHulls { get; } = new HashSet(); public static void Init() { if (File.Exists(HintManagerFile)) { var doc = XMLExtensions.TryLoadXml(HintManagerFile); if (doc?.Root != null) { HintIdentifiers = new HashSet(); foreach (var element in doc.Root.Elements()) { GetHintsRecursive(element, element.NameAsIdentifier()); } } else { DebugConsole.ThrowError($"File \"{HintManagerFile}\" is empty - cannot initialize the HintManager!"); } } else { DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!"); } static void GetHintsRecursive(XElement element, Identifier identifier) { if (!element.HasElements) { HintIdentifiers.Add(identifier); if (element.GetAttributeIdentifierArray("tags", null) is Identifier[] tags) { HintTags.TryAdd(identifier, tags.ToHashSet()); } if (element.GetAttributeIdentifier("order", Identifier.Empty) is Identifier orderIdentifier && orderIdentifier != Identifier.Empty) { Identifier orderOption = element.GetAttributeIdentifier("orderoption", Identifier.Empty); HintOrders.Add(identifier, (orderIdentifier, orderOption)); } return; } else if (element.Name.ToString().Equals("reminder")) { TimeBeforeReminders = element.GetAttributeInt("timebeforereminders", TimeBeforeReminders); ReminderCooldown = element.GetAttributeInt("remindercooldown", ReminderCooldown); } foreach (var childElement in element.Elements()) { GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}".ToIdentifier()); } } } public static void Update() { if (HintIdentifiers == null || GameSettings.CurrentConfig.DisableInGameHints) { return; } if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; } if (ActiveHintMessageBox != null) { if (ActiveHintMessageBox.Closed) { ActiveHintMessageBox = null; OnUpdate = null; } else { OnUpdate?.Invoke(); return; } } CheckIsInteracting(); CheckIfDivingGearOutOfOxygen(); CheckHulls(); CheckReminders(); } public static void OnSetSelectedItem(Character character, Item oldItem, Item newItem) { if (oldItem == newItem) { return; } if (Character.Controlled != null && Character.Controlled == character && oldItem != null && !oldItem.IsLadder) { TimeStoppedInteracting = Timing.TotalTime; } if (newItem == null) { return; } if (newItem.IsLadder) { return; } if (newItem.GetComponent() is ConnectionPanel cp && cp.User == character) { return; } OnStartedInteracting(character, newItem); } private static void OnStartedInteracting(Character character, Item item) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || item == null) { return; } string hintIdentifierBase = "onstartedinteracting"; // onstartedinteracting.brokenitem if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (DisplayHint($"{hintIdentifierBase}.brokenitem".ToIdentifier())) { return; } } // Don't display other item-related hints if the repair interface is displayed if (item.Repairables.Any(r => r.ShouldDrawHUD(character))) { return; } // onstartedinteracting.lootingisstealing if (item.Submarine?.Info?.Type == SubmarineType.Outpost && item.ContainedItems.Any(i => !i.AllowStealing)) { if (DisplayHint($"{hintIdentifierBase}.lootingisstealing".ToIdentifier())) { return; } } // onstartedinteracting.turretperiscope if (item.HasTag(Tags.Periscope) && item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag(Tags.Turret)) is Turret) { if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(), variables: new[] { ("[shootkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)), ("[deselectkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)) })) { return; } } // onstartedinteracting.item... hintIdentifierBase += ".item"; foreach (Identifier hintIdentifier in HintIdentifiers) { if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; } if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; } if (!item.HasTag(hintTags)) { continue; } if (DisplayHint(hintIdentifier)) { return; } } } public static void OnStartRepairing(Character character, Repairable repairable) { if (repairable.ForceDeteriorationTimer > 0.0f && !character.IsTraitor) { CoroutineManager.Invoke(() => { DisplayHint($"repairingsabotageditem".ToIdentifier()); }, delay: 5.0f); } } public static void OnItemMarkedForRelocation() { DisplayHint($"onitemmarkedforrelocation".ToIdentifier()); } public static void OnItemMarkedForDeconstruction(Character character) { if (character == Character.Controlled) { DisplayHint($"onitemmarkedfordeconstruction".ToIdentifier()); } } private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } if (Character.Controlled?.SelectedItem == null) { return; } if (Character.Controlled.SelectedItem.GetComponent() is Reactor reactor && reactor.PowerOn && Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable containedItems && containedItems.Count(i => i.HasTag(Tags.ReactorFuel)) > 1) { if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } } public static void OnRoundStarted() { // Make sure everything's been reset properly, OnRoundEnded() isn't always called when exiting a game Reset(); TimeRoundStarted = GameMain.GameScreen.GameTime; var initRoundHandle = CoroutineManager.StartCoroutine(InitRound(), "HintManager.InitRound"); if (!CanDisplayHints(requireGameScreen: false, requireControllingCharacter: false)) { return; } CoroutineManager.StartCoroutine(DisplayRoundStartedHints(initRoundHandle), "HintManager.DisplayRoundStartedHints"); static IEnumerable InitRound() { while (Character.Controlled == null) { yield return CoroutineStatus.Running; } // Get the ballast hulls on round start not to find them again and again later BallastHulls.Clear(); var sub = Submarine.MainSubs.FirstOrDefault(s => s != null && s.TeamID == Character.Controlled.TeamID); if (sub != null) { foreach (var item in sub.GetItems(true)) { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } yield return CoroutineStatus.Success; } static IEnumerable DisplayRoundStartedHints(CoroutineHandle initRoundHandle) { while (GameMain.Instance.LoadingScreenOpen || Screen.Selected != GameMain.GameScreen || CoroutineManager.IsCoroutineRunning(initRoundHandle) || CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SinglePlayerCampaign.DoInitialCameraTransition") || CoroutineManager.IsCoroutineRunning("MultiPlayerCampaign.DoInitialCameraTransition") || GUIMessageBox.VisibleBox != null || Character.Controlled == null) { yield return CoroutineStatus.Running; } OnStartedControlling(); while (ActiveHintMessageBox != null) { yield return CoroutineStatus.Running; } if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { DisplayHint("onroundstarted.voipdisabled".ToIdentifier(), onUpdate: () => { if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { return; } ActiveHintMessageBox.Close(); }); } if (GameMain.GameSession is { TraitorsEnabled: true }) { DisplayHint("traitorsonboard".ToIdentifier()); DisplayHint("traitorsonboard2".ToIdentifier()); } yield return CoroutineStatus.Success; } } public static void OnRoundEnded() { Reset(); } private static void Reset() { CoroutineManager.StopCoroutines("HintManager.InitRound"); CoroutineManager.StopCoroutines("HintManager.DisplayRoundStartedHints"); if (ActiveHintMessageBox != null) { GUIMessageBox.MessageBoxes.Remove(ActiveHintMessageBox); ActiveHintMessageBox = null; } OnUpdate = null; HintsIgnoredThisRound.Clear(); } public static void OnSonarSpottedCharacter(Item sonar, Character spottedCharacter) { if (!CanDisplayHints()) { return; } if (sonar == null || sonar.Removed) { return; } if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } if (Character.Controlled.SelectedItem != sonar) { return; } if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } DisplayHint("onsonarspottedenemy".ToIdentifier()); } public static void OnAfflictionDisplayed(Character character, List displayedAfflictions) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || displayedAfflictions == null) { return; } foreach (var affliction in displayedAfflictions) { if (affliction?.Prefab == null) { continue; } if (affliction.Prefab.IsBuff) { continue; } if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; } if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.DepthInRadiation(character) ?? 0) > 0) { continue; } if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } DisplayHint("onafflictiondisplayed".ToIdentifier(), variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)) }, icon: affliction.Prefab.Icon, iconColor: CharacterHealth.GetAfflictionIconColor(affliction), onUpdate: () => { if (CharacterHealth.OpenHealthWindow == null) { return; } ActiveHintMessageBox.Close(); }); return; } } public static void OnShootWithoutAiming(Character character, Item item) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (character.HasSelectedAnyItem || character.FocusedItem != null) { return; } if (item == null || !item.IsShootable || !item.RequireAimToUse) { return; } if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } if (GUI.MouseOn != null) { return; } if (Character.Controlled.Inventory?.visualSlots != null && Character.Controlled.Inventory.visualSlots.Any(s => s.InteractRect.Contains(PlayerInput.MousePosition))) { return; } Identifier hintIdentifier = "onshootwithoutaiming".ToIdentifier(); if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; } if (!item.HasTag(tags)) { return; } DisplayHint(hintIdentifier, variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim)) }, onUpdate: () => { if (character.SelectedItem == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) { ActiveHintMessageBox.Close(); } }); } public static void OnWeldingDoor(Character character, Door door) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (door == null || door.Stuck < 20.0f) { return; } DisplayHint("onweldingdoor".ToIdentifier()); } public static void OnTryOpenStuckDoor(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } DisplayHint("ontryopenstuckdoor".ToIdentifier()); } public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType) { if (!CanDisplayHints()) { return; } if (interactionType == CampaignMode.InteractionType.None) { return; } Identifier hintIdentifier = $"onshowcampaigninterface.{interactionType}".ToIdentifier(); DisplayHint(hintIdentifier, onUpdate: () => { if (!(GameMain.GameSession?.Campaign is CampaignMode campaign) || (!campaign.ShowCampaignUI && !campaign.ForceMapUI) || campaign.CampaignUI?.SelectedTab != CampaignMode.InteractionType.Map) { ActiveHintMessageBox.Close(); } }); } public static void OnShowCommandInterface() { IgnoreReminder("commandinterface"); if (!CanDisplayHints()) { return; } DisplayHint("onshowcommandinterface".ToIdentifier(), onUpdate: () => { if (CrewManager.IsCommandInterfaceOpen) { return; } ActiveHintMessageBox.Close(); }); } public static void OnShowHealthInterface() { if (!CanDisplayHints()) { return; } if (CharacterHealth.OpenHealthWindow == null) { return; } DisplayHint("onshowhealthinterface".ToIdentifier(), onUpdate: () => { if (CharacterHealth.OpenHealthWindow != null) { return; } ActiveHintMessageBox.Close(); }); } public static void OnShowTabMenu() { IgnoreReminder("tabmenu"); } public static void OnObtainedItem(Character character, Item item) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || item == null) { return; } if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; } foreach (Identifier tag in item.GetTags()) { if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } } if ((item.HasTag(Tags.GeneticMaterial) && character.Inventory.FindItemByTag(Tags.GeneticMaterial, recursive: true) != null) || (item.HasTag(Tags.GeneticDevice) && character.Inventory.FindItemByTag(Tags.GeneticDevice, recursive: true) != null)) { if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } } } public static void OnStartDeconstructing(Character character, Deconstructor deconstructor) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || deconstructor == null) { return; } if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent() is not null)) { DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier()); } } public static void OnStoleItem(Character character, Item item) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (item == null || item.AllowStealing || !item.StolenDuringRound) { return; } DisplayHint("onstoleitem".ToIdentifier(), onUpdate: () => { if (item == null || item.Removed || item.GetRootInventoryOwner() != character) { ActiveHintMessageBox.Close(); } }); } public static void OnHandcuffed(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || !character.LockHands) { return; } DisplayHint("onhandcuffed".ToIdentifier(), onUpdate: () => { if (character != null && !character.Removed && character.LockHands) { return; } ActiveHintMessageBox.Close(); }); } public static void OnRadioJammed(Item radioItem) { if (!CanDisplayHints()) { return; } if (radioItem?.ParentInventory is not CharacterInventory characterInventory) { return; } if (characterInventory.Owner != Character.Controlled) { return; } DisplayHint("radiojammed".ToIdentifier()); } public static void OnReactorOutOfFuel(Reactor reactor) { if (!CanDisplayHints()) { return; } if (reactor == null) { return; } if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } if (!HasValidJob("engineer")) { return; } DisplayHint("onreactoroutoffuel".ToIdentifier(), onUpdate: () => { if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; } ActiveHintMessageBox.Close(); }); } public static void OnAssignedAsTraitor() { if (!CanDisplayHints()) { return; } DisplayHint("assignedastraitor".ToIdentifier()); DisplayHint("assignedastraitor2".ToIdentifier()); } public static void OnAvailableTransition(CampaignMode.TransitionType transitionType) { if (!CanDisplayHints()) { return; } if (transitionType == CampaignMode.TransitionType.None) { return; } DisplayHint($"onavailabletransition.{transitionType}".ToIdentifier()); } public static void OnShowSubInventory(Item item) { if (item?.Prefab == null) { return; } if (item.Prefab.Identifier == "toolbelt") { IgnoreReminder("toolbelt"); } } public static void OnChangeCharacter() { IgnoreReminder("characterchange"); } public static void OnCharacterUnconscious(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (character.IsDead) { return; } if (character.CharacterHealth != null && character.Vitality < character.CharacterHealth.MinVitality) { return; } DisplayHint("oncharacterunconscious".ToIdentifier()); } public static void OnCharacterKilled(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (GameMain.IsMultiplayer) { return; } if (GameMain.GameSession?.CrewManager == null) { return; } if (GameMain.GameSession.CrewManager.GetCharacters().None(c => !c.IsDead)) { return; } DisplayHint("oncharacterkilled".ToIdentifier()); } private static void OnStartedControlling() { if (Level.IsLoadedOutpost) { return; } if (Character.Controlled?.Info?.Job?.Prefab == null) { return; } Identifier hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}".ToIdentifier(); DisplayHint(hintIdentifier, icon: Character.Controlled.Info.Job.Prefab.Icon, iconColor: Character.Controlled.Info.Job.Prefab.UIColor, onDisplay: () => { if (!HintOrders.TryGetValue(hintIdentifier, out var orderInfo)) { return; } var orderPrefab = OrderPrefab.Prefabs[orderInfo.identifier]; if (orderPrefab == null) { return; } Item targetEntity = null; ItemComponent targetItem = null; if (orderPrefab.MustSetTarget) { targetEntity = orderPrefab.GetMatchingItems(true, interactableFor: Character.Controlled, orderOption: orderInfo.option).FirstOrDefault(); if (targetEntity == null) { return; } targetItem = orderPrefab.GetTargetItemComponent(targetEntity); } var order = new Order(orderPrefab, orderInfo.option, targetEntity, targetItem, orderGiver: Character.Controlled).WithManualPriority(CharacterInfo.HighestManualOrderPriority); GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order); }); } public static void OnAutoPilotPathUpdated(Steering steering) { if (!CanDisplayHints()) { return; } if (!HasValidJob("captain")) { return; } if (steering?.Item?.Submarine?.Info == null) { return; } if (steering.Item.Submarine.Info.Type != SubmarineType.Player) { return; } if (steering.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } if (!steering.AutoPilot || steering.MaintainPos) { return; } if (steering.SteeringPath?.CurrentNode?.Tunnel?.Type != Level.TunnelType.MainPath) { return; } if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; } if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartExit)) { return; } if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndExit)) { return; } DisplayHint("onautopilotreachedoutpost".ToIdentifier()); } public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints if (component is not Repairable || actionType != ActionType.OnFailure) { return; } DisplayHint("onrepairfailed".ToIdentifier()); } public static void OnActiveOrderAdded(Order order) { if (!CanDisplayHints()) { return; } if (order == null) { return; } if (order.Identifier == "reportballastflora" && order.TargetEntity is Hull h && h.Submarine?.TeamID == Character.Controlled.TeamID) { DisplayHint("onballastflorainfected".ToIdentifier()); } if (order.Identifier == "deconstructitems" && Item.DeconstructItems.None()) { DisplayHint("ondeconstructorder".ToIdentifier()); } } public static void OnSetOrder(Character character, Order order) { if (!CanDisplayHints()) { return; } if (character == null || order == null) { return; } if (order.OrderGiver == Character.Controlled && order.Identifier == "deconstructitems" && Item.DeconstructItems.None()) { DisplayHint("ondeconstructorder".ToIdentifier()); } } private static void CheckIfDivingGearOutOfOxygen() { if (!CanDisplayHints()) { return; } var divingGear = Character.Controlled.GetEquippedItem(Tags.DivingGear, InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () => { if (divingGear == null || divingGear.Removed || Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) || divingGear.GetContainedItemConditionPercentage() > 0.0f) { ActiveHintMessageBox.Close(); } }); } private static void CheckHulls() { if (!CanDisplayHints()) { return; } if (Character.Controlled.CurrentHull == null) { return; } if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull)) { if (IsOnFriendlySub() && DisplayHint("onballastflorainfected".ToIdentifier())) { return; } } foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) { if (gap.ConnectedDoor == null || gap.ConnectedDoor.Impassable) { continue; } if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, gap.ConnectedDoor.Item.WorldPosition) > 400 * 400) { continue; } if (!gap.IsRoomToRoom) { if (!IsWearingDivingSuit()) { continue; } if (Character.Controlled.IsProtectedFromPressure) { continue; } if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } foreach (var me in gap.linkedTo) { if (me == Character.Controlled.CurrentHull) { continue; } if (me is not Hull adjacentHull) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; } } static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem(Tags.HeavyDivingGear, InvSlotType.OuterClothes) is Item; } static bool IsOnFriendlySub() => Character.Controlled.Submarine is Submarine sub && (sub.TeamID == Character.Controlled.TeamID || sub.TeamID == CharacterTeamType.FriendlyNPC); } private static void CheckReminders() { if (!CanDisplayHints()) { return; } if (Level.Loaded == null) { return; } if (GameMain.GameScreen.GameTime < TimeRoundStarted + TimeBeforeReminders) { return; } if (GameMain.GameScreen.GameTime < TimeReminderLastDisplayed + ReminderCooldown) { return; } string hintIdentifierBase = "reminder"; if (GameMain.GameSession.GameMode.IsSinglePlayer) { if (DisplayHint($"{hintIdentifierBase}.characterchange".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; } } if (Level.Loaded.Type != LevelData.LevelType.Outpost) { if (DisplayHint($"{hintIdentifierBase}.commandinterface".ToIdentifier(), variables: new[] { ("[commandkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)) }, onUpdate: () => { if (!CrewManager.IsCommandInterfaceOpen) { return; } ActiveHintMessageBox.Close(); })) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; } } if (DisplayHint($"{hintIdentifierBase}.tabmenu".ToIdentifier(), variables: new[] { ("[infotabkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)) }, onUpdate: () => { if (!GameSession.IsTabMenuOpen) { return; } ActiveHintMessageBox.Close(); })) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; } if (Character.Controlled.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt") { if (DisplayHint($"{hintIdentifierBase}.toolbelt".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; } } } 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) { if (hintIdentifier == Identifier.Empty) { return false; } if (!HintIdentifiers.Contains(hintIdentifier)) { return false; } if (IgnoredHints.Instance.Contains(hintIdentifier)) { return false; } if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; } LocalizedString text; Identifier textTag = extendTextTag ? $"hint.{hintIdentifier}".ToIdentifier() : hintIdentifier; if (variables != null && variables.Length > 0) { text = TextManager.GetWithVariables(textTag, variables); } else { text = TextManager.Get(textTag); } if (text.IsNullOrEmpty()) { #if DEBUG DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\""); #endif return false; } HintsIgnoredThisRound.Add(hintIdentifier); ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, TextManager.ParseInputTypes(text), icon); if (iconColor.HasValue) { ActiveHintMessageBox.IconColor = iconColor.Value; } OnUpdate = onUpdate; SoundPlayer.PlayUISound(GUISoundType.UIMessage); ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f); onDisplay?.Invoke(); GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none".ToIdentifier()}:HintDisplayed:{hintIdentifier}"); return true; } public static bool OnDontShowAgain(GUITickBox tickBox) { IgnoreHint((Identifier)tickBox.UserData, ignore: tickBox.Selected); return true; } private static void IgnoreHint(Identifier hintIdentifier, bool ignore = true) { if (hintIdentifier.IsEmpty) { return; } if (!HintIdentifiers.Contains(hintIdentifier)) { #if DEBUG DebugConsole.ThrowError($"Tried to ignore a hint not defined in {HintManagerFile}: {hintIdentifier}"); #endif return; } if (ignore) { IgnoredHints.Instance.Add(hintIdentifier); } else { IgnoredHints.Instance.Remove(hintIdentifier); } } private static void IgnoreReminder(string reminderIdentifier) { HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}".ToIdentifier()); } public static bool OnDisableHints(GUITickBox tickBox) { var config = GameSettings.CurrentConfig; config.DisableInGameHints = tickBox.Selected; GameSettings.SetCurrentConfig(config); GameSettings.SaveCurrentConfig(); return true; } private static bool CanDisplayHints(bool requireGameScreen = true, bool requireControllingCharacter = true) { if (HintIdentifiers == null) { return false; } if (GameSettings.CurrentConfig.DisableInGameHints) { return false; } if (ActiveHintMessageBox != null) { return false; } if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } if (ObjectiveManager.AnyObjectives) { return false; } if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } return true; } private static bool HasValidJob(string jobIdentifier) { // In singleplayer, we can control all character so we don't care about job restrictions if (GameMain.GameSession.GameMode.IsSinglePlayer) { return true; } if (Character.Controlled.HasJob(jobIdentifier)) { return true; } // In multiplayer, if there are players with the job, display the hint to all players foreach (var c in GameMain.GameSession.CrewManager.GetCharacters()) { if (c == null || !c.IsRemotePlayer) { continue; } if (c.IsUnconscious || c.IsDead || c.Removed) { continue; } if (!c.HasJob(jobIdentifier)) { continue; } return false; } return true; } } }