using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Barotrauma { abstract partial class CampaignMode : GameMode { protected bool crewDead; protected Color overlayColor; protected LocalizedString overlayText, overlayTextBottom; protected Color overlayTextColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; protected GUIButton endRoundButton; public GUIButton ReadyCheckButton; public GUIButton EndRoundButton => endRoundButton; protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; public static CancellationTokenSource StartRoundCancellationToken { get; private set; } public bool ForceMapUI { get; protected set; } private bool showCampaignUI; private bool wasChatBoxOpen; public bool ShowCampaignUI { get { return showCampaignUI; } set { if (value == showCampaignUI) { return; } var chatBox = CrewManager?.ChatBox ?? GameMain.Client?.ChatBox; if (value) { if (chatBox != null) { wasChatBoxOpen = chatBox.ToggleOpen; chatBox.ToggleOpen = false; } } else if (chatBox != null) { chatBox.ToggleOpen = wasChatBoxOpen; } if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) { SubmarinePreview.Close(); } showCampaignUI = value; } } /// /// Gets the current personal wallet /// In singleplayer this is the campaign bank and in multiplayer this is the personal wallet /// public virtual Wallet Wallet => GetWallet(); public override void ShowStartMessage() { foreach (Mission mission in Missions.ToList()) { new GUIMessageBox( RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" }; } } /// /// There is a server-side implementation of the method in /// public bool AllowedToManageCampaign(ClientPermissions permissions) { //allow managing the round if the client has permissions, is the owner, the only client in the server, //or if no-one has management permissions if (GameMain.Client == null) { return true; } return GameMain.Client.HasPermission(permissions) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); } public static bool AllowedToManageWallets() { if (GameMain.Client == null) { return true; } return GameMain.Client.HasPermission(ClientPermissions.ManageMoney) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.IsServerOwner; } public override void Draw(SpriteBatch spriteBatch) { if (overlayColor.A > 0) { if (overlaySprite != null) { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Black * (overlayColor.A / 255.0f), isFilled: true); float scale = Math.Max(GameMain.GraphicsWidth / overlaySprite.size.X, GameMain.GraphicsHeight / overlaySprite.size.Y); overlaySprite.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2, overlayColor, overlaySprite.size / 2, scale: scale); } else { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) { var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); Vector2 textPos = centerPos - textSize / 2; backgroundSprite.Draw(spriteBatch, centerPos, Color.White * (overlayTextColor.A / 255.0f), origin: backgroundSprite.size / 2, rotate: 0.0f, scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 1.5f)); GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); if (!overlayTextBottom.IsNullOrEmpty()) { Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); } } } if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; if (ReadyCheckButton != null) { ReadyCheckButton.Visible = false; } return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; } endRoundButton.Visible = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); LocalizedString buttonText = ""; switch (availableTransition) { case TransitionType.ProgressToNextLocation: case TransitionType.ProgressToNextEmptyLocation: if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousEmptyLocation: if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } else { endRoundButton.Visible = false; } break; } if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } if (endRoundButton.Visible) { if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) { buttonText = TextManager.Get("map"); } else if (prevCampaignUIAutoOpenType != availableTransition && (availableTransition == TransitionType.ProgressToNextEmptyLocation || availableTransition == TransitionType.ReturnToPreviousEmptyLocation)) { HintManager.OnAvailableTransition(availableTransition); //opening the campaign map pauses the game and prevents HintManager from running -> update it manually to get the hint to show up immediately HintManager.Update(); Map.SelectLocation(-1); endRoundButton.OnClicked(EndRoundButton, null); prevCampaignUIAutoOpenType = availableTransition; } endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; } if (Character.Controlled?.CharacterHealth?.SuicideButton?.Visible ?? false) { endRoundButton.RectTransform.ScreenSpaceOffset = new Point(0, Character.Controlled.CharacterHealth.SuicideButton.Rect.Height); } else if (GameMain.Client != null && GameMain.Client.IsFollowSubTickBoxVisible) { endRoundButton.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.Padding + GameMain.Client.FollowSubTickBox.Rect.Height); } else { endRoundButton.RectTransform.ScreenSpaceOffset = Point.Zero; } } endRoundButton.DrawManually(spriteBatch); if (this is MultiPlayerCampaign && ReadyCheckButton != null) { ReadyCheckButton.RectTransform.ScreenSpaceOffset = endRoundButton.RectTransform.ScreenSpaceOffset; ReadyCheckButton.DrawManually(spriteBatch); if (ReadyCheck.ReadyCheckCooldown > DateTime.Now) { float progress = (ReadyCheck.ReadyCheckCooldown - DateTime.Now).Seconds / 60.0f; ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUIStyle.Red); } } } public Task SelectSummaryScreen(RoundSummary roundSummary, LevelData newLevel, bool mirror, Action action) { var roundSummaryScreen = RoundSummaryScreen.Select(overlaySprite, roundSummary); GUI.ClearCursorWait(); StartRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); } catch (Exception e) { roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; }, StartRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; action?.Invoke(); }); return loadTask; } partial void NPCInteractProjSpecific(Character npc, Character interactor) { if (npc == null || interactor == null) { return; } switch (npc.CampaignInteractionType) { case InteractionType.None: case InteractionType.Talk: case InteractionType.Examine: return; case InteractionType.Upgrade when !UpgradeManager.CanUpgradeSub(): UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc); return; case InteractionType.Crew when GameMain.NetworkMember != null: CampaignUI.CrewManagement.SendCrewState(false); goto default; case InteractionType.MedicalClinic: CampaignUI.MedicalClinic.RequestLatestPending(); goto default; default: ShowCampaignUI = true; CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); CampaignUI.UpgradeStore?.RequestRefresh(); break; } } public override void AddToGUIUpdateList() { if (ShowCampaignUI || ForceMapUI) { campaignUIContainer?.AddToGUIUpdateList(); if (CampaignUI?.UpgradeStore?.HoveredEntity != null) { if (CampaignUI.SelectedTab != InteractionType.Upgrade) { return; } CampaignUI?.UpgradeStore?.ItemInfoFrame.AddToGUIUpdateList(order: 1); } } base.AddToGUIUpdateList(); CrewManager.AddToGUIUpdateList(); endRoundButton.AddToGUIUpdateList(); ReadyCheckButton?.AddToGUIUpdateList(); } protected void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen) { Submarine.MainSub.CheckFuel(); SubmarineInfo nextSub = PendingSubmarineSwitch ?? Submarine.MainSub.Info; bool lowFuel = nextSub.Name == Submarine.MainSub.Info.Name ? Submarine.MainSub.Info.LowFuel : nextSub.LowFuel; if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains("reactorfuel")))) { var extraConfirmationBox = new GUIMessageBox(TextManager.Get("lowfuelheader"), TextManager.Get("lowfuelwarning"), new LocalizedString[2] { TextManager.Get("ok"), TextManager.Get("cancel") }); extraConfirmationBox.Buttons[0].OnClicked = (b, o) => { Confirm(); return true; }; extraConfirmationBox.Buttons[0].OnClicked += extraConfirmationBox.Close; extraConfirmationBox.Buttons[1].OnClicked = extraConfirmationBox.Close; } else { Confirm(); } void Confirm() { var availableTransition = GetAvailableTransition(out _, out _); if (Character.Controlled != null && availableTransition == TransitionType.ReturnToPreviousLocation && Character.Controlled?.Submarine == Level.Loaded?.StartOutpost) { onConfirm(); } else if (Character.Controlled != null && availableTransition == TransitionType.ProgressToNextLocation && Character.Controlled?.Submarine == Level.Loaded?.EndOutpost) { onConfirm(); } else { onReturnToMapScreen(); } } } public override void Update(float deltaTime) { base.Update(deltaTime); MedicalClinic?.Update(deltaTime); if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); } if (ShowCampaignUI || ForceMapUI) { CampaignUI?.Update(deltaTime); } } } }