using Barotrauma.Extensions; 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 { internal abstract partial class CampaignMode : GameMode { public bool CrewDead { get; protected set; } protected Color overlayColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; protected GUIButton endRoundButton; public GUIButton ReadyCheckButton; public GUIButton EndRoundButton => endRoundButton; protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; public SlideshowPlayer SlideshowPlayer { get; protected set; } private CancellationTokenSource startRoundCancellationToken; 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) { switch (CampaignUI?.SelectedTab) { case InteractionType.PurchaseSub: SubmarinePreview.Close(); break; case InteractionType.MedicalClinic: CampaignUI.MedicalClinic?.OnDeselected(); break; case InteractionType.Store: CampaignUI.Store?.OnDeselected(); break; } } 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()) { if (!mission.Prefab.ShowStartMessage) { continue; } 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" }; } } private static bool IsOwner(Client client) => client != null && client.IsOwner; /// /// There is a server-side implementation of the method in /// public static 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.IsServerOwner || AnyOneAllowedToManageCampaign(permissions); } public static bool AllowedToManageWallets() { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } public static bool AllowImmediateItemDelivery() { if (GameMain.Client == null) { return true; } return GameMain.Client.ServerSettings.AllowImmediateItemDelivery || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.IsServerOwner; } protected GUIButton CreateEndRoundButton() { int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); int buttonHeight = (int)(40 * GUI.yScale); var rectT = HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y, buttonWidth, buttonHeight), GUI.Canvas); rectT.Pivot = Pivot.Center; return new GUIButton(rectT, TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, TextBlock = { Shadow = true, AutoScaleHorizontal = true } }; } 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); } } SlideshowPlayer?.DrawManually(spriteBatch); 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; } bool allowEndingRound = false; endRoundButton.Color = endRoundButton.Style.Color; endRoundButton.HoverColor = endRoundButton.Style.HoverColor; RichString overrideEndRoundButtonToolTip = string.Empty; 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?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !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?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.None: default: bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true }; if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome && (inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub)) { overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub"); endRoundButton.Color = GUIStyle.Red * 0.7f; endRoundButton.HoverColor = GUIStyle.Red; } buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } else { allowEndingRound = false; } break; } if (Level.IsLoadedOutpost && (!ObjectiveManager.AllActiveObjectivesCompleted() && this is not MultiPlayerCampaign)) { allowEndingRound = false; } if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0f; } endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) { if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) { buttonText = TextManager.Get("map"); } else if (prevCampaignUIAutoOpenType != availableTransition && availableTransition == TransitionType.ProgressToNextEmptyLocation) { 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 (overrideEndRoundButtonToolTip != string.Empty) { endRoundButton.ToolTip = overrideEndRoundButtonToolTip; } else 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 = Environment.CurrentManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); } catch (Exception e) { roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; startRoundCancellationToken = null; }, startRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; action?.Invoke(); }); return loadTask; } public void CancelStartRound() { startRoundCancellationToken?.Cancel(); } public void ThrowIfStartRoundCancellationRequested() { if (startRoundCancellationToken != null && startRoundCancellationToken.Token.IsCancellationRequested) { startRoundCancellationToken.Token.ThrowIfCancellationRequested(); startRoundCancellationToken = null; } } 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.HRManagerUI.SendCrewState(false); goto default; case InteractionType.MedicalClinic: CampaignUI.MedicalClinic.RequestLatestPending(); goto default; default: ShowCampaignUI = true; CampaignUI.SelectTab(npc.CampaignInteractionType, npc); CampaignUI.UpgradeStore?.RequestRefresh(); break; } if (npc.AIController is HumanAIController humanAi && humanAi.IsInHostileFaction()) { npc.Speak(TextManager.Get("dialoglowrepcampaigninteraction").Value, identifier: "dialoglowrepcampaigninteraction".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } } 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(); bool lowFuel = Submarine.MainSub.Info.LowFuel; if (PendingSubmarineSwitch != null) { lowFuel = TransferItemsOnSubSwitch ? (lowFuel && PendingSubmarineSwitch.LowFuel) : PendingSubmarineSwitch.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 DEBUG if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) { if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } GUIFrame summaryFrame = GameMain.GameSession.RoundSummary.CreateSummaryFrame(GameMain.GameSession, ""); GUIMessageBox.MessageBoxes.Add(summaryFrame); GameMain.GameSession.RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; } #endif if (ShowCampaignUI || ForceMapUI) { CampaignUI?.Update(deltaTime); } } } }