diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index ddf2b6bc6..7fd2cfdc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -551,6 +551,7 @@ namespace Barotrauma { bool hasOwner = inc.ReadBoolean(); int ownerId = hasOwner ? inc.ReadByte() : -1; + float humanPrefabHealthMultiplier = inc.ReadSingle(); int balance = inc.ReadInt32(); int rewardDistribution = inc.ReadRangedInteger(0, 100); byte teamID = inc.ReadByte(); @@ -573,6 +574,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 91e852beb..5c563220d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -6,7 +6,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -1345,6 +1344,7 @@ namespace Barotrauma { UserData = item, DisabledColor = Color.White * 0.1f, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } @@ -1352,6 +1352,7 @@ namespace Barotrauma if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 166979300..088fb2725 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -108,6 +108,15 @@ namespace Barotrauma } } + public void RemoveFile(File file) + { + if (HasFile(file)) + { + files.Remove(file); + DiscardHashAndInstallTime(); + } + } + public void DiscardHashAndInstallTime() { ExpectedHash = null; @@ -144,7 +153,7 @@ namespace Barotrauma => rootElement.Add(new XAttribute(name, value.ToString() ?? "")); addRootAttribute("name", Name); - addRootAttribute("modversion", ModVersion); + if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } addRootAttribute("corepackage", IsCore); if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } addRootAttribute("gameversion", GameMain.Version); diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs new file mode 100644 index 000000000..43fe28e83 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ModMerger.cs @@ -0,0 +1,133 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Steam; +using Barotrauma.IO; + +namespace Barotrauma +{ + public static class ModMerger + { + public static void AskMerge(ContentPackage[] mods) + { + ErrorIfNonLocal(mods); + + var msgBox = new GUIMessageBox(TextManager.Get("MergeModsHeader"), "", relativeSize: (0.5f, 0.8f), + buttons: new LocalizedString[] { TextManager.Get("ConfirmModMerge"), TextManager.Get("Cancel") }); + msgBox.Buttons[1].OnClicked = msgBox.Close; + + var desc = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsDesc")); + var modsList = new GUIListBox(new RectTransform((1.0f, 0.5f), msgBox.Content.RectTransform)) + { + OnSelected = (component, o) => false, + HoverCursor = CursorState.Default + }; + foreach (var mod in mods) + { + new GUITextBlock(new RectTransform((1.0f, 0.11f), modsList.Content.RectTransform), mod.Name) + { + CanBeFocused = false + }; + } + var footer = new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform), TextManager.Get("MergeModsFooter")); + var resultName = new GUITextBox(new RectTransform((1.0f, 0.1f), msgBox.Content.RectTransform)) + { + Text = (mods.Count(m => m.Files.Length > 1)==1) + ? mods.First(m => m.Files.Length > 1).Name + : "" + }; + + void flashText() + { + resultName!.Select(); + resultName.Flash(GUIStyle.Red); + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (string.IsNullOrEmpty(resultName.Text)) + { + flashText(); + return false; + } + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName.Text}"; + + bool dirMatches(ContentPackage mod) + => mod.Dir.CleanUpPathCrossPlatform(correctFilenameCase: false) + .Equals(targetDir, StringComparison.OrdinalIgnoreCase); + if (ContentPackageManager.LocalPackages.Any(dirMatches) + && !mods.Any(dirMatches)) + { + flashText(); + return false; + } + + MergeMods(mods, resultName.Text); + msgBox.Close(); + return false; + }; + } + + private static void MergeMods(ContentPackage[] mods, string resultName) + { + ModProject resultProject = new ModProject + { + Name = resultName + }; + + string targetDir = $"{ContentPackage.LocalModsDir}/{resultName}"; + Directory.CreateDirectory(targetDir); + + foreach (var mod in mods) + { + foreach (var file in Directory.GetFiles(mod.Dir, "*", System.IO.SearchOption.AllDirectories) + .Select(f => f.CleanUpPathCrossPlatform(correctFilenameCase: false))) + { + if (Path.GetFileName(file).Equals(ContentPackage.FileListFileName, StringComparison.OrdinalIgnoreCase)) { continue; } + + string targetFilePath = file[mod.Dir.Length..]; + if (targetFilePath.StartsWith("/") || targetFilePath.StartsWith("\\")) + { + targetFilePath = targetFilePath[1..]; + } + + targetFilePath = Path.Combine(targetDir, targetFilePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + //DebugConsole.NewMessage(targetFilePath); + + Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!); + File.Copy(file, targetFilePath, overwrite: true); + + var oldFileInProject = resultProject.Files.FirstOrDefault(f + => f.Path.Equals(targetFilePath, StringComparison.OrdinalIgnoreCase)); + if (oldFileInProject != null) + { + resultProject.RemoveFile(oldFileInProject); + } + + var fileInMod = mod.Files.Find(f => f.Path == file); + if (fileInMod != null) + { + var newFileInProject = ModProject.File.FromPath(targetFilePath, fileInMod.GetType()); + resultProject.AddFile(newFileInProject); + } + } + } + resultProject.Save(Path.Combine(targetDir, ContentPackage.FileListFileName)); + + foreach (var mod in mods) + { + Directory.Delete(mod.Dir); + } + (SettingsMenu.Instance!.WorkshopMenu as MutableWorkshopMenu)!.PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); + } + + private static void ErrorIfNonLocal(ContentPackage[] mods) + { + var nonLocal = mods.Where(m => !ContentPackageManager.LocalPackages.Contains(m)).ToArray(); + if (nonLocal.Any()) + { + throw new Exception($"{string.Join(", ", nonLocal.Select(m => m.Name))} are not local mods"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs index 795b4ae24..748995b91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,9 +8,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Steam; using Microsoft.Xna.Framework; -using Directory = Barotrauma.IO.Directory; -using File = Barotrauma.IO.File; -using Path = Barotrauma.IO.Path; +using Barotrauma.IO; namespace Barotrauma.Transition { @@ -258,13 +255,13 @@ namespace Barotrauma.Transition { string[] getFiles(string path, string pattern) => Directory.Exists(path) - ? Directory.GetFiles(path, pattern, SearchOption.TopDirectoryOnly) + ? Directory.GetFiles(path, pattern, System.IO.SearchOption.TopDirectoryOnly) : Array.Empty(); subs = getFiles(oldSubsPath, "*.sub"); itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml"); - string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", SearchOption.TopDirectoryOnly); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly); var publishedItems = await SteamManager.Workshop.GetPublishedItems(); foreach (var modDir in allOldMods) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index c1ee89fb8..1a780c3ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -107,6 +107,7 @@ namespace Barotrauma var buttonLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -150,6 +151,7 @@ namespace Barotrauma var buttonRight = new GUIButton(new RectTransform(new Vector2(0.1f, 0.8f), channelSettingsContent.RectTransform), style: "DeviceButton") { + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -178,6 +180,7 @@ namespace Barotrauma TextColor = new Color(51, 59, 46), SelectedTextColor = GUIStyle.Green, UserData = i, + PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) @@ -357,10 +360,15 @@ namespace Barotrauma CanBeFocused = true, ForceUpperCase = ForceUpperCase.No, UserData = message.SenderClient, + PlaySoundOnSelect = false, OnClicked = (_, o) => { if (!(o is Client client)) { return false; } - GameMain.NetLobbyScreen?.SelectPlayer(client); + if (GameMain.NetLobbyScreen != null) + { + GameMain.NetLobbyScreen.SelectPlayer(client); + SoundPlayer.PlayUISound(GUISoundType.Select); + } return true; }, OnSecondaryClicked = (_, o) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index e99e15745..d5707afa4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -178,7 +178,14 @@ namespace Barotrauma return Sprites.ContainsKey(state) ? Sprites[state]?.First()?.Sprite : null; } - public void GetSize(XElement element) + public void RefreshSize() + { + Width = null; + Height = null; + GetSize(Element); + } + + private void GetSize(XElement element) { Point size = new Point(0, 0); foreach (var subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 43b772d1c..3aa3511df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -193,12 +193,13 @@ namespace Barotrauma }; validateHiresButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { - ClickSound = GUISoundType.HireRepairClick, + ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() @@ -403,6 +404,7 @@ namespace Barotrauma { var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) @@ -429,6 +431,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = HasPermission, OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 71023ab04..4fcd29809 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -182,7 +182,10 @@ namespace Barotrauma window = new GUIFrame(new RectTransform(Vector2.One * 0.8f, backgroundFrame.RectTransform, Anchor.Center)); var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true); - sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)); + sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)) + { + PlaySoundOnSelect = true + }; var drives = System.IO.DriveInfo.GetDrives(); foreach (var drive in drives) @@ -241,6 +244,7 @@ namespace Barotrauma fileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), fileListLayout.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (child, userdata) => { if (userdata is null) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index ddddc7fc9..251799b66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -24,15 +24,17 @@ namespace Barotrauma ChatMessage, RadioMessage, DeadMessage, - Click, + Select, PickItem, PickItemFail, DropItem, PopupMenu, - DecreaseQuantity, - IncreaseQuantity, - HireRepairClick, - UISwitch + Decrease, + Increase, + UISwitch, + TickBox, + ConfirmTransaction, + Cart, } public enum CursorState @@ -2384,7 +2386,7 @@ namespace Barotrauma CreateButton("PauseMenuResume", buttonContainer, null); CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true); - bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; + bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) @@ -2399,11 +2401,11 @@ namespace Barotrauma GameMain.GameSession.LoadPreviousSave(); }); - if (IsOutpostLevel()) + if (IsFriendlyOutpostLevel()) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { - if (IsOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } + if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } }); } } @@ -2416,7 +2418,7 @@ namespace Barotrauma } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { - bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsOutpostLevel(); + bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel(); if (canSave) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 1846bc809..09e8107b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -159,7 +159,9 @@ namespace Barotrauma private float pulseExpand; private bool flashed; - public GUISoundType ClickSound { get; set; } = GUISoundType.Click; + public GUISoundType ClickSound { get; set; } = GUISoundType.Select; + + public override bool PlaySoundOnSelect { get; set; } = true; public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } @@ -247,7 +249,10 @@ namespace Barotrauma } else if (PlayerInput.PrimaryMouseButtonClicked()) { - SoundPlayer.PlayUISound(ClickSound); + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(ClickSound); + } if (OnClicked != null) { if (OnClicked(this, UserData)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index deb5dab1c..dff86c500 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -383,6 +383,8 @@ namespace Barotrauma public bool ExternalHighlight = false; + public virtual bool PlaySoundOnSelect { get; set; } = false; + private RectTransform rectTransform; public RectTransform RectTransform { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 8b8e09f16..40eae2afa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -113,7 +113,8 @@ namespace Barotrauma { AutoHideScrollBar = false, ScrollBarVisible = false, - Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding + Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding, + PlaySoundOnSelect = true }; foreach (var (option, size) in optionsAndSizes) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index d34f6bf91..389456b9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -183,7 +183,8 @@ namespace Barotrauma listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) { IsFixedSize = false }, style: null) { - Enabled = !selectMultiple + Enabled = !selectMultiple, + PlaySoundOnSelect = true, }; if (!selectMultiple) { listBox.OnSelected = SelectItem; } GUIStyle.Apply(listBox, "GUIListBox", this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 0089c3e94..8e43f9c6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -309,6 +309,45 @@ namespace Barotrauma } } + public override bool PlaySoundOnSelect { get; set; } = false; + + public bool PlaySoundOnDragStop { get; set; } = false; + + public GUISoundType? SoundOnDragStart { get; set; } = null; + + public GUISoundType? SoundOnDragStop { get; set; } = null; + + #region enums + public enum Force + { + Yes, + No + } + + public enum AutoScroll + { + Enabled, + Disabled + } + + public enum TakeKeyBoardFocus + { + Yes, + No + } + + public enum PlaySelectSound + { + Yes, + No + } + + private AutoScroll GetAutoScroll(bool b) + { + return b ? AutoScroll.Enabled : AutoScroll.Disabled; + } + #endregion + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { @@ -396,7 +435,7 @@ namespace Barotrauma UpdateScrollBarSize(); } - public void Select(object userData, bool force = false, bool autoScroll = true) + public void Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled) { var children = Content.Children; int i = 0; @@ -515,9 +554,12 @@ namespace Barotrauma /// Scrolls the list to the specific element. /// /// - public void ScrollToElement(GUIComponent component, bool playSound = true) + public void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound = PlaySelectSound.No) { - if (playSound) { SoundPlayer.PlayUISound(GUISoundType.Click); } + if (playSelectSound == PlaySelectSound.Yes) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } @@ -573,9 +615,16 @@ namespace Barotrauma } } + private double lastDragStartTime; + private void StartDraggingElement(GUIComponent child) { DraggedElement = child; + if (Timing.TotalTime > lastDragStartTime + 0.2f) + { + lastDragStartTime = Timing.TotalTime; + SoundPlayer.PlayUISound(SoundOnDragStart); + } } private bool UpdateDragging() @@ -586,6 +635,10 @@ namespace Barotrauma var draggedElem = draggedElement; OnRearranged?.Invoke(this, draggedElem.UserData); DraggedElement = null; + if (PlaySoundOnDragStop) + { + SoundPlayer.PlayUISound(SoundOnDragStop); + } RepositionChildren(); if (AllSelected.Contains(draggedElem)) { return true; } } @@ -710,7 +763,7 @@ namespace Barotrauma int index = Content.Children.ToList().IndexOf(component); if (index >= 0) { - Select(index, false, false, takeKeyBoardFocus: true); + Select(index, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes); } } } @@ -733,7 +786,7 @@ namespace Barotrauma { ScrollToElement(child); } - Select(i, autoScroll: false, takeKeyBoardFocus: true); + Select(i, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } if (CurrentDragMode != DragMode.NoDragging @@ -929,14 +982,13 @@ namespace Barotrauma if (ClampScrollToElements) { bool scrollDown = Math.Clamp(PlayerInput.ScrollWheelSpeed, 0, 1) > 0; - if (scrollDown) { - SelectPrevious(takeKeyBoardFocus: true); + SelectPrevious(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } else { - SelectNext(takeKeyBoardFocus: true); + SelectNext(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } } } @@ -964,7 +1016,7 @@ namespace Barotrauma return FindScrollableParentListBox(target.Parent); } - public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectNext(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex + 1; while (index < Content.CountChildren) @@ -972,10 +1024,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -983,7 +1035,7 @@ namespace Barotrauma } } - public void SelectPrevious(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void SelectPrevious(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex - 1; while (index >= 0) @@ -991,10 +1043,10 @@ namespace Barotrauma GUIComponent child = Content.GetChild(index); if (child.Visible) { - Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); + Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { - ScrollToElement(child); + ScrollToElement(child, playSelectSound); } break; } @@ -1002,7 +1054,7 @@ namespace Barotrauma } } - public void Select(int childIndex, bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) + public void Select(int childIndex, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { if (childIndex >= Content.CountChildren || childIndex < 0) { return; } @@ -1013,7 +1065,7 @@ namespace Barotrauma if (OnSelected != null) { // TODO: The callback is called twice, fix this! - wasSelected = force || OnSelected(child, child.UserData); + wasSelected = force == Force.Yes || OnSelected(child, child.UserData); } if (!wasSelected) { return; } @@ -1055,7 +1107,7 @@ namespace Barotrauma // Ensure that the selected element is visible. This may not be the case, if the selection is run from code. (e.g. if we have two list boxes that are synced) // TODO: This method only works when moving one item up/down (e.g. when using the up and down arrows) - if (autoScroll) + if (autoScroll == AutoScroll.Enabled) { if (ScrollBar.IsHorizontal) { @@ -1086,11 +1138,19 @@ namespace Barotrauma } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. - if (takeKeyBoardFocus && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) + if (takeKeyBoardFocus == TakeKeyBoardFocus.Yes && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; } + + // List box child components can be parents to other components that can play sounds when selected (e.g. store elements) + // so the list box shouldn't play the Select sound if the GUI.MouseOn component has a sound to play + if (playSelectSound == PlaySelectSound.Yes && PlaySoundOnSelect && !child.PlaySoundOnSelect && + (GUI.MouseOn == null || GUI.MouseOn.Parent == Content || !GUI.MouseOn.PlaySoundOnSelect)) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Select(IEnumerable children) @@ -1293,16 +1353,16 @@ namespace Barotrauma switch (key) { case Keys.Down: - if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Up: - if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Left: - if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Right: - if (isHorizontal && AllowArrowKeyScroll) { SelectNext(); } + if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Enter: case Keys.Space: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 7e049e601..f3802cca6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -182,7 +182,7 @@ namespace Barotrauma public float valueStep; private float pressedTimer; - private float pressedDelay = 0.5f; + private readonly float pressedDelay = 0.5f; private bool IsPressedTimerRunning { get { return pressedTimer > 0; } } public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null, bool hidePlusMinusButtons = false) : base(style, rectT) @@ -228,6 +228,7 @@ namespace Barotrauma var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); GUIStyle.Apply(PlusButton, "PlusButton", this); + PlusButton.ClickSound = GUISoundType.Increase; PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -249,6 +250,7 @@ namespace Barotrauma MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); GUIStyle.Apply(MinusButton, "MinusButton", this); + MinusButton.ClickSound = GUISoundType.Decrease; MinusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -423,8 +425,8 @@ namespace Barotrauma intValue = Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = intValue < MaxValueInt; - MinusButton.Enabled = intValue > MinValueInt; + PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt; } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 1487d6943..137eee850 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -98,7 +98,6 @@ namespace Barotrauma foreach (var subElement in element.Elements().Reverse()) { if (subElement.NameAsIdentifier() != "override") { continue; } - if (subElement.GetAttributeBool("iscjk", false)) { return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); @@ -111,8 +110,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeContentPath("file")?.Value; } @@ -125,8 +123,7 @@ namespace Barotrauma //check if any of the language override fonts want to override the font size as well foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { uint overrideFontSize = GetFontSize(subElement, 0); if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.CurrentConfig.Graphics.TextScale); } @@ -149,8 +146,7 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("dynamicloading", false); } @@ -162,14 +158,20 @@ namespace Barotrauma { foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + if (IsValidOverride(subElement)) { return subElement.GetAttributeBool("iscjk", false); } } return element.GetAttributeBool("iscjk", false); } + + private bool IsValidOverride(XElement element) + { + if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; } + var languages = element.GetAttributeIdentifierArray("language", Array.Empty()); + return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language); + } } public class GUIFont : GUISelector diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 1a17d1124..30ec4af6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -322,9 +322,8 @@ namespace Barotrauma { if (!enabled || !PlayerInput.PrimaryMouseButtonDown()) { return false; } if (barSize >= 1.0f) { return false; } - DraggingBar = this; - + SoundPlayer.PlayUISound(GUISoundType.Select); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 850f347bb..d81ee4206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -34,7 +34,6 @@ namespace Barotrauma public readonly static PrefabCollection ComponentStyles = new PrefabCollection(); public readonly static GUIFont Font = new GUIFont("Font"); - public readonly static GUIFont GlobalFont = new GUIFont("GlobalFont"); public readonly static GUIFont UnscaledSmallFont = new GUIFont("UnscaledSmallFont"); public readonly static GUIFont SmallFont = new GUIFont("SmallFont"); public readonly static GUIFont LargeFont = new GUIFont("LargeFont"); @@ -142,10 +141,6 @@ namespace Barotrauma public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public readonly static GUIColor EquipmentIndicatorNotEquipped = new GUIColor("EquipmentIndicatorNotEquipped"); - public readonly static GUIColor EquipmentIndicatorEquipped = new GUIColor("EquipmentIndicatorEquipped"); - public readonly static GUIColor EquipmentIndicatorRunningOut = new GUIColor("EquipmentIndicatorRunningOut"); - public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); @@ -159,7 +154,7 @@ namespace Barotrauma public static void Apply(GUIComponent targetComponent, Identifier styleName, GUIComponent parent = null) { - GUIComponentStyle componentStyle = null; + GUIComponentStyle componentStyle; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index d48669710..fcbf3a5f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -251,6 +251,8 @@ namespace Barotrauma public bool Readonly { get; set; } + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -363,6 +365,10 @@ namespace Barotrauma selected = true; GUI.KeyboardDispatcher.Subscriber = this; OnSelected?.Invoke(this, Keys.None); + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(GUISoundType.Select); + } } public void Deselect() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 05e59d5fc..47ce9cab1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -1,15 +1,13 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; namespace Barotrauma { public class GUITickBox : GUIComponent { - private GUILayoutGroup layoutGroup; - private GUIFrame box; - private GUITextBlock text; + private readonly GUILayoutGroup layoutGroup; + private readonly GUIFrame box; + private readonly GUITextBlock text; public delegate bool OnSelectedHandler(GUITickBox obj); public OnSelectedHandler OnSelected; @@ -129,6 +127,12 @@ namespace Barotrauma set { text.Text = value; } } + public float ContentWidth { get; private set; } + + public GUISoundType SoundType { private get; set; } = GUISoundType.TickBox; + + public override bool PlaySoundOnSelect { get; set; } = true; + public GUITickBox(RectTransform rectT, LocalizedString label, GUIFont font = null, string style = "") : base(null, rectT) { CanBeFocused = true; @@ -180,6 +184,7 @@ namespace Barotrauma box.RectTransform.MinSize = new Point(Rect.Height); box.RectTransform.Resize(box.RectTransform.MinSize); text.SetTextPos(); + ContentWidth = box.Rect.Width + text.Padding.X + text.TextSize.X + text.Padding.Z; } protected override void Update(float deltaTime) @@ -209,6 +214,10 @@ namespace Barotrauma { Selected = true; } + if (PlaySoundOnSelect) + { + SoundPlayer.PlayUISound(SoundType); + } } } else if (isSelected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ad3b9c4d8..f255c8f5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -122,7 +122,7 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.58f); + int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.3f); int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index a86a2ca4c..54373bcff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -569,6 +569,7 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("medicalclinic.heal")) { + ClickSound = GUISoundType.ConfirmTransaction, Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetBalance() >= medicalClinic.GetTotalCost(), OnClicked = (button, _) => { @@ -595,6 +596,7 @@ namespace Barotrauma GUIButton clearButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("campaignstore.clearall")) { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -684,6 +686,7 @@ namespace Barotrauma GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), style: "CrewManagementRemoveButton") { + ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { button.Enabled = false; @@ -766,6 +769,7 @@ namespace Barotrauma GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) { + ClickSound = GUISoundType.Cart, Font = GUIStyle.SubHeadingFont, Visible = false }; @@ -887,7 +891,10 @@ namespace Barotrauma GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.25f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.SubHeadingFont); - GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); + GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton") + { + ClickSound = GUISoundType.Cart + }; ImmutableArray elementsToDisable = ImmutableArray.Create(prefabBlock, backgroundFrame, icon, vitalityBlock, severityBlock, buyButton, descriptionBlock, priceBlock); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index d54f1a9f5..105bd7a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -390,7 +390,7 @@ namespace Barotrauma ToolTip = TextManager.Get("campaignstore.reputationtooltip") }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), - TextManager.Get("reputation"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("reputationmodifier"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, @@ -656,7 +656,7 @@ namespace Barotrauma SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = HasActiveTabPermissions(), ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => @@ -1567,8 +1567,6 @@ namespace Barotrauma } AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity); }; - amountInput.PlusButton.ClickSound = GUISoundType.IncreaseQuantity; - amountInput.MinusButton.ClickSound = GUISoundType.DecreaseQuantity; frame.HoverColor = frame.SelectedColor = Color.Transparent; } @@ -1622,7 +1620,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { - ClickSound = GUISoundType.IncreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable && pi.Quantity > 0, ForceUpperCase = ForceUpperCase.Yes, UserData = "addbutton", @@ -1633,7 +1631,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton") { - ClickSound = GUISoundType.DecreaseQuantity, + ClickSound = GUISoundType.Cart, Enabled = !forceDisable, ForceUpperCase = ForceUpperCase.Yes, UserData = "removebutton", @@ -2076,11 +2074,13 @@ namespace Barotrauma { if (IsBuying) { + confirmButton.ClickSound = GUISoundType.ConfirmTransaction; confirmButton.Text = TextManager.Get("CampaignStore.Purchase"); confirmButton.OnClicked = (b, o) => BuyItems(); } else { + confirmButton.ClickSound = GUISoundType.Select; confirmButton.Text = TextManager.Get("CampaignStoreTab.Sell"); confirmButton.OnClicked = (b, o) => { @@ -2088,6 +2088,7 @@ namespace Barotrauma TextManager.Get("FireWarningHeader"), TextManager.Get("CampaignStore.SellWarningText"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDialog.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems(); confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 707e3580a..28de3502a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -29,6 +29,8 @@ namespace Barotrauma private GUITextBlock descriptionTextBlock; private int selectionIndicatorThickness; private GUIImage listBackground; + private GUITickBox transferItemsTickBox; + private GUITextBlock itemTransferReminderBlock; private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; @@ -61,6 +63,23 @@ namespace Barotrauma public GUIButton previewButton; } + private bool TransferItemsOnSwitch + { + get + { + return transferItemsOnSwitch; + } + set + { + transferItemsOnSwitch = value; + if (transferItemsTickBox != null) + { + transferItemsTickBox.Selected = value; + } + } + } + private bool transferItemsOnSwitch = true; + public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) { if (GameMain.GameSession.Campaign == null) { return; } @@ -149,11 +168,12 @@ namespace Barotrauma GUIListBox descriptionFrame = new GUIListBox(new RectTransform(new Vector2(0.59f, 1f), infoFrame.RectTransform), style: null) { Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding * 1.5f, HUDLayoutSettings.Padding / 2f) }; descriptionTextBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionFrame.Content.RectTransform), string.Empty, font: GUIStyle.Font, wrap: true) { CanBeFocused = false }; - GUILayoutGroup buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), content.RectTransform), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + GUILayoutGroup bottomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), content.RectTransform, Anchor.CenterRight), childAnchor: Anchor.CenterRight) { IsHorizontal = true, AbsoluteSpacing = HUDLayoutSettings.Padding }; + float transferInfoFrameWidth = 1.0f; if (closeAction != null) { - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), TextManager.Get("Close"), style: "GUIButtonFreeScale") { OnClicked = (button, userData) => { @@ -161,11 +181,33 @@ namespace Barotrauma return true; } }; + transferInfoFrameWidth -= closeButton.RectTransform.RelativeSize.X; } - if (purchaseService) confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), buttonFrame.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + if (purchaseService) + { + confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); + transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; + } + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); + transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; + GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) + { + CanBeFocused = false + }; + transferItemsTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 1.0f), transferInfoFrame.RectTransform, Anchor.CenterRight), TextManager.Get("transferitems"), font: GUIStyle.SubHeadingFont) + { + Selected = TransferItemsOnSwitch, + Visible = false, + OnSelected = (tb) => transferItemsOnSwitch = tb.Selected + }; + transferItemsTickBox.RectTransform.Resize(new Point(Math.Min((int)transferItemsTickBox.ContentWidth, transferInfoFrame.Rect.Width), transferItemsTickBox.Rect.Height)); + itemTransferReminderBlock = new GUITextBlock(new RectTransform(Vector2.One, transferInfoFrame.RectTransform, Anchor.CenterRight), null) + { + TextAlignment = Alignment.CenterRight, + Visible = false + }; pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); pageIndicator = GUIStyle.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); @@ -272,7 +314,7 @@ namespace Barotrauma } } - public void RefreshSubmarineDisplay(bool updateSubs) + public void RefreshSubmarineDisplay(bool updateSubs, bool setTransferOptionToTrue = false) { if (!initialized) { @@ -286,6 +328,10 @@ namespace Barotrauma { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (setTransferOptionToTrue) + { + TransferItemsOnSwitch = true; + } if (updateSubs) { UpdateSubmarines(); @@ -401,6 +447,10 @@ namespace Barotrauma { SelectSubmarine(null, Rectangle.Empty); } + else + { + UpdateItemTransferInfoFrame(); + } } private void UpdateSubmarines() @@ -553,6 +603,40 @@ namespace Barotrauma selectedSubmarineIndicator.RectTransform.NonScaledSize = Point.Zero; SetConfirmButtonState(false); } + + UpdateItemTransferInfoFrame(); + } + + private void UpdateItemTransferInfoFrame() + { + if (selectedSubmarine != null) + { + var pendingSub = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch; + if (Submarine.MainSub?.Info?.Name == selectedSubmarine.Name && pendingSub == null) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } + else if (pendingSub?.Name == selectedSubmarine.Name) + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Text = GameMain.GameSession.Campaign.TransferItemsOnSubSwitch ? + TextManager.Get("itemtransferenabledreminder") : + TextManager.Get("itemtransferdisabledreminder"); + itemTransferReminderBlock.Visible = true; + } + else + { + transferItemsTickBox.Selected = TransferItemsOnSwitch; + transferItemsTickBox.Visible = true; + itemTransferReminderBlock.Visible = false; + } + } + else + { + transferItemsTickBox.Visible = false; + itemTransferReminderBlock.Visible = false; + } } private void SetConfirmButtonState(bool state) @@ -614,24 +698,27 @@ namespace Barotrauma ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), ("[amount]", deliveryFee.ToString()), ("[currencyname]", currencyName)), messageBoxOptions); + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; } else { - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", + var text = TextManager.GetWithVariables("switchsubmarinetext", ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)), messageBoxOptions); + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.SwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.SwitchSub); } return true; }; @@ -653,23 +740,25 @@ namespace Barotrauma if (!purchaseOnly) { - msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", + var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), ("[currencyname]", currencyName), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); + text += GetItemTransferText(); + msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), text, messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); RefreshSubmarineDisplay(true); } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseAndSwitchSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, TransferItemsOnSwitch, Networking.VoteType.PurchaseAndSwitchSub); } return true; }; @@ -690,14 +779,20 @@ namespace Barotrauma } else { - GameMain.Client.InitiateSubmarineChange(selectedSubmarine, Networking.VoteType.PurchaseSub); + GameMain.Client.InitiateSubmarineChange(selectedSubmarine, false, Networking.VoteType.PurchaseSub); } return true; }; } + msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = msgBox.Close; - } + } + + private LocalizedString GetItemTransferText() + { + return "\n\n" + TextManager.Get(TransferItemsOnSwitch ? "itemswillbetransferred" : "itemswontbetransferred"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8a71ba121..cbf116f0b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -360,7 +360,7 @@ namespace Barotrauma var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); talentsButton.OnAddedToGUIUpdateList += (component) => { - talentsButton.Enabled = Character.Controlled?.Info != null || (GameMain.Client?.CharacterInfo != null && GameMain.GameSession?.GameMode is MultiPlayerCampaign); + talentsButton.Enabled = Character.Controlled?.Info != null || GameMain.Client?.CharacterInfo != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { SelectInfoFrameTab(InfoFrameTab.Crew); @@ -453,7 +453,8 @@ namespace Barotrauma GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform)) { Padding = new Vector4(2, 5, 0, 0), - AutoHideScrollBar = false + AutoHideScrollBar = false, + PlaySoundOnSelect = true }; crewList.UpdateDimensions(); @@ -928,8 +929,8 @@ namespace Barotrauma } else { - Vector2 stringOffset = GUIStyle.GlobalFont.MeasureString(inLobbyString) / 2f; - GUIStyle.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + Vector2 stringOffset = GUIStyle.Font.MeasureString(inLobbyString) / 2f; + GUIStyle.Font.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); } } @@ -1914,6 +1915,7 @@ namespace Barotrauma { OnClicked = (button, o) => { + GameMain.Client?.SendCharacterInfo(); characterSettingsFrame!.Visible = false; talentFrameMain.Visible = true; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f694cb4d5..6833b74d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -462,7 +462,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -497,7 +497,7 @@ namespace Barotrauma button.Enabled = false; } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -539,7 +539,7 @@ namespace Barotrauma GameMain.Client?.SendCampaignState(); } return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); } else { @@ -589,7 +589,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); @@ -622,7 +622,8 @@ namespace Barotrauma PadBottom = true, SelectTop = true, ClampScrollToElements = true, - Spacing = 8 + Spacing = 8, + PlaySoundOnSelect = true }; Dictionary> upgrades = new Dictionary>(); @@ -1123,7 +1124,10 @@ namespace Barotrauma { priceText.Text = string.Empty; } - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) { Enabled = false }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) + { + Enabled = false + }; if (upgradePrefab != null) { var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); @@ -1212,7 +1216,7 @@ namespace Barotrauma Campaign.UpgradeManager.PurchaseUpgrade(prefab, category); GameMain.Client?.SendCampaignState(); return true; - }); + }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction); return true; }; @@ -1400,7 +1404,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { - ScrollToCategory(data => data.Category.IsWallUpgrade); + ScrollToCategory(data => data.Category.IsWallUpgrade, GUIListBox.PlaySelectSound.Yes); } } } @@ -1682,7 +1686,7 @@ namespace Barotrauma } } - private void ScrollToCategory(Predicate predicate) + private void ScrollToCategory(Predicate predicate, GUIListBox.PlaySelectSound playSelectSound = GUIListBox.PlaySelectSound.No) { if (currentStoreLayout == null) { return; } @@ -1690,7 +1694,7 @@ namespace Barotrauma { if (child.UserData is CategoryData data && predicate(data)) { - currentStoreLayout.ScrollToElement(child); + currentStoreLayout.ScrollToElement(child, playSelectSound); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index e54913559..7d83f6e99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -26,7 +26,7 @@ namespace Barotrauma private Color SubmarineColor => GUIStyle.Orange; private Point createdForResolution; - public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) + public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime) { if (starter == null || info == null) { return null; } @@ -38,7 +38,7 @@ namespace Barotrauma getMaxVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountMax(type) ?? 0, }; subVoting.onVoteEnd = () => subVoting.SendSubmarineVoteEndMessage(info, type); - subVoting.SetSubmarineVotingText(starter, info, type); + subVoting.SetSubmarineVotingText(starter, info, transferItems, type); subVoting.Initialize(starter, type); return subVoting; } @@ -160,19 +160,21 @@ namespace Barotrauma } #region Submarine Voting - private void SetSubmarineVotingText(Client starter, SubmarineInfo info, VoteType type) + + private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; string characterRichString = $"‖color:{nameColor.R},{nameColor.G},{nameColor.B}‖{name}‖color:end‖"; string submarineRichString = $"‖color:{SubmarineColor.R},{SubmarineColor.G},{SubmarineColor.B}‖{info.DisplayName}‖color:end‖"; - + string tag = string.Empty; LocalizedString text = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - text = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + tag = transferItems ? "submarinepurchaseandswitchwithitemsvote" : "submarinepurchaseandswitchvote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[amount]", info.Price.ToString()), @@ -189,7 +191,8 @@ namespace Barotrauma int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - text = TextManager.GetWithVariables("submarineswitchfeevote", + tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[locationname]", endLocation.Name), @@ -198,13 +201,13 @@ namespace Barotrauma } else { - text = TextManager.GetWithVariables("submarineswitchnofeevote", + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString)); } break; } - votingOnText = RichString.Rich(text); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 188c91bed..01b319c25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -943,6 +943,23 @@ namespace Barotrauma Timing.Accumulator = 0.0f; } + private void FixRazerCortex() + { +#if WINDOWS + //Razer Cortex's overlay is broken. + //For whatever reason, it messes up the blendstate and, + //because MonoGame reasonably assumes that you don't need + //to touch it if you're setting it to the exact same one + //you were already using, it doesn't fix Razer's mess. + //Therefore, we need to change the blendstate TWICE: + //once to force MonoGame to change it, and then again to + //use the blendstate we actually want. + var oldBlendState = GraphicsDevice.BlendState; + GraphicsDevice.BlendState = oldBlendState == BlendState.Opaque ? BlendState.NonPremultiplied : BlendState.Opaque; + GraphicsDevice.BlendState = oldBlendState; +#endif + } + /// /// This is called when the game should draw itself. /// @@ -950,7 +967,9 @@ namespace Barotrauma { Stopwatch sw = new Stopwatch(); sw.Start(); - + + FixRazerCortex(); + double deltaTime = gameTime.ElapsedGameTime.TotalSeconds; if (Timing.FrameLimit > 0) @@ -1043,7 +1062,7 @@ namespace Barotrauma } // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) + if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); spCampaign.CargoManager.ClearSoldItemsProjSpecific(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 7dbeffd05..52fa7e3cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1608,7 +1608,7 @@ namespace Barotrauma { if (character == Character.Controlled && crewList.SelectedComponent != characterComponent) { - crewList.Select(character, force: true); + crewList.Select(character, GUIListBox.Force.Yes); } // Icon colors might change based on the target so we check if they need to be updated if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bde737f0..41d89b0d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -587,196 +587,78 @@ namespace Barotrauma //static because we may need to instantiate the campaign if it hasn't been done yet public static void ClientRead(IReadMessage msg) { + NetFlags requiredFlags = (NetFlags)msg.ReadUInt16(); + bool isFirstRound = msg.ReadBoolean(); byte campaignID = msg.ReadByte(); - UInt16 updateID = msg.ReadUInt16(); UInt16 saveID = msg.ReadUInt16(); string mapSeed = msg.ReadString(); - UInt16 currentLocIndex = msg.ReadUInt16(); - UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionCount = msg.ReadByte(); - List selectedMissionIndices = new List(); - for (int i = 0; i < selectedMissionCount; i++) - { - selectedMissionIndices.Add(msg.ReadByte()); - } - - ushort ownedSubCount = msg.ReadUInt16(); - List ownedSubIndices = new List(); - for (int i = 0; i < ownedSubCount; i++) - { - ownedSubIndices.Add(msg.ReadUInt16()); - } - - bool allowDebugTeleport = msg.ReadBoolean(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } - - Dictionary factionReps = new Dictionary(); - byte factionsCount = msg.ReadByte(); - for (int i = 0; i < factionsCount; i++) - { - factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); - } - - bool forceMapUI = msg.ReadBoolean(); - - bool purchasedHullRepairs = msg.ReadBoolean(); - bool purchasedItemRepairs = msg.ReadBoolean(); - bool purchasedLostShuttles = msg.ReadBoolean(); - - byte missionCount = msg.ReadByte(); - var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); - for (int i = 0; i < missionCount; i++) - { - Identifier missionIdentifier = msg.ReadIdentifier(); - byte connectionIndex = msg.ReadByte(); - availableMissions.Add((missionIdentifier, connectionIndex)); - } - - var storeBalances = new Dictionary(); - if (msg.ReadBoolean()) - { - byte storeCount = msg.ReadByte(); - for (int i = 0; i < storeCount; i++) - { - Identifier identifier = msg.ReadIdentifier(); - UInt16 storeBalance = msg.ReadUInt16(); - storeBalances.Add(identifier, storeBalance); - } - } - - var buyCrateItems = ReadPurchasedItems(msg, sender: null); - var subSellCrateItems = ReadPurchasedItems(msg, sender: null); - var purchasedItems = ReadPurchasedItems(msg, sender: null); - var soldItems = ReadSoldItems(msg); - - ushort pendingUpgradeCount = msg.ReadUInt16(); - List pendingUpgrades = new List(); - for (int i = 0; i < pendingUpgradeCount; i++) - { - Identifier upgradeIdentifier = msg.ReadIdentifier(); - UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - Identifier categoryIdentifier = msg.ReadIdentifier(); - UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); - int upgradeLevel = msg.ReadByte(); - if (prefab == null || category == null) { continue; } - pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); - } - - ushort purchasedItemSwapCount = msg.ReadUInt16(); - List purchasedItemSwaps = new List(); - for (int i = 0; i < purchasedItemSwapCount; i++) - { - UInt16 itemToRemoveID = msg.ReadUInt16(); - Identifier itemToInstallIdentifier = msg.ReadIdentifier(); - ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } - purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); - } - - bool hasCharacterData = msg.ReadBoolean(); - CharacterInfo myCharacterInfo = null; - if (hasCharacterData) - { - myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); - } + bool refreshCampaignUI = false; if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure, mapSeed); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); } //server has a newer save file - if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) - { - campaign.PendingSaveID = saveID; - } - - if (NetIdUtils.IdMoreRecent(updateID, campaign.lastUpdateID)) - { - campaign.SuppressStateSending = true; - campaign.IsFirstRound = isFirstRound; + if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { campaign.PendingSaveID = saveID; } + campaign.IsFirstRound = isFirstRound; - //we need to have the latest save file to display location/mission/store - if (campaign.LastSaveID == saveID) + if (requiredFlags.HasFlag(NetFlags.Misc)) + { + DebugConsole.Log("Received campaign update (Misc)"); + UInt16 id = msg.ReadUInt16(); + bool purchasedHullRepairs = msg.ReadBoolean(); + bool purchasedItemRepairs = msg.ReadBoolean(); + bool purchasedLostShuttles = msg.ReadBoolean(); + if (ShouldApply(NetFlags.Misc, id, requireUpToDateSave: false)) + { + refreshCampaignUI = campaign.PurchasedHullRepairs != purchasedHullRepairs || + campaign.PurchasedItemRepairs != purchasedItemRepairs || + campaign.PurchasedLostShuttles != purchasedLostShuttles; + campaign.PurchasedHullRepairs = purchasedHullRepairs; + campaign.PurchasedItemRepairs = purchasedItemRepairs; + campaign.PurchasedLostShuttles = purchasedLostShuttles; + } + } + + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) + { + DebugConsole.Log("Received campaign update (MapAndMissions)"); + UInt16 id = msg.ReadUInt16(); + bool forceMapUI = msg.ReadBoolean(); + bool allowDebugTeleport = msg.ReadBoolean(); + UInt16 currentLocIndex = msg.ReadUInt16(); + UInt16 selectedLocIndex = msg.ReadUInt16(); + + byte missionCount = msg.ReadByte(); + var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); + for (int i = 0; i < missionCount; i++) + { + Identifier missionIdentifier = msg.ReadIdentifier(); + byte connectionIndex = msg.ReadByte(); + availableMissions.Add((missionIdentifier, connectionIndex)); + } + + byte selectedMissionCount = msg.ReadByte(); + List selectedMissionIndices = new List(); + for (int i = 0; i < selectedMissionCount; i++) + { + selectedMissionIndices.Add(msg.ReadByte()); + } + + if (ShouldApply(NetFlags.MapAndMissions, id, requireUpToDateSave: true)) { campaign.ForceMapUI = forceMapUI; - - UpgradeStore.WaitForServerUpdate = false; - + campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - campaign.Map.SelectMission(selectedMissionIndices); - - GameMain.GameSession.OwnedSubmarines.Clear(); - foreach (int ownedSubIndex in ownedSubIndices) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) - { - GameMain.GameSession.OwnedSubmarines.Add(sub); - } - } - - campaign.Map.AllowDebugTeleport = allowDebugTeleport; - campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); - campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); - campaign.CargoManager.SetPurchasedItems(purchasedItems); - campaign.CargoManager.SetSoldItems(soldItems); - foreach (var balance in storeBalances) - { - if (campaign.Map.CurrentLocation.GetStore(balance.Key) is { } store) - { - store.Balance = balance.Value; - } - } - campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); - foreach (var purchasedItemSwap in purchasedItemSwaps) - { - if (purchasedItemSwap.ItemToInstall == null) - { - campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); - } - else - { - campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); - } - } - foreach (Item item in Item.ItemList) - { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) - { - item.PendingItemSwap = null; - } - } - - foreach (var (identifier, rep) in factionReps) - { - Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); - if (faction?.Reputation != null) - { - faction.Reputation.SetReputation(rep); - } - else - { - DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); - } - } - - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - foreach (var availableMission in availableMissions) { MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); @@ -800,36 +682,268 @@ namespace Barotrauma campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } - - GameMain.NetLobbyScreen.ToggleCampaignMode(true); - } - - bool shouldRefresh = campaign.PurchasedHullRepairs != purchasedHullRepairs || - campaign.PurchasedItemRepairs != purchasedItemRepairs || - campaign.PurchasedLostShuttles != purchasedLostShuttles; - - campaign.PurchasedHullRepairs = purchasedHullRepairs; - campaign.PurchasedItemRepairs = purchasedItemRepairs; - campaign.PurchasedLostShuttles = purchasedLostShuttles; - - if (shouldRefresh) - { - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } - - if (myCharacterInfo != null) - { - GameMain.Client.CharacterInfo = myCharacterInfo; - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + campaign.Map.SelectMission(selectedMissionIndices); + ReadStores(msg, apply: true); } else { - GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + ReadStores(msg, apply: false); + } + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + DebugConsole.Log("Received campaign update (SubList)"); + UInt16 id = msg.ReadUInt16(); + ushort ownedSubCount = msg.ReadUInt16(); + List ownedSubIndices = new List(); + for (int i = 0; i < ownedSubCount; i++) + { + ownedSubIndices.Add(msg.ReadUInt16()); } - campaign.lastUpdateID = updateID; - campaign.SuppressStateSending = false; + if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false)) + { + GameMain.GameSession.OwnedSubmarines.Clear(); + foreach (int ownedSubIndex in ownedSubIndices) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) + { + GameMain.GameSession.OwnedSubmarines.Add(sub); + } + } + } } + + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) + { + DebugConsole.Log("Received campaign update (UpgradeManager)"); + UInt16 id = msg.ReadUInt16(); + + ushort pendingUpgradeCount = msg.ReadUInt16(); + List pendingUpgrades = new List(); + for (int i = 0; i < pendingUpgradeCount; i++) + { + Identifier upgradeIdentifier = msg.ReadIdentifier(); + UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); + Identifier categoryIdentifier = msg.ReadIdentifier(); + UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); + int upgradeLevel = msg.ReadByte(); + if (prefab == null || category == null) { continue; } + pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); + } + + ushort purchasedItemSwapCount = msg.ReadUInt16(); + List purchasedItemSwaps = new List(); + for (int i = 0; i < purchasedItemSwapCount; i++) + { + UInt16 itemToRemoveID = msg.ReadUInt16(); + Identifier itemToInstallIdentifier = msg.ReadIdentifier(); + ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } + purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + } + + if (ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) + { + UpgradeStore.WaitForServerUpdate = false; + campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); + campaign.UpgradeManager.PurchasedUpgrades.Clear(); + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); + } + else + { + campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + item.PendingItemSwap = null; + } + } + } + } + + + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInBuyCrate)"); + UInt16 id = msg.ReadUInt16(); + var buyCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) + { + DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate)"); + UInt16 id = msg.ReadUInt16(); + var subSellCrateItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); + campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) + { + DebugConsole.Log("Received campaign update (PuchasedItems)"); + UInt16 id = msg.ReadUInt16(); + var purchasedItems = ReadPurchasedItems(msg, sender: null); + if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetPurchasedItems(purchasedItems); + campaign.SetLastUpdateIdForFlag(NetFlags.PurchasedItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.SoldItems)) + { + DebugConsole.Log("Received campaign update (SoldItems)"); + UInt16 id = msg.ReadUInt16(); + var soldItems = ReadSoldItems(msg); + if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true)) + { + campaign.CargoManager.SetSoldItems(soldItems); + campaign.SetLastUpdateIdForFlag(NetFlags.SoldItems, id); + ReadStores(msg, apply: true); + } + else + { + ReadStores(msg, apply: false); + } + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + DebugConsole.Log("Received campaign update (Reputation)"); + UInt16 id = msg.ReadUInt16(); + float? reputation = null; + if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } + Dictionary factionReps = new Dictionary(); + byte factionsCount = msg.ReadByte(); + for (int i = 0; i < factionsCount; i++) + { + factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); + } + if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) + { + if (reputation.HasValue) + { + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + foreach (var (identifier, rep) in factionReps) + { + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); + if (faction?.Reputation != null) + { + faction.Reputation.SetReputation(rep); + } + else + { + DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); + } + } + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + DebugConsole.Log("Received campaign update (CharacterInfo)"); + UInt16 id = msg.ReadUInt16(); + bool hasCharacterData = msg.ReadBoolean(); + CharacterInfo myCharacterInfo = null; + if (hasCharacterData) + { + myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); + } + if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) + { + if (myCharacterInfo != null) + { + GameMain.Client.CharacterInfo = myCharacterInfo; + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); + } + else + { + GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); + } + } + } + + campaign.SuppressStateSending = true; + //we need to have the latest save file to display location/mission/store + if (campaign.LastSaveID == saveID) + { + GameMain.NetLobbyScreen.ToggleCampaignMode(true); + } + if (refreshCampaignUI) + { + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); + } + campaign.SuppressStateSending = false; + + bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave) + { + if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) && + (!requireUpToDateSave || saveID == campaign.LastSaveID)) + { + campaign.SetLastUpdateIdForFlag(flag, id); + return true; + } + else + { + return false; + } + } + + void ReadStores(IReadMessage msg, bool apply) + { + var storeBalances = new Dictionary(); + if (msg.ReadBoolean()) + { + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier identifier = msg.ReadIdentifier(); + UInt16 storeBalance = msg.ReadUInt16(); + storeBalances.Add(identifier, storeBalance); + } + } + if (apply) + { + foreach (var balance in storeBalances) + { + if (campaign.Map?.CurrentLocation?.GetStore(balance.Key) is { } store) + { + store.Balance = balance.Value; + } + } + } + } + } public void ClientReadCrew(IReadMessage msg) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 382c43a58..ed5e25c86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -58,12 +58,12 @@ namespace Barotrauma /// /// Instantiates a new single player campaign /// - private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - map = new Map(this, mapSeed, settings); Settings = settings; + map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) @@ -79,7 +79,7 @@ namespace Barotrauma /// /// Loads a previously saved single player campaign from XML /// - private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign) + private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; @@ -87,7 +87,7 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); break; case "crew": @@ -95,7 +95,7 @@ namespace Barotrauma ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "map": - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); break; case "metadata": CampaignMetadata = new CampaignMetadata(this, subElement); @@ -163,21 +163,14 @@ namespace Barotrauma /// /// Start a completely new single player campaign /// - public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) - { - var campaign = new SinglePlayerCampaign(mapSeed, settings); - return campaign; - } + public static SinglePlayerCampaign StartNew(string mapSeed, CampaignSettings startingSettings) => new SinglePlayerCampaign(mapSeed, startingSettings); /// /// Load a previously saved single player campaign from xml /// /// /// - public static SinglePlayerCampaign Load(XElement element) - { - return new SinglePlayerCampaign(element); - } + public static SinglePlayerCampaign Load(XElement element) => new SinglePlayerCampaign(element); private void InitUI() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 2245ad99d..490516616 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -64,7 +64,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; public static int Spacing; - public static int HideButtonWidth; private Layout layout; public Layout CurrentLayout @@ -77,64 +76,11 @@ namespace Barotrauma SetSlotPositions(layout); } } - public bool Hidden { get; set; } - - private bool hidePersonalSlots; - private float hidePersonalSlotsState; - private GUIButton hideButton; + private Rectangle personalSlotArea; - public bool HidePersonalSlots - { - get { return hidePersonalSlots; } - } - - public Rectangle PersonalSlotArea - { - get { return personalSlotArea; } - } - - private readonly GUIImage[] indicators = new GUIImage[5]; - private readonly int[] indicatorIndices = new int[5]; - private Vector2 indicatorSpriteSize; - private GUILayoutGroup indicatorGroup; - partial void InitProjSpecific(XElement element) { - Hidden = true; - - hideButton = new GUIButton(new RectTransform(new Point((int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)), HUDLayoutSettings.BottomRightInfoArea.Height), GUI.Canvas) - { AbsoluteOffset = HUDLayoutSettings.CrewArea.Location }, - "", style: "EquipmentToggleButton"); - - indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; - indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUIStyle.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; - - indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); - indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); - indicators[2] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorOutfit"); - indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); - indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); - - indicatorIndices[0] = FindLimbSlot(InvSlotType.OuterClothes); - indicatorIndices[1] = FindLimbSlot(InvSlotType.Card); - indicatorIndices[2] = FindLimbSlot(InvSlotType.InnerClothes); - indicatorIndices[3] = FindLimbSlot(InvSlotType.Head); - indicatorIndices[4] = FindLimbSlot(InvSlotType.Headset); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].CanBeFocused = false; - } - - hideButton.OnClicked += (GUIButton btn, object userdata) => - { - hidePersonalSlots = !hidePersonalSlots; - return true; - }; - hidePersonalSlots = false; - SlotPositions = new Vector2[SlotTypes.Length]; CurrentLayout = Layout.Default; SetSlotPositions(layout); @@ -271,25 +217,6 @@ namespace Barotrauma return false; } - private void SetIndicatorSizes() - { - indicatorGroup.RectTransform.AbsoluteOffset = new Point((int)Math.Round(4 * GUI.Scale), (int)Math.Round(7 * GUI.Scale)); - indicatorGroup.RectTransform.NonScaledSize = new Point(hideButton.Rect.Width - indicatorGroup.RectTransform.AbsoluteOffset.X * 2, hideButton.Rect.Height - indicatorGroup.RectTransform.AbsoluteOffset.Y * 2); - indicatorGroup.AbsoluteSpacing = (int)Math.Ceiling(2 * GUI.Scale); - - int indicatorHeight = (indicatorGroup.RectTransform.NonScaledSize.Y - indicatorGroup.AbsoluteSpacing * (indicators.Length - 1)) / indicators.Length; - int indicatorWidth = (int)(indicatorSpriteSize.X / (indicatorSpriteSize.Y / indicatorHeight)); - - if (HideButtonWidth % 2 != indicatorWidth % 2) indicatorWidth++; - - Point indicatorSize = new Point(indicatorWidth, indicatorHeight); - - for (int i = 0; i < indicators.Length; i++) - { - indicators[i].RectTransform.NonScaledSize = indicatorSize; - } - } - private void SetSlotPositions(Layout layout) { bool isFourByThree = GUI.IsFourByThree(); @@ -302,13 +229,9 @@ namespace Barotrauma Spacing = (int)(8 * UIScale); } - HideButtonWidth = (int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; - hideButton.Visible = false; - if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -320,7 +243,7 @@ namespace Barotrauma int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); @@ -343,16 +266,6 @@ namespace Barotrauma x += SlotSize.X + Spacing; } } - - if (hideButtonSlotIndex > -1) - { - hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); - hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); - hideButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.BottomRightInfoArea.Left - HideButtonWidth + GUI.IntScaleCeiling(2f), HUDLayoutSettings.BottomRightInfoArea.Y + GUI.IntScaleCeiling(1f)); - hideButton.Visible = Screen.Selected != GameMain.SubEditorScreen || !GameMain.SubEditorScreen.WiringMode; - - SetIndicatorSizes(); - } } break; case Layout.Right: @@ -533,58 +446,13 @@ namespace Barotrauma bool hoverOnInventory = GUI.MouseOn == null && ((selectedSlot != null && selectedSlot.IsSubSlot) || (DraggingItems.Any() && (DraggingSlot == null || !DraggingSlot.MouseOn()))); - if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; - - if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) - { - if (hideButton.Visible) - { - hideButton.AddToGUIUpdateList(); - hideButton.UpdateManually(deltaTime, alsoChildren: true); - - hidePersonalSlotsState = hidePersonalSlots ? - Math.Min(hidePersonalSlotsState + deltaTime * 5.0f, 1.0f) : - Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); - - bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; - for (int i = 0; i < visualSlots.Length; i++) - { - if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } - if (HidePersonalSlots) - { - if (selectedSlot?.Slot == visualSlots[i]) { selectedSlot = null; } - highlightedSubInventorySlots.RemoveWhere(s => s.Slot == visualSlots[i]); - } - visualSlots[i].IsMoving = personalSlotsMoving; - visualSlots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); - } - } - } + if (CharacterHealth.OpenHealthWindow != null) { hoverOnInventory = true; } if (hoverOnInventory) { HideTimer = 0.5f; } if (HideTimer > 0.0f) { HideTimer -= deltaTime; } UpdateSlotInput(); - //force personal slots open if an item is running out of battery/fuel/oxygen/etc - if (hidePersonalSlots) - { - for (int i = 0; i < visualSlots.Length; i++) - { - var item = slots[i].FirstOrDefault(); - if (item?.OwnInventory != null && item.OwnInventory.Capacity == 1 && PersonalSlots.HasFlag(SlotTypes[i])) - { - var containedItem = item.OwnInventory.AllItems.FirstOrDefault(); - if (containedItem != null && - containedItem.Condition > 0.0f && - containedItem.Condition / containedItem.MaxCondition < 0.15f) - { - hidePersonalSlots = false; - } - } - } - } - hideSubInventories.Clear(); //remove highlighted subinventory slots that can no longer be accessed highlightedSubInventorySlots.RemoveWhere(s => @@ -653,8 +521,6 @@ namespace Barotrauma if (character == Character.Controlled && character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { - UpdateEquipmentIndicators(); - //remove the highlighted slots of other characters' inventories when not grabbing anyone highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); @@ -799,40 +665,6 @@ namespace Barotrauma } } } - - private void UpdateEquipmentIndicators() - { - for (int i = 0; i < indicators.Length; i++) - { - if (indicatorIndices[i] < 0) { continue; } - Item item = slots[indicatorIndices[i]].FirstOrDefault(); - if (item != null) - { - Wearable wearable = item.GetComponent(); - if (wearable != null && wearable.DisplayContainedStatus) - { - float conditionPercentage = item.GetContainedItemConditionPercentage(); - - if (conditionPercentage != -1) - { - indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUIStyle.EquipmentIndicatorRunningOut, GUIStyle.EquipmentIndicatorEquipped); - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorRunningOut; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorEquipped; - } - } - else - { - indicators[i].Color = GUIStyle.EquipmentIndicatorNotEquipped; - } - } - } private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { @@ -942,6 +774,7 @@ namespace Barotrauma } else { + bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedConstruction?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && @@ -967,8 +800,9 @@ namespace Barotrauma return QuickUseAction.TakeFromCharacter; } else if (character.HeldItems.Any(i => - i.OwnInventory != null && - ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + i.OwnInventory != null && + /*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ + ((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -1131,11 +965,18 @@ namespace Barotrauma } break; case QuickUseAction.PutToEquippedItem: + foreach (Item heldItem in character.HeldItems) { if (heldItem.OwnInventory == null) { continue; } + //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items + //(in that case, the quick action should just fill up the stack) + bool disallowSwapping = + heldItem.OwnInventory.Capacity == 1 && + heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && + heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: true, allowCombine: false, user: Character.Controlled))) + (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) @@ -1197,11 +1038,6 @@ namespace Barotrauma DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem, SlotTypes[i]); } - - if (hideButton != null && hideButton.Visible && !Locked) - { - hideButton.DrawManually(spriteBatch, alsoChildren: true); - } VisualSlot highlightedQuickUseSlot = null; Rectangle inventoryArea = Rectangle.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index a189f5470..4966e5c49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -203,7 +203,7 @@ namespace Barotrauma.Items.Components private float lastMuffleCheckTime; private ItemSound loopingSound; private SoundChannel loopingSoundChannel; - private List playingOneshotSoundChannels = new List(); + private readonly List playingOneshotSoundChannels = new List(); public ItemComponent ReplacedBy; public ItemComponent GetReplacementOrThis() @@ -211,13 +211,16 @@ namespace Barotrauma.Items.Components return ReplacedBy?.GetReplacementOrThis() ?? this; } + public bool NeedsSoundUpdate() + { + if (hasSoundsOfType[(int)ActionType.Always]) { return true; } + if (loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { return true; } + if (playingOneshotSoundChannels.Count > 0) { return true; } + return false; + } + public void UpdateSounds() { - if (!isActive || item.Condition <= 0.0f) - { - StopSounds(ActionType.OnActive); - } - if (loopingSound != null && loopingSoundChannel != null && loopingSoundChannel.IsPlaying) { if (Timing.TotalTime > lastMuffleCheckTime + 0.2f) @@ -280,6 +283,7 @@ namespace Barotrauma.Items.Components loopingSound.RoundSound.GetRandomFrequencyMultiplier(), SoundPlayer.ShouldMuffleSound(Character.Controlled, item.WorldPosition, loopingSound.Range, Character.Controlled?.CurrentHull)); loopingSoundChannel.Looping = true; + item.CheckNeedsSoundUpdate(this); //TODO: tweak loopingSoundChannel.Near = loopingSound.Range * 0.4f; loopingSoundChannel.Far = loopingSound.Range; @@ -298,7 +302,6 @@ namespace Barotrauma.Items.Components loopingSound = null; } } - return; } @@ -333,6 +336,7 @@ namespace Barotrauma.Items.Components } PlaySound(matchingSounds[index], item.WorldPosition); + item.CheckNeedsSoundUpdate(this); } } private void PlaySound(ItemSound itemSound, Vector2 position) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7af73e740..90849cd19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), paddedItemFrame.RectTransform), style: null) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { selectedItem = userdata as FabricationRecipe; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index d14a9b237..371d3882b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -333,6 +333,7 @@ namespace Barotrauma.Items.Components GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, o) => { if (o is ItemPrefab prefab) @@ -744,11 +745,11 @@ namespace Barotrauma.Items.Components if (key == Keys.Down) { - listBox.SelectNext(true, autoScroll: true); + listBox.SelectNext(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Up) { - listBox.SelectPrevious(true, autoScroll: true); + listBox.SelectPrevious(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes); } else if (key == Keys.Enter) { @@ -782,7 +783,7 @@ namespace Barotrauma.Items.Components if (component.Visible && first) { - listBox.Select(i, force: true, autoScroll: false); + listBox.Select(i, GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled); first = false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index a3724b8a3..2798830ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components } GuiFrame = selectionUI.GuiFrame; - selectionUI.RefreshSubmarineDisplay(true); + selectionUI.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); IsActive = true; return base.Select(character); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 2fbf7b38d..d85bca980 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -927,6 +927,8 @@ namespace Barotrauma.Items.Components bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); + ushort userID = msg.ReadUInt16(); + Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; @@ -935,7 +937,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: Entity.FindEntityByID(userID) as Character), "toggle_docking"); } if (autoPilot) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs index de9dc05fe..c0d347358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Planter.cs @@ -40,8 +40,6 @@ namespace Barotrauma.Items.Components } } - private LightComponent lightComponent; - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { for (var i = 0; i < GrowableSeeds.Length; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index d1a295c91..156e1afc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -418,7 +418,7 @@ namespace Barotrauma.Items.Components if (!GameMain.IsMultiplayer) { RepairBoost(qteSuccess); } - SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.IncreaseQuantity : GUISoundType.DecreaseQuantity); + SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.Increase : GUISoundType.Decrease); //on failure during cooldown reset cursor to beginning if (!qteSuccess && qteCooldown > 0.0f) { qteTimer = QteDuration; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 8c7ef71ea..8b97d27be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index bda201aa8..32610f313 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -569,6 +569,18 @@ namespace Barotrauma } } + public void CheckNeedsSoundUpdate(ItemComponent ic) + { + if (ic.NeedsSoundUpdate()) + { + if (!updateableComponents.Contains(ic)) + { + updateableComponents.Add(ic); + } + isActive = true; + } + } + public void UpdateSpriteStates(float deltaTime) { if (activeContainedSprite != null) @@ -940,6 +952,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { if (!(userData is Identifier)) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 651f40201..ecdbeeacc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -98,7 +98,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(); + Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); InitProjectSpecific(); return true; } @@ -642,11 +642,11 @@ namespace Barotrauma } } - if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.HasOutpost())) + if (GameMain.DebugDraw) { - if (location.Reputation != null) + Vector2 dPos = pos; + if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) { - Vector2 dPos = pos; dPos.Y += 48; string name = $"Reputation: {location.Name}"; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -663,6 +663,8 @@ namespace Barotrauma GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } + dPos.Y += 48; + GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatZeroDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index c8e5cedeb..d0a908933 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -154,13 +154,13 @@ namespace Barotrauma crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); } - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + if (RecommendedCrewExperience != CrewExperienceLevel.Unknown) { var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + TextManager.Get(RecommendedCrewExperience.ToIdentifier()), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index ad05d0b20..e72fc7fb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -100,12 +100,16 @@ namespace Barotrauma GUIListBox specsContainer = null; new GUICustomComponent(new RectTransform(Vector2.One, innerPadded.RectTransform, Anchor.Center), - (spriteBatch, component) => { + (spriteBatch, component) => + { + if (isDisposed) { return; } camera.UpdateTransform(interpolate: true, updateListener: false); Rectangle drawRect = new Rectangle(component.Rect.X + 1, component.Rect.Y + 1, component.Rect.Width - 2, component.Rect.Height - 2); RenderSubmarine(spriteBatch, drawRect, component); }, - (deltaTime, component) => { + (deltaTime, component) => + { + if (isDisposed) { return; } bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && @@ -294,8 +298,8 @@ namespace Barotrauma private void BakeMapEntity(XElement element) { - string identifier = element.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(identifier)) { return; } + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { return; } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Equals(Rectangle.Empty)) { return; } @@ -308,7 +312,16 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + MapEntityPrefab prefab = null; + if (element.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase) && + ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) + { + prefab = ip; + } + else + { + prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); + } if (prefab == null) { return; } var texture = prefab.Sprite.Texture; @@ -329,7 +342,6 @@ namespace Barotrauma bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; - StructurePrefab structurePrefab = prefab as StructurePrefab; if (itemPrefab != null) { BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); @@ -337,7 +349,7 @@ namespace Barotrauma if (!overrideSprite) { - if (structurePrefab != null) + if (prefab is StructurePrefab structurePrefab) { ParseUpgrades(structurePrefab.ConfigElement, ref scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2e36c70de..797640696 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -689,7 +689,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text.Text == ChildServerRelay.CrashMessage)) + if (!GUIMessageBox.MessageBoxes.Any(mb => (mb as GUIMessageBox)?.Text?.Text == ChildServerRelay.CrashMessage)) { var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; @@ -824,7 +824,11 @@ namespace Barotrauma.Networking byte campaignID = inc.ReadByte(); UInt16 campaignSaveID = inc.ReadUInt16(); - UInt16 campaignUpdateID = inc.ReadUInt16(); + Dictionary campaignUpdateIDs = new Dictionary(); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaignUpdateIDs[flag] = inc.ReadUInt16(); + } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -843,7 +847,7 @@ namespace Barotrauma.Networking campaign != null && campaign.CampaignID == campaignID && campaign.LastSaveID == campaignSaveID && - campaign.LastUpdateID == campaignUpdateID; + campaignUpdateIDs.All(kvp => campaign.GetLastUpdateIdForFlag(kvp.Key) == kvp.Value); } readyToStartMsg.Write(readyToStart); @@ -2401,7 +2405,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(netFlag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2446,7 +2453,10 @@ namespace Barotrauma.Networking { outmsg.Write(campaign.LastSaveID); outmsg.Write(campaign.CampaignID); - outmsg.Write(campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + outmsg.Write(campaign.GetLastUpdateIdForFlag(flag)); + } outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } @@ -2644,7 +2654,7 @@ namespace Barotrauma.Networking if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Unsure); + GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -2674,9 +2684,12 @@ namespace Barotrauma.Networking } DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID); - //decrement campaign update ID so the server will send us the latest data + //decrement campaign update IDs so the server will send us the latest data //(as there may have been campaign updates after the save file was created) - campaign.LastUpdateID--; + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + campaign.SetLastUpdateIdForFlag(flag, (ushort)(campaign.GetLastUpdateIdForFlag(flag) - 1)); + } break; case FileTransferType.Mod: if (!(Screen.Selected is ModDownloadScreen)) { return; } @@ -2775,6 +2788,15 @@ namespace Barotrauma.Networking GameMain.GameSession = null; } + public void SendCharacterInfo() + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.UPDATE_CHARACTERINFO); + WriteCharacterInfo(msg); + msg.Write((byte)ServerNetObject.END_OF_MESSAGE); + clientPeer?.Send(msg, DeliveryMethod.Reliable); + } + public void WriteCharacterInfo(IWriteMessage msg) { msg.Write(characterInfo == null); @@ -2824,18 +2846,18 @@ namespace Barotrauma.Networking } #region Submarine Change Voting - public void InitiateSubmarineChange(SubmarineInfo sub, VoteType voteType) + public void InitiateSubmarineChange(SubmarineInfo sub, bool transferItems, VoteType voteType) { if (sub == null) { return; } - Vote(voteType, sub); + Vote(voteType, (sub, transferItems)); } - public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, float timeOut) + public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float timeOut) { if (info == null) { return; } if (votingInterface != null && votingInterface.VoteRunning) { return; } votingInterface?.Remove(); - votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, timeOut); + votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, transferItems, timeOut); } #endregion @@ -3014,7 +3036,7 @@ namespace Barotrauma.Networking msg.Write(mapSeed); msg.Write(sub.Name); msg.Write(sub.MD5Hash.StringRepresentation); - settings.Serialize(msg); + msg.Write(settings); clientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 09cb93edc..6e097aabf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Networking timeout = Screen.Selected == GameMain.GameScreen ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - + PacketHeader packetHeader = (PacketHeader)data[0]; if (!packetHeader.IsServerMessage()) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 04c501909..2f5c2f480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -11,7 +11,13 @@ namespace Barotrauma.Networking private bool isActive; private readonly UInt64 selfSteamID; + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey); + private UInt64 ReadSteamId(IReadMessage inc) + => inc.ReadUInt64() ^ ownerKey64; + private void WriteSteamId(IWriteMessage msg, UInt64 val) + => msg.Write(val ^ ownerKey64); + private long sentBytes, receivedBytes; class RemotePeer @@ -58,6 +64,8 @@ namespace Barotrauma.Networking { if (isActive) { return; } + this.ownerKey = ownerKey; + initializationStep = ConnectionInitialization.SteamTicketAndVersion; ServerConnection = new PipeConnection(selfSteamID); @@ -103,7 +111,7 @@ namespace Barotrauma.Networking //known now int prevBitPosition = msg.Message.BitPosition; msg.Message.BitPosition = sizeof(ulong) * 8; - msg.Message.Write(ownerID); + WriteSteamId(msg.Message, ownerID); msg.Message.BitPosition = prevBitPosition; byte[] msgToSend = (byte[])msg.Message.Buffer.Clone(); Array.Resize(ref msgToSend, msg.Message.LengthBytes); @@ -141,8 +149,8 @@ namespace Barotrauma.Networking } IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(steamId); - outMsg.Write(remotePeer.OwnerSteamID); + WriteSteamId(outMsg, steamId); + WriteSteamId(outMsg, remotePeer.OwnerSteamID); outMsg.Write(data, 1, dataLength - 1); DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; @@ -232,7 +240,7 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - UInt64 recipientSteamId = inc.ReadUInt64(); + UInt64 recipientSteamId = ReadSteamId(inc); DeliveryMethod deliveryMethod = (DeliveryMethod)inc.ReadByte(); int p2pDataStart = inc.BytePosition; @@ -343,8 +351,8 @@ namespace Barotrauma.Networking if (packetHeader.IsConnectionInitializationStep()) { IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(selfSteamID); - outMsg.Write(selfSteamID); + WriteSteamId(outMsg, selfSteamID); + WriteSteamId(outMsg, selfSteamID); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep)); outMsg.Write(Name); @@ -436,8 +444,8 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[msg.LengthBytes]; msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - msgToSend.Write(selfSteamID); - msgToSend.Write(selfSteamID); + WriteSteamId(msgToSend, selfSteamID); + WriteSteamId(msgToSend, selfSteamID); msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); msgToSend.Write((UInt16)length); msgToSend.Write(msgData, 0, length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index de7227cc8..a889a9b4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -7,6 +7,20 @@ namespace Barotrauma { partial class Voting { + private struct SubmarineVoteInfo + { + public SubmarineInfo SubmarineInfo { get; set; } + public bool TransferItems { get; set; } + public int DeliveryFee { get; set; } + + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + { + SubmarineInfo = submarineInfo; + TransferItems = transferItems; + DeliveryFee = deliveryFee; + } + } + private readonly Dictionary voteCountYes = new Dictionary(), voteCountNo = new Dictionary(), @@ -131,14 +145,16 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is SubmarineInfo voteSub) + if (data is (SubmarineInfo voteSub, bool transferItems)) { //initiate sub vote msg.Write(true); msg.Write(voteSub.Name); + msg.Write(transferItems); } else { + // vote if (!(data is int)) { return; } msg.Write(false); msg.Write((int)data); @@ -246,7 +262,7 @@ namespace Barotrauma float timeOut = inc.ReadByte(); Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); - if (!myClient.InGame) { return; } + if (myClient == null || !myClient.InGame) { return; } switch (voteType) { @@ -254,13 +270,14 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName1 = inc.ReadString(); + bool transferItems = inc.ReadBoolean(); SubmarineInfo info = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName1); if (info == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } - GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, timeOut); + GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, transferItems, timeOut); break; case VoteType.TransferMoney: byte fromClientId = inc.ReadByte(); @@ -279,39 +296,40 @@ namespace Barotrauma case VoteState.Passed: case VoteState.Failed: bool passed = inc.ReadBoolean(); - - SubmarineInfo subInfo = null; + SubmarineVoteInfo submarineVoteInfo = default; switch (voteType) { case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - subInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); - if (subInfo == null) + var submarineInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + bool transferItems = inc.ReadBoolean(); + int deliveryFee = inc.ReadInt16(); + if (submarineInfo == null) { DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); return; } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - if (passed && subInfo != null) + if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { - int deliveryFee = inc.ReadInt16(); switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, deliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index c7cd94394..38cda4a69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,8 +1,11 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; +using System.Collections.Immutable; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -15,11 +18,11 @@ namespace Barotrauma protected GUITextBox saveNameBox, seedBox; protected GUIButton loadGameButton; - + public Action StartNewGame; public Action LoadGame; - protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 } protected CategoryFilter subFilter = CategoryFilter.All; public GUIButton StartButton @@ -33,15 +36,11 @@ namespace Barotrauma get; protected set; } - - public GUITickBox EnableRadiationToggle { get; set; } - public GUILayoutGroup CampaignSettingsContent { get; set; } + public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } - public GUITextBlock MaxMissionCountText; - public CampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) { this.newGameContainer = newGameContainer; @@ -102,5 +101,259 @@ namespace Barotrauma return saveFrame; } + + public struct CampaignSettingElements + { + public SettingValue RadiationEnabled; + public SettingValue MaxMissionCount; + public SettingValue StartingFunds; + public SettingValue Difficulty; + public SettingValue StartItemSet; + + public CampaignSettings CreateSettings() + { + return new CampaignSettings(element: null) + { + RadiationEnabled = RadiationEnabled.GetValue(), + MaxMissionCount = MaxMissionCount.GetValue(), + StartingBalanceAmount = StartingFunds.GetValue(), + Difficulty = Difficulty.GetValue(), + StartItemSet = StartItemSet.GetValue() + }; + } + } + + public readonly struct SettingValue + { + private readonly Func getter; + private readonly Action setter; + + public T GetValue() + { + return getter.Invoke(); + } + + public void SetValue(T value) + { + setter.Invoke(value); + } + + public SettingValue(Func get, Action set) + { + getter = get; + setter = set; + } + } + + private readonly struct SettingCarouselElement + { + public readonly LocalizedString Label; + public readonly T Value; + public readonly bool IsHidden; + + public SettingCarouselElement(T value, string label, bool isHidden = false) + { + Value = value; + Label = TextManager.Get(label).Fallback(label); + IsHidden = isHidden; + } + } + + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + { + const float verticalSize = 0.14f; + + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length); + + presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); + + foreach (CampaignSettings settings in CampaignModePresets.List) + { + string name = settings.PresetName; + presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + } + + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) + { + Spacing = GUI.IntScale(5) + }; + + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + + ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); + SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); + + ImmutableArray> fundOptions = ImmutableArray.Create( + new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high"), + new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), + new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low") + ); + + SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions); + + ImmutableArray> difficultyOptions = ImmutableArray.Create( + new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), + new SettingCarouselElement(GameDifficulty.Medium, "difficulty.medium"), + new SettingCarouselElement(GameDifficulty.Hard, "difficulty.hard"), + new SettingCarouselElement(GameDifficulty.Hellish, "difficulty.hellish", isHidden: true) + ); + + SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; + SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); + + SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, verticalSize); + + presetDropdown.OnSelected = (selected, o) => + { + if (o is CampaignSettings settings) + { + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + return true; + } + return false; + }; + + return new CampaignSettingElements + { + RadiationEnabled = radiationEnabled, + MaxMissionCount = maxMissionCountInput, + StartingFunds = startingFundsInput, + Difficulty = difficultyInput, + StartItemSet = startingSetInput + }; + + // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, float verticalSize) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + { + ClickSound = GUISoundType.Decrease, + UserData = -valueStep + }; + GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputContainer.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", + hidePlusMinusButtons: true) + { + IntValue = defaultValue + }; + inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + { + ClickSound = GUISoundType.Increase, + UserData = valueStep + }; + + minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + numberInput.IntValue += change; + return true; + } + + return new SettingValue(() => numberInput.IntValue, i => numberInput.IntValue = i); + } + + static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, + ImmutableArray> options) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1 }; + GUIFrame inputFrame = new GUIFrame(new RectTransform(Vector2.One, inputContainer.RectTransform), style: null); + GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", hidePlusMinusButtons: true) + { + IntValue = options.IndexOf(defaultValue), + MinValueInt = 0, + MaxValueInt = options.Length, + Visible = false + }; + inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + GUITextBox inputLabel = new GUITextBox(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), text: defaultValue.Label.Value, textAlignment: Alignment.Center, createPenIcon: false) + { + CanBeFocused = false + }; + + GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1 }; + + minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + + bool ChangeValue(GUIButton btn, object userData) + { + if (!(userData is int change)) { return false; } + + int hiddenOptions = 0; + + for (int i = options.Length - 1; i >= 0; i--) + { + if (options[i].IsHidden) + { + hiddenOptions++; + continue; + } + break; + } + + int limit = options.Length - hiddenOptions; + + if (PlayerInput.IsShiftDown()) + { + limit = options.Length; + } + + int newValue = MathUtils.PositiveModulo(Math.Clamp(numberInput.IntValue + change, min: -1, max: limit), limit); + SetValue(newValue); + return true; + } + + void SetValue(int value) + { + numberInput.IntValue = value; + inputLabel.Text = options[value].Label.Value; + } + + return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); + } + + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize) + { + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); + GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); + GUITickBox tickBox = new GUITickBox(new RectTransform(Vector2.One, tickboxContainer.RectTransform), string.Empty) + { + Selected = defaultValue, + ToolTip = tooltip + }; + tickBox.Box.IgnoreLayoutGroups = true; + tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); + inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); + } + + static GUILayoutGroup CreateSettingBase(GUIComponent parent, LocalizedString description, LocalizedString tooltip, float horizontalSize, float verticalSize) + { + GUILayoutGroup settingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), description, font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, wrap: true) { ToolTip = tooltip }; + GUILayoutGroup inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + inputContainer.RectTransform.IsFixedSize = true; + settingHolder.RectTransform.MinSize = new Point(0, (int)descriptionBlock.TextSize.Y); + return inputContainer; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 97e08e561..0c297bbf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -18,71 +18,35 @@ namespace Barotrauma var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.0f + RelativeSpacing = 0.05f + }; + + GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true + }; + + GUILayoutGroup campaignSettingLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), verticalLayout.RectTransform), isHorizontal: false) + { + Stretch = true, + RelativeSpacing = 0.05f }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { - textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } + textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - GUIFrame radiationBoxContainer - = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), style: null); - GUITickBox radiationEnabledTickBox = null; - if (MapGenerationParams.Instance.RadiationParams != null) - { - radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = true, - OnSelected = box => true - }; - } + nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true) - { - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - int maxMissionCount = GameMain.NetworkMember.ServerSettings.MaxMissionCount; - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleLeft"); - var maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); - void updateMissionCountText() - { - maxMissionCount = MathHelper.Clamp(maxMissionCount, - CampaignSettings.MinMissionCountLimit, - CampaignSettings.MaxMissionCountLimit); - - maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); - } - maxMissionCountButtons[1] - = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), - style: "GUIButtonToggleRight"); - maxMissionCountButtons[0].OnClicked = (button, o) => - { - maxMissionCount--; - updateMissionCountText(); - return false; - }; - maxMissionCountButtons[1].OnClicked = (button, o) => - { - maxMissionCount++; - updateMissionCountText(); - return false; - }; - updateMissionCountText(); - maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.04f), + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) @@ -99,7 +63,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } selectedSub = GameMain.NetLobbyScreen.SelectedSub; - + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); @@ -115,11 +79,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings - { - RadiationEnabled = radiationEnabledTickBox?.Selected ?? GameMain.NetworkMember.ServerSettings.RadiationEnabled, - MaxMissionCount = maxMissionCount - }; + CampaignSettings settings = elements.CreateSettings(); if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -172,12 +132,16 @@ namespace Barotrauma }; StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = 8000; + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); + } if (GameMain.NetLobbyScreen.SelectedSub != null) { initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; @@ -238,6 +202,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -257,7 +222,7 @@ namespace Barotrauma file1WriteTime = File.GetLastWriteTime(file1); } catch - { + { //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list }; try diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 870827d91..40737a179 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -1,12 +1,11 @@ -using Barotrauma.Tutorials; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using System.Globalization; -using Barotrauma.Extensions; namespace Barotrauma { @@ -142,7 +141,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUIStyle.SubHeadingFont); var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); @@ -155,8 +154,12 @@ namespace Barotrauma { Stretch = true }; - - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) + { + PlaySoundOnSelect = true, + ScrollBarVisible = true + }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); @@ -191,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CampaignMode.InitialMoney; + int initialMoney = CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -200,12 +203,16 @@ namespace Barotrauma return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(); + CreateCustomizeWindow(CurrentSettings, settings => + { + CurrentSettings = settings; + UpdateSubList(SubmarineInfo.SavedSubmarines); + }); return true; } }; @@ -218,7 +225,7 @@ namespace Barotrauma return false; } }; - + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { IgnoreLayoutGroups = true, @@ -353,54 +360,21 @@ namespace Barotrauma StealRandomizeButton(CharacterMenus[i], jobTextContainer); } } - - private void CreateCustomizeWindow() + + private void CreateCustomizeWindow(CampaignSettings prevSettings, Action onClosed = null) { - CampaignCustomizeSettings = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); - CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; + CampaignCustomizeSettings = new GUIMessageBox("", "", new[] { TextManager.Get("OK") }, new Vector2(0.25f, 0.3f), minSize: new Point(450, 350)); - CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) - { - RelativeSpacing = 0.1f - }; + GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - if (MapGenerationParams.Instance.RadiationParams != null) - { - bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUIStyle.Font) - { - Selected = prevRadiationToggleEnabled, - ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") - }; - } - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", "missions"), wrap: true); - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } - }; - RichString prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); - MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); - maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } + + onClosed?.Invoke(elements.CreateSettings()); + return CampaignCustomizeSettings.Close(button, o); }; - maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); } private static void StealRandomizeButton(CharacterInfo.AppearanceCustomizationMenu menu, GUIComponent parent) @@ -412,7 +386,7 @@ namespace Barotrauma randomizeButton.RectTransform.Parent = parent.RectTransform; randomizeButton.RectTransform.RelativeSize = Vector2.One * 1.3f; } - + private bool FinishSetup(GUIButton btn, object userdata) { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) @@ -420,7 +394,7 @@ namespace Barotrauma saveNameBox.Flash(GUIStyle.Red); return false; } - + SubmarineInfo selectedSub = null; if (!(subList.SelectedData is SubmarineInfo)) { return false; } @@ -443,16 +417,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings(); - settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text.SanitizedValue, out int missionCount)) - { - settings.MaxMissionCount = missionCount; - } - else - { - settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; - } + CampaignSettings settings = CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -499,7 +464,7 @@ namespace Barotrauma return true; } - + public void RandomizeSeed() { seedBox.Text = ToolBox.RandomSeed(8); @@ -509,7 +474,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - var sub = child.UserData as SubmarineInfo; + SubmarineInfo sub = child.UserData as SubmarineInfo; if (sub == null) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } @@ -523,7 +488,7 @@ namespace Barotrauma if (!(obj is SubmarineInfo sub)) { return true; } #if !DEBUG - if (sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -556,8 +521,8 @@ namespace Barotrauma subsToShow.Sort((s1, s2) => { - int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; - int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; + int p1 = s1.Price > CurrentSettings.InitialMoney ? 10 : 0; + int p2 = s2.Price > CurrentSettings.InitialMoney ? 10 : 0; return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); @@ -582,13 +547,13 @@ namespace Barotrauma var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -598,7 +563,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); @@ -625,6 +590,7 @@ namespace Barotrauma saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = SelectSaveFile }; @@ -650,8 +616,9 @@ namespace Barotrauma { var saveFrame = CreateSaveElement(saveInfo); if (saveFrame == null) { continue; } - + XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); + if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); @@ -725,9 +692,10 @@ namespace Barotrauma string subName = doc.Root.GetAttributeString("submarine", ""); string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); + DateTime? time = null; if (long.TryParse(saveTime, out long unixTime)) { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + time = ToolBox.Epoch.ToDateTime(unixTime); saveTime = time.ToString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 81973be7f..6cc459e40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -729,7 +729,7 @@ namespace Barotrauma break; case CampaignMode.InteractionType.PurchaseSub: if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); - submarineSelection.RefreshSubmarineDisplay(true); + submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 72363e2fe..11105e73a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2858,7 +2858,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; void PopulateListBox() @@ -2996,7 +2999,10 @@ namespace Barotrauma.CharacterEditor { var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); loadBox.Buttons[0].OnClicked += loadBox.Close; - var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)); + var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var deleteButton = loadBox.Buttons[2]; deleteButton.Enabled = false; // Type filtering diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 3f61ef66d..2f8f73962 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -482,7 +482,10 @@ namespace Barotrauma.CharacterEditor RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUIStyle.SubHeadingFont); - var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { OnClicked = (b, d) => @@ -659,7 +662,10 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); + var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) + { + PlaySoundOnSelect = true, + }; var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { OnClicked = (b, d) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 46b2e753f..22b86c510 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -225,7 +225,7 @@ namespace Barotrauma return true; } - public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm, GUISoundType? overrideConfirmButtonSound = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); @@ -244,6 +244,10 @@ namespace Barotrauma msgBox.Close(); return true; }; + if (overrideConfirmButtonSound.HasValue) + { + msgBox.Buttons[0].ClickSound = overrideConfirmButtonSound.Value; + } return msgBox; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 64a24a84d..ed93ceee0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -34,6 +34,8 @@ namespace Barotrauma private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; + private readonly GUIDropDown selectedSubDropDown; + private Sprite editingSprite; private LightSource pointerLightSource; @@ -57,7 +59,10 @@ namespace Barotrauma RelativeSpacing = 0.01f }; - paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)); + paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; paramsList.OnSelected += (GUIComponent component, object obj) => { selectedParams = obj as LevelGenerationParams; @@ -70,7 +75,10 @@ namespace Barotrauma var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUIStyle.SubHeadingFont); - ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; ruinParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -79,7 +87,10 @@ namespace Barotrauma var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUIStyle.SubHeadingFont); - caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; caveParamsList.OnSelected += (GUIComponent component, object obj) => { CreateCaveParamsEditor(obj as CaveGenerationParams); @@ -89,7 +100,10 @@ namespace Barotrauma var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); - outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); + outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true + }; outpostParamsList.OnSelected += (GUIComponent component, object obj) => { CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); @@ -171,6 +185,16 @@ namespace Barotrauma Vector2 GetSeedElementRelativeSize() => new Vector2(0.5f * (1.0f - randomizeButtonRelativeSize.X), 1.0f); static string GetLevelSeed() => ToolBox.RandomSeed(8); + var subDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform), TextManager.Get("submarine")); + selectedSubDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform)); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { + if (sub.Type != SubmarineType.Player) { continue; } + selectedSubDropDown.AddItem(sub.DisplayName, userData: sub); + } + subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.RectTransform.MinSize.Y); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), @@ -186,11 +210,18 @@ namespace Barotrauma { bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); + + if (selectedSubDropDown.SelectedData is SubmarineInfo subInfo) + { + Submarine.MainSub = new Submarine(subInfo); + } GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - Level.Generate(currentLevelData, mirror: mirrorLevel.Selected); + var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); + Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { @@ -228,7 +259,7 @@ namespace Barotrauma var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() .Where(f => !(f is SubmarineFile))).ToArray(); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == GameSettings.CurrentConfig.QuickStartSub); + SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); @@ -259,6 +290,7 @@ namespace Barotrauma levelObjectList = new GUIListBox(new RectTransform(new Vector2(0.99f, 0.85f), bottomPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, UseGridLayout = true }; levelObjectList.OnSelected += (GUIComponent component, object obj) => @@ -866,7 +898,11 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - item?.GetComponent()?.Update((float)deltaTime, Cam); + if (item == null) { continue; } + foreach (var light in item.GetComponents()) + { + light.Update((float)deltaTime, Cam); + } } } GameMain.LightManager?.Update((float)deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index f8c3d29c9..14748954f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -429,7 +429,10 @@ namespace Barotrauma //PLACEHOLDER var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); + new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) + { + PlaySoundOnSelect = true, + }; var tutorialTypes = new List() { typeof(MechanicTutorial), @@ -850,16 +853,12 @@ namespace Barotrauma arguments += " -nopassword"; } - int ownerKey = 0; if (Steam.SteamManager.GetSteamID() != 0) { arguments += " -steamid " + Steam.SteamManager.GetSteamID(); } - else - { - ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); - arguments += " -ownerkey " + ownerKey; - } + int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); + arguments += " -ownerkey " + ownerKey; string filename = Path.Combine( Path.GetDirectoryName(exeName), @@ -1244,7 +1243,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Decrease }; maxPlayersBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), textAlignment: Alignment.Center) { @@ -1264,7 +1264,8 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, - OnClicked = ChangeMaxPlayers + OnClicked = ChangeMaxPlayers, + ClickSound = GUISoundType.Increase }; maxPlayersLabel.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 001618db3..1c21c6f27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -179,7 +179,7 @@ namespace Barotrauma get { return ModeList.SelectedIndex; } set { - ModeList.Select(value, true); + ModeList.Select(value, GUIListBox.Force.Yes); } } @@ -504,6 +504,7 @@ namespace Barotrauma PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } }; @@ -816,6 +817,7 @@ namespace Barotrauma SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -901,6 +903,7 @@ namespace Barotrauma }; ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = VotableClicked }; @@ -1515,6 +1518,7 @@ namespace Barotrauma JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, + PlaySoundOnSelect = true, OnSelected = (child, obj) => { if (child.IsParentOf(GUI.MouseOn)) return false; @@ -1600,6 +1604,7 @@ namespace Barotrauma { Enabled = true, KeepSpaceForScrollBar = false, + PlaySoundOnSelect = true, ScrollBarEnabled = false, ScrollBarVisible = false }; @@ -3185,7 +3190,7 @@ namespace Barotrauma var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; - if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, true); } + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, GUIListBox.Force.Yes); } selectedModeIndex = modeIndex; if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) @@ -3301,7 +3306,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - ModeList.Select(GameModePreset.MultiPlayerCampaign, true); + ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } } @@ -3417,7 +3422,7 @@ namespace Barotrauma UserData = i, OnClicked = (btn, obj) => { - JobList.Select((int)obj, true); + JobList.Select((int)obj, GUIListBox.Force.Yes); SwitchJob(btn, null); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } JobList.Deselect(); @@ -3553,7 +3558,7 @@ namespace Barotrauma else { subList.OnSelected -= VotableClicked; - subList.Select(sub, force: true); + subList.Select(sub, GUIListBox.Force.Yes); subList.OnSelected += VotableClicked; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 81473f6d6..c48aa0ac5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -129,7 +129,10 @@ namespace Barotrauma OnClicked = (btn, userdata) => { FilterEmitters(""); filterBox.Text = ""; filterBox.Flash(Color.White); return true; } }; - prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)); + prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)) + { + PlaySoundOnSelect = true, + }; prefabList.OnSelected += (GUIComponent component, object obj) => { cam.Position = Vector2.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 1bf91b69e..49bed19f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -504,6 +504,7 @@ namespace Barotrauma serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (btn, obj) => { @@ -1473,7 +1474,6 @@ namespace Barotrauma { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { - Font = GUIStyle.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 8a78c4e45..df8fc28ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -85,12 +85,12 @@ namespace Barotrauma { OnClicked = (button, userData) => { - var selected = selectedSprites; + var selected = selectedSprites.ToList(); Sprite firstSelected = selected.First(); selected.ForEach(s => s.ReloadTexture()); RefreshLists(); - textureList.Select(firstSelected.FullPath, autoScroll: false); - selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); + textureList.Select(firstSelected.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); + selected.ForEachMod(s => spriteList.Select(s, autoScroll: GUIListBox.AutoScroll.Disabled)); texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); texturePathText.TextColor = GUIStyle.Green; return true; @@ -206,6 +206,7 @@ namespace Barotrauma textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { var newTexturePath = userData as string; @@ -213,7 +214,7 @@ namespace Barotrauma { selectedTexturePath = newTexturePath; ResetZoom(); - spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: false); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); } foreach (GUIComponent child in spriteList.Content.Children) @@ -248,6 +249,7 @@ namespace Barotrauma spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)) { + PlaySoundOnSelect = true, OnSelected = (listBox, userData) => { if (userData is Sprite sprite) @@ -481,7 +483,7 @@ namespace Barotrauma var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); if (scaledRect.Contains(PlayerInput.MousePosition)) { - spriteList.Select(sprite, autoScroll: false); + spriteList.Select(sprite, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(spriteList); UpdateScrollBar(textureList); // Release the keyboard so that we can nudge the source rects @@ -847,7 +849,7 @@ namespace Barotrauma base.Select(); LoadSprites(); RefreshLists(); - spriteList.Select(0, autoScroll: false); + spriteList.Select(0, autoScroll: GUIListBox.AutoScroll.Disabled); } protected override void DeselectEditorSpecific() @@ -905,7 +907,7 @@ namespace Barotrauma } if (sprite.FullPath != selectedTexturePath) { - textureList.Select(sprite.FullPath, autoScroll: false); + textureList.Select(sprite.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled); UpdateScrollBar(textureList); } xmlPathText.Text = string.Empty; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index b4c90e032..a702a3d4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; using Barotrauma.IO; +using Barotrauma.Steam; namespace Barotrauma { @@ -58,15 +59,6 @@ namespace Barotrauma islinked = Linkage; } } - - #warning TODO: switch this to an enum? - private static readonly ImmutableArray crewExperienceLevels = new string[] - { - "CrewExperienceLow", - "CrewExperienceMid", - "CrewExperienceHigh" - }.ToImmutableArray(); - public enum Mode { @@ -99,6 +91,8 @@ namespace Barotrauma private SubmarineInfo backedUpSubInfo; + private readonly HashSet publishedWorkshopItemIds = new HashSet(); + private Point screenResolution; private bool lightingEnabled; @@ -550,6 +544,7 @@ namespace Barotrauma }; previouslyUsedList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), previouslyUsedPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = SelectPrefab }; @@ -635,6 +630,7 @@ namespace Barotrauma undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (_, userData) => { @@ -1006,7 +1002,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - Visible = false + Visible = false, + PlaySoundOnSelect = true, }; paddedTab.Recalculate(); @@ -1136,7 +1133,8 @@ namespace Barotrauma OnSelected = SelectPrefab, UseGridLayout = true, CheckSelected = MapEntityPrefab.GetSelected, - ClampMouseRectToParent = true + ClampMouseRectToParent = true, + PlaySoundOnSelect = true, }; entityListInner.ContentBackground.ClampMouseRectToParent = true; entityListInner.Content.ClampMouseRectToParent = true; @@ -1190,7 +1188,7 @@ namespace Barotrauma frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + frame.ToolTip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; if (!ep.Description.IsNullOrEmpty()) { frame.ToolTip += '\n' + ep.Description; @@ -1326,6 +1324,17 @@ namespace Barotrauma { base.Select(); + TaskPool.Add( + $"DeterminePublishedItemIds", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + + publishedWorkshopItemIds.Clear(); + publishedWorkshopItemIds.UnionWith(items.Select(it => it.Id.Value)); + }); + GUI.PreventPauseMenuToggle = false; if (!Directory.Exists(autoSavePath)) { @@ -1573,10 +1582,6 @@ namespace Barotrauma ClearFilter(); ClearLayers(); - while (packageReloadQueue.TryDequeue(out var p)) - { - ContentPackageManager.ReloadContentPackage(p); - } } private void CreateDummyCharacter() @@ -1699,8 +1704,20 @@ namespace Barotrauma autoSaveLabel?.FadeOut(0.5f, true, 1f); } - private bool SaveSub(GUIButton button, object obj) + private bool SaveSub(ContentPackage packageToSaveTo) { + void handleExceptions(Action action) + { + try + { + action(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while trying to save {nameBox.Text}", e, createMessageBox: true); + } + } + if (string.IsNullOrWhiteSpace(nameBox.Text)) { GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); @@ -1722,7 +1739,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { - SaveSubToFile(nameBox.Text); + handleExceptions(() => SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; msgBox.Close(); return true; @@ -1735,17 +1752,22 @@ namespace Barotrauma return true; } - var result = SaveSubToFile(nameBox.Text); + bool result = false; + handleExceptions(() => result = SaveSubToFile(nameBox.Text, packageToSaveTo)); saveFrame = null; return result; } - private readonly Queue packageReloadQueue = new Queue(); - - private void EnqueueForReload(ContentPackage p) + private void ReloadModifiedPackage(ContentPackage p) { if (p is null) { return; } - if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } + p.ReloadSubsAndItemAssemblies(); + if (p.Files.Length == 0) + { + Directory.Delete(p.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } } public static Type DetermineSubFileType(SubmarineType type) @@ -1760,12 +1782,9 @@ namespace Barotrauma SubmarineType.Player => typeof(SubmarineFile), _ => null }; - - private bool SaveSubToFile(string name) - { - bool canModifyPackage(ContentPackage p) - => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + private bool SaveSubToFile(string name, ContentPackage packageToSaveTo) + { Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) @@ -1784,6 +1803,7 @@ namespace Barotrauma modProject.AddFile(newFile); } + using var _ = Validation.SkipInDebugBuilds(); modProject.DiscardHashAndInstallTime(); modProject.Save(packagePath); } @@ -1819,61 +1839,66 @@ namespace Barotrauma name = name.Trim(); string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}"; - - var vanilla = GameMain.VanillaContent; - var vanillaSubs = vanilla?.GetFiles()?.Select(f => f.Path); - bool isVanillaSub = vanillaSubs?.Any(f => f.Value == MainSub.Info.FilePath.CleanUpPath()) ?? false; - string savePath = name + ".sub"; + string savePath = $"{name}.sub"; string prevSavePath = null; - if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && - MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + if (packageToSaveTo != null) + { + var modProject = new ModProject(packageToSaveTo); + var fileListPath = packageToSaveTo.Path; + if (packageToSaveTo == ContentPackageManager.VanillaCorePackage) + { +#if !DEBUG + throw new InvalidOperationException("Cannot save to Vanilla package"); +#endif + savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch + { + SubmarineType.Player => "Content/Submarines/{0}", + SubmarineType.Outpost => "Content/Map/Outposts/{0}", + SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... + SubmarineType.Wreck => "Content/Map/Wrecks/{0}", + SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", + SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", + SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", + _ => throw new InvalidOperationException() + }, savePath); + modProject.ModVersion = ""; + } + else + { + savePath = Path.Combine(packageToSaveTo.Dir, savePath); + } + addSubAndSaveModProject(modProject, savePath, fileListPath); + } + else if (MainSub?.Info != null + && MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir) + && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = MainSub.Info.FilePath.CleanUpPath(); string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); - string[] subDirs = prevDir.Split('/'); - ModProject modProject = new ModProject() { Name = name }; + ModProject modProject = new ModProject { Name = name }; string fileListPath = null; - if (subDirs.Length > 1 && subDirs[0].Equals(ContentPackage.LocalModsDir, StringComparison.InvariantCultureIgnoreCase)) + ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info); + if (contentPackage != null) { - string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); - if (contentPackage != null) - { - modProject = new ModProject(contentPackage); - fileListPath = contentPackage.Path; - EnqueueForReload(contentPackage); - } + modProject = new ModProject(contentPackage); + fileListPath = contentPackage.Path; + packageToSaveTo = contentPackage; } - savePath = Path.Combine(prevDir, savePath).CleanUpPath(); - if (!isVanillaSub) - { - addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); - } + savePath = Path.Combine(prevDir, savePath).CleanUpPath(); + addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } else { - savePath = Path.Combine(newLocalModDir, savePath); - ModProject modProject = new ModProject() { Name = name }; + savePath = Path.Combine(newLocalModDir, savePath); + ModProject modProject = new ModProject { Name = name }; addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); } savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false); -#if !DEBUG - if (vanilla != null) - { - string pathToCompare = savePath.Replace(@"\", @"/"); - if (vanillaSubs.Any(sub => sub.Value.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) - { - GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUIStyle.Red, font: GUIStyle.LargeFont); - return false; - } - } -#endif - if (MainSub != null) { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; @@ -1912,6 +1937,7 @@ namespace Barotrauma GameSettings.SaveCurrentConfig(); } } + if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; @@ -1940,12 +1966,7 @@ namespace Barotrauma SetMode(Mode.Default); } - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) - { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } - }; - - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1964,7 +1985,7 @@ namespace Barotrauma submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); - nameBox = new GUITextBox(new RectTransform(new Vector2(.95f, 0.05f), leftColumn.RectTransform)) + nameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)) { OnEnterPressed = ChangeSubName }; @@ -2037,17 +2058,14 @@ namespace Barotrauma //--------------------------------------- - var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); + + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) { - IgnoreLayoutGroups = true, CanBeFocused = true, Visible = false, Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, outpostSettingsContainer.RectTransform), "InnerFrame") - { - IgnoreLayoutGroups = true - }; // module flags --------------------- @@ -2286,15 +2304,75 @@ namespace Barotrauma }; outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); - //------------------------------------------------------------------ + //--------------------------------------- - var subSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), leftColumn.RectTransform)) + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + + // ------------------- + + var beaconMinDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUIFrame(new RectTransform(Vector2.One, subSettingsContainer.RectTransform), "InnerFrame") + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), + TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMinDifficultyGroup.RectTransform), NumberType.Int) { - IgnoreLayoutGroups = true + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MinLevelDifficulty = numberInput.IntValue; + } + }; + var beaconMaxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), + TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) + { + IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; + } + }; + + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDamagedWalls = tb.Selected; + return true; + } + }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires")) + { + Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.AllowDisconnectedWires = tb.Selected; + return true; + } + }; + beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + + //------------------------------------------------------------------ + + var subSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + Stretch = true }; var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2321,7 +2399,7 @@ namespace Barotrauma MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice); } - var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -2329,19 +2407,27 @@ namespace Barotrauma TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); - classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); - classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); - classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); - classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); + foreach (SubmarineClass @class in Enum.GetValues(typeof(SubmarineClass))) + { + classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{@class}"), @class); + } + classDropDown.AddItem(TextManager.Get(nameof(SubmarineTag.Shuttle)), SubmarineTag.Shuttle); classDropDown.OnSelected += (selected, userdata) => { - SubmarineClass submarineClass = (SubmarineClass)userdata; - MainSub.Info.SubmarineClass = submarineClass; + switch (userdata) + { + case SubmarineClass submarineClass: + MainSub.Info.RemoveTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = submarineClass; + break; + case SubmarineTag.Shuttle: + MainSub.Info.AddTag(SubmarineTag.Shuttle); + MainSub.Info.SubmarineClass = SubmarineClass.Undefined; + break; + } return true; }; - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - classText.Enabled = classDropDown.ButtonEnabled = !MainSub.Info.HasTag(SubmarineTag.Shuttle); + classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2388,30 +2474,55 @@ namespace Barotrauma var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft"); var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), - text: crewExperienceLevels[0], textAlignment: Alignment.Center); + text: TextManager.Get(SubmarineInfo.CrewExperienceLevel.CrewExperienceLow.ToIdentifier()), textAlignment: Alignment.Center); var toggleExpRight = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleRight"); toggleExpLeft.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex--; - if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience--; + if (MainSub.Info.RecommendedCrewExperience < SubmarineInfo.CrewExperienceLevel.CrewExperienceLow) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; toggleExpRight.OnClicked += (btn, userData) => { - int currentIndex = crewExperienceLevels.IndexOf((string)experienceText.UserData); - currentIndex++; - if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; - experienceText.UserData = crewExperienceLevels[currentIndex]; - experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience++; + if (MainSub.Info.RecommendedCrewExperience > SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); return true; }; + + var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), hideInMenusArea.RectTransform), + TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + + new GUITickBox(new RectTransform((0.4f, 1.0f), hideInMenusArea.RectTransform), "") + { + Selected = MainSub.Info.HasTag(SubmarineTag.HideInMenus), + OnSelected = box => + { + if (box.Selected) + { + MainSub.Info.AddTag(SubmarineTag.HideInMenus); + } + else + { + MainSub.Info.RemoveTag(SubmarineTag.HideInMenus); + } + return true; + } + }; if (MainSub != null) { @@ -2419,9 +2530,11 @@ namespace Barotrauma int max = MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(MainSub.Info.RecommendedCrewExperience) ? - crewExperienceLevels[0] : MainSub.Info.RecommendedCrewExperience; - experienceText.Text = TextManager.Get((string)experienceText.UserData); + if (MainSub.Info.RecommendedCrewExperience == SubmarineInfo.CrewExperienceLevel.Unknown) + { + MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow; + } + experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier()); } subTypeDropdown.OnSelected += (selected, userdata) => @@ -2434,10 +2547,10 @@ namespace Barotrauma } previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; - outpostSettingsContainer.IgnoreLayoutGroups = !outpostSettingsContainer.Visible; + + beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; subSettingsContainer.Visible = type == SubmarineType.Player; - subSettingsContainer.IgnoreLayoutGroups = !subSettingsContainer.Visible; return true; }; subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); @@ -2498,77 +2611,144 @@ namespace Barotrauma previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); - var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.06f), rightColumn.RectTransform), isHorizontal: true); - var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), - TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUIStyle.SmallFont); - - var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), - horizontalArea.RectTransform, Anchor.BottomLeft), - style: "InnerFrame"); - - foreach (SubmarineTag tag in Enum.GetValues(typeof(SubmarineTag))) + GUIButton createTabberBtn(string labelTag) { - LocalizedString tagStr = TextManager.Get(tag.ToString()); - var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), - tagStr, font: GUIStyle.SmallFont) - { - Selected = MainSub != null && MainSub.Info.HasTag(tag), - UserData = tag, - OnSelected = (GUITickBox tickBox) => - { - if (MainSub == null) return false; - SubmarineTag tag = (SubmarineTag)tickBox.UserData; - if (tag == SubmarineTag.Shuttle) - { - if (tickBox.Selected) - { - classDropDown.SelectItem(SubmarineClass.Undefined); - } - else - { - classDropDown.SelectItem(MainSub.Info.SubmarineClass); - } - classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; - } - if (tickBox.Selected) - { - MainSub.Info.AddTag(tag); - } - else - { - MainSub.Info.RemoveTag(tag); - } - return true; - } - }; + var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton"); + btn.RectTransform.MaxSize = RectTransform.MaxPoint; + btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + btn.Font = GUIStyle.SmallFont; + return btn; } - var contentPackagesLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), + var saveToPackageTabBtn = createTabberBtn("SaveToLocalPackage"); + saveToPackageTabBtn.Selected = true; + var reqPackagesTabBtn = createTabberBtn("RequiredContentPackages"); + reqPackagesTabBtn.Selected = false; + + var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); + + var saveInPackageLayout = new GUILayoutGroup(new RectTransform(Vector2.One, horizontalArea.RectTransform, Anchor.BottomRight)) { Stretch = true }; - var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), - TextManager.Get("RequiredContentPackages"), wrap: true, font: GUIStyle.SmallFont); - contentPackagesLabel.RectTransform.MinSize - = GUIStyle.SmallFont.MeasureString(contentPackagesLabel.WrappedText).ToPoint(); + var packageToSaveInList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + saveInPackageLayout.RectTransform)); - var contentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), - contentPackagesLayout.RectTransform)); + var packToSaveInFilter + = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform), + createClearButton: true); + GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p) + { + var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = p + }; + if (p != null && p != ContentPackageManager.VanillaCorePackage) { listItem.ToolTip = p.Dir; } + var retVal = + new GUILayoutGroup(new RectTransform(Vector2.One, listItem.RectTransform), + isHorizontal: true) { Stretch = true }; + var iconFrame = + new GUIFrame( + new RectTransform(Vector2.One, retVal.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null) { CanBeFocused = false }; + var pkgText = new GUITextBlock(new RectTransform(Vector2.One, retVal.RectTransform), itemText) + { CanBeFocused = false }; + return retVal; + } + +#if DEBUG + //this is a debug-only option so I won't bother submitting it for localization + var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage); + var modifyVanillaListIcon = modifyVanillaListItem.GetChild(); + GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton"); +#endif + + var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null); + var newPackageListIcon = newPackageListItem.GetChild(); + var newPackageListText = newPackageListItem.GetChild(); + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), + onUpdate: (f, component) => + { + bool canCreateNewPackage = true; + foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) + { + contentChild.Visible = !(contentChild.UserData is ContentPackage p) + || !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase); + canCreateNewPackage &= contentChild.Visible; + contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && + !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); + } + + if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); + newPackageListText.Text = TextManager.Get("CreateNewLocalPackage"); + } + if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage) + { + GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton"); + newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text); + } + }); + packageToSaveInList.Select(0); + ContentPackage ownerPkg = null; + if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } + foreach (var p in ContentPackageManager.LocalPackages) + { + addItemToPackageToSaveList(p.Name, p); + } + + if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase)) + { + packageToSaveInList.Select(ownerPkg); + packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); + } + + var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, + horizontalArea.RectTransform, Anchor.BottomRight)) + { + Stretch = true, + Visible = false + }; + + var requiredContentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + requiredContentPackagesLayout.RectTransform)); + + var filterLayout = new GUILayoutGroup( + new RectTransform((1.0f, 0.15f), requiredContentPackagesLayout.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + var contentPackFilter - = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), + = new GUITextBox(new RectTransform((0.6f, 1.0f), filterLayout.RectTransform), createClearButton: true); contentPackFilter.OnTextChanged += (box, text) => { - contentPackList.Content.Children.ForEach(c + requiredContentPackList.Content.Children.ForEach(c => c.Visible = !(c is GUITickBox tb && !tb.Text.Contains(text, StringComparison.OrdinalIgnoreCase))); return true; }; + var autoDetectBtn = new GUIButton(new RectTransform((0.4f, 1.0f), filterLayout.RectTransform), + text: TextManager.Get("AutoDetectRequiredPackages"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage) + .Distinct().OfType().Select(p => p.Name).ToHashSet(); + var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); + tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? "")); + return false; + } + }; + if (MainSub != null) { List allContentPacks = MainSub.Info.RequiredContentPackages.ToList(); @@ -2596,7 +2776,7 @@ namespace Barotrauma foreach (string contentPackageName in allContentPacks) { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), requiredContentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) { Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName @@ -2616,7 +2796,19 @@ namespace Barotrauma } } + GUIButton.OnClickedHandler switchToTab(GUIButton tabBtn, GUIComponent tab) + => (button, obj) => + { + horizontalArea.Children.ForEach(c => c.Visible = false); + contentPackageTabber.Children.ForEach(c => c.Selected = false); + tabBtn.Selected = true; + tab.Visible = true; + return false; + }; + saveToPackageTabBtn.OnClicked = switchToTab(saveToPackageTabBtn, saveInPackageLayout); + reqPackagesTabBtn.OnClicked = switchToTab(reqPackagesTabBtn, requiredContentPackagesLayout); + var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedSaveFrame.RectTransform, Anchor.BottomCenter, minSize: new Point(0, 30)), style: null); var cancelButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), @@ -2632,22 +2824,23 @@ namespace Barotrauma var saveButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight), TextManager.Get("SaveSubButton")) { - OnClicked = SaveSub + OnClicked = (button, o) => SaveSub(packageToSaveInList.SelectedData as ContentPackage) }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = + subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize = new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostSettingsContainer.Rect.Height)); subSettingsContainer.Recalculate(); outpostSettingsContainer.Recalculate(); + beaconSettingsContainer.Recalculate(); descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; subTypeDropdown.SelectItem(MainSub.Info.Type); - if (quickSave) { SaveSub(saveButton, saveButton.UserData); } + if (quickSave) { SaveSub(null); } } private void CreateSaveAssemblyScreen() @@ -2774,16 +2967,7 @@ namespace Barotrauma } else { - var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); - var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); - if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath.Value) == ItemAssemblyPrefab.VanillaSaveFolder) - { - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); - } - else - { - Save(); - } + Save(); } void Save() @@ -2792,7 +2976,7 @@ namespace Barotrauma if (existingContentPackage == null) { //content package doesn't exist, create one - ModProject modProject = new ModProject() { Name = nameBox.Text }; + ModProject modProject = new ModProject { Name = nameBox.Text }; var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); @@ -2853,6 +3037,13 @@ namespace Barotrauma } } + private IEnumerable GetLoadableSubs() + { + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + return SubmarineInfo.SavedSubmarines.Where(s + => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder); + } + private void CreateLoadScreen() { CloseItem(); @@ -2887,6 +3078,7 @@ namespace Barotrauma var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform)) { + PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (GUIComponent selected, object userData) => { @@ -2899,7 +3091,7 @@ namespace Barotrauma return true; } - var package = GetContentPackageIntrinsicallyTiedToSub(subInfo); + var package = GetLocalPackageThatOwnsSub(subInfo); if (package != null) { deleteBtn.Enabled = true; @@ -2907,11 +3099,11 @@ namespace Barotrauma else { deleteBtn.Enabled = false; - if (ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == subInfo.FilePath) ?? false) + if (IsVanillaSub(subInfo)) { deleteBtn.ToolTip = TextManager.Get("cantdeletevanillasub"); } - else if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == subInfo.FilePath)) is ContentPackage subPackage) + else if (GetPackageThatOwnsSub(subInfo, ContentPackageManager.AllPackages) is ContentPackage subPackage) { deleteBtn.ToolTip = TextManager.GetWithVariable("cantdeletemodsub", "[modname]", subPackage.Name); } @@ -2925,9 +3117,10 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - List sortedSubs = new List(SubmarineInfo.SavedSubmarines.Where(s => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder)); - sortedSubs.Sort((s1, s2) => { return s1.Type.CompareTo(s2.Type) * 100 + s1.Name.CompareTo(s2.Name); }); + var sortedSubs = GetLoadableSubs() + .OrderBy(s => s.Type) + .ThenBy(s => s.Name) + .ToList(); SubmarineInfo prevSub = null; @@ -2968,7 +3161,7 @@ namespace Barotrauma if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) { - if (GetContentPackageIntrinsicallyTiedToSub(sub) == null && + if (GetLocalPackageThatOwnsSub(sub) == null && ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) { //workshop mod @@ -3080,7 +3273,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), controlBtnHolder.RectTransform, Anchor.BottomRight), TextManager.Get("Load")) { - OnClicked = LoadSub + OnClicked = HitLoadSubButton }; controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height); @@ -3117,9 +3310,9 @@ namespace Barotrauma /// Recovers the auto saved submarine /// /// - private void LoadAutoSave(object UserData) + private void LoadAutoSave(object userData) { - if (!(UserData is XElement element)) { return; } + if (!(userData is XElement element)) { return; } #warning TODO: revise string filePath = element.GetAttributeStringUnrestricted("file", ""); @@ -3152,7 +3345,7 @@ namespace Barotrauma loadFrame = null; } - private bool LoadSub(GUIButton button, object obj) + private bool HitLoadSubButton(GUIButton button, object obj) { if (loadFrame == null) { @@ -3167,14 +3360,68 @@ namespace Barotrauma return false; } - if (subList.SelectedComponent == null) { return false; } - if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } + if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; } - LoadSub(selectedSubInfo); - - return true; + var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); + if (ownerPackage is null) + { + if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) + { + if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) + { + AskLoadPublishedSub(selectedSubInfo, workshopPackage); + } + else + { + AskLoadSubscribedSub(selectedSubInfo); + } + } + else if (IsVanillaSub(selectedSubInfo)) + { +#if DEBUG + LoadSub(selectedSubInfo); +#else + AskLoadVanillaSub(selectedSubInfo); +#endif + } + } + else + { + LoadSub(selectedSubInfo); + } + return false; } + void AskLoadSub(SubmarineInfo info, LocalizedString header, LocalizedString desc) + { + var msgBox = new GUIMessageBox( + header, + desc, + new[] { TextManager.Get("LoadAnyway"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked = (button, o) => + { + LoadSub(info); + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + void AskLoadPublishedSub(SubmarineInfo info, ContentPackage pkg) + => AskLoadSub(info, + TextManager.Get("LoadingPublishedSubmarineHeader"), + TextManager.GetWithVariable("LoadingPublishedSubmarineDesc", "[modname]", pkg.Name)); + + void AskLoadSubscribedSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingSubscribedSubmarineHeader"), + TextManager.Get("LoadingSubscribedSubmarineDesc")); + + void AskLoadVanillaSub(SubmarineInfo info) + => AskLoadSub(info, + TextManager.Get("LoadingVanillaSubmarineHeader"), + TextManager.Get("LoadingVanillaSubmarineDesc")); + public void LoadSub(SubmarineInfo info) { Submarine.Unload(); @@ -3202,7 +3449,10 @@ namespace Barotrauma { if (item.ParentInventory != null || item.body != null) continue; var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + foreach (var light in item.GetComponents()) + { + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); + } } new GUIMessageBox("", TextManager.Get("AdjustedLightsNotification")); return true; @@ -3213,11 +3463,18 @@ namespace Barotrauma ReconstructLayers(); } - private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) - => ContentPackageManager.LocalPackages.Regular - .Where(p => p.Files.Length == 1) - .FirstOrDefault(regularPackage => regularPackage.Files[0].Path == sub.FilePath); + private static ContentPackage GetPackageThatOwnsSub(SubmarineInfo sub, IEnumerable packages) + => packages.FirstOrDefault(package => package.Files.Any(f => f.Path == sub.FilePath)); + private static ContentPackage GetLocalPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.LocalPackages); + + private static ContentPackage GetWorkshopPackageThatOwnsSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.WorkshopPackages); + + private static bool IsVanillaSub(SubmarineInfo sub) + => GetPackageThatOwnsSub(sub, ContentPackageManager.VanillaCorePackage.ToEnumerable()) != null; + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } @@ -3225,7 +3482,7 @@ namespace Barotrauma //If the sub is included in a content package that only defines that one sub, //check that it's a local content package and only allow deletion if it is. //(deleting from the Submarines folder is also currently allowed, but this is temporary) - var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); + var subPackage = GetLocalPackageThatOwnsSub(sub); if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } var msgBox = new GUIMessageBox( @@ -3238,9 +3495,11 @@ namespace Barotrauma { if (subPackage != null) { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); - ContentPackageManager.LocalPackages.Refresh(); - ContentPackageManager.EnabledPackages.DisableRemovedMods(); + File.Delete(sub.FilePath); + ModProject modProject = new ModProject(subPackage); + modProject.RemoveFile(modProject.Files.First(f => ContentPath.FromRaw(subPackage, f.Path) == sub.FilePath)); + modProject.Save(subPackage.Path); + ReloadModifiedPackage(subPackage); } sub.Dispose(); @@ -3869,6 +4128,7 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { + PlaySoundOnSelect = true, OnSelected = SelectWire }; @@ -4846,7 +5106,7 @@ namespace Barotrauma int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; if (index > -1 && index < listBox.Content.CountChildren) { - listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); + listBox.Select(index); SkipInventorySlotUpdate = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index fb767cd11..afd88b6da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -1282,6 +1282,7 @@ namespace Barotrauma var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) { + PlaySoundOnSelect = true, OnSelected = (component, userData) => { string text = userData as string ?? ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index d4ef4f065..45c0e1c6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -171,7 +171,7 @@ namespace Barotrauma int childIndex = values.IndexOf(currentValue); dropdown.Select(childIndex); dropdown.ListBox.ForceLayoutRecalculation(); - dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex), playSound: false); + dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex)); dropdown.OnSelected = (dd, obj) => { setter((T)obj); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 821725532..0705c3084 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -418,7 +418,7 @@ namespace Barotrauma } else { - if (!Level.IsLoadedOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && + if (!Level.IsLoadedFriendlyOutpost && Character.Controlled?.CurrentHull?.Submarine is Submarine sub && sub.Info != null && !sub.Info.IsOutpost) { hullSoundSource = Character.Controlled.CurrentHull; @@ -889,5 +889,13 @@ namespace Barotrauma .Where(s => s.Type == soundType) .GetRandomUnsynced()?.Sound?.Play(null, "ui"); } + + public static void PlayUISound(GUISoundType? soundType) + { + if (soundType.HasValue) + { + PlayUISound(soundType.Value); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 7696fbefe..a21ef24c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -176,12 +176,15 @@ namespace Barotrauma.Steam Directory.CreateDirectory(PublishStagingDir); await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); + var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); + ContentPackage tempPkg = ContentPackage.TryLoad(stagingFileListPath) ?? throw new Exception("Staging copy could not be loaded"); + //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be - ModProject modProject = new ModProject(contentPackage) + ModProject modProject = new ModProject(tempPkg) { ModVersion = modVersion }; - modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); + modProject.Save(stagingFileListPath); } public static async Task CreateLocalCopy(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 484fe3182..7cc29acce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -46,7 +46,10 @@ namespace Barotrauma.Steam regularBox.CanBeFocused = true; } } - filterBox = CreateSearchBox(mainLayout, width: 1.0f); + + var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); + searchRectT.RelativeSize = (1.0f, searchRectT.RelativeSize.Y); + filterBox = CreateSearchBox(searchRectT); Label(mainLayout, TextManager.Get("CannotChangeMods"), GUIStyle.Font); } @@ -55,9 +58,8 @@ namespace Barotrauma.Steam { string str = filterBox.Text; regularList.Content.Children - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs new file mode 100644 index 000000000..59624f305 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -0,0 +1,746 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + + private readonly GUIDropDown enabledCoreDropdown; + private readonly GUIListBox enabledRegularModsList; + private readonly GUIListBox disabledRegularModsList; + private readonly Action onInstalledInfoButtonHit; + private readonly GUITextBox modsListFilter; + private readonly Dictionary modsListFilterTickboxes; + private readonly GUIButton bulkUpdateButton; + + private GUIComponent? draggedElement = null; + private GUIListBox? draggedElementOrigin = null; + + private void UpdateSubscribedModInstalls() + { + if (!SteamManager.IsInitialized) { return; } + + uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); + if (numSubscribedMods == memSubscribedModCount) { return; } + memSubscribedModCount = numSubscribedMods; + + var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); + var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); + foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(item); + } + } + + TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => + { + if (!t.TryGetResult(out ISet publishedItems)) { return; } + + var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); + bool needsRefresh = false; + foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + SteamManager.Workshop.Uninstall(item); + needsRefresh = true; + } + + if (needsRefresh) + { + PopulateInstalledModLists(); + } + }); + } + + private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( + GUIComponent parent, + float leftWidth = 0.3875f, + float centerWidth = 0.025f, + float rightWidth = 0.5875f, + bool split = false, + float height = 1.0f) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); + var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, center, right); + } + + private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + { + if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) + { + //move the dragged elements to the index determined previously + var draggedElement = from.DraggedElement; + + var selected = from.AllSelected.ToList(); + selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); + + float oldCount = to.Content.CountChildren; + float newCount = oldCount + selected.Count; + + var offset = draggedElement.RectTransform.AbsoluteOffset; + offset += from.Content.Rect.Location; + offset -= to.Content.Rect.Location; + + for (int i = 0; i < selected.Count; i++) + { + var c = selected[i]; + c.Parent.RemoveChild(c); + c.RectTransform.Parent = to.Content.RectTransform; + c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); + } + + from.DraggedElement = null; + from.Deselect(); + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + + //recalculate the dragged element's offset so it doesn't jump around + draggedElement.RectTransform.AbsoluteOffset = offset; + + to.DraggedElement = draggedElement; + + to.BarScroll *= (oldCount / newCount); + } + } + + private Action? currentSwapFunc = null; + private GUISoundType? swapSoundType = null; + + private void PlaySwapSound() + { + SoundPlayer.PlayUISound(swapSoundType); + } + + private void SetSwapFunc(GUIListBox from, GUIListBox to) + { + currentSwapFunc = () => + { + to.Deselect(); + var selected = from.AllSelected.ToArray(); + foreach (var frame in selected) + { + frame.Parent.RemoveChild(frame); + frame.RectTransform.Parent = to.Content.RectTransform; + } + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + }; + + if (to == enabledRegularModsList) + { + swapSoundType = GUISoundType.Increase; + } + else if (to == disabledRegularModsList) + { + swapSoundType = GUISoundType.Decrease; + } + else + { + swapSoundType = null; + } + } + + private void CreateInstalledModsTab( + out GUIDropDown enabledCoreDropdown, + out GUIListBox enabledRegularModsList, + out GUIListBox disabledRegularModsList, + out Action onInstalledInfoButtonHit, + out GUITextBox modsListFilter, + out Dictionary modsListFilterTickboxes, + out GUIButton bulkUpdateButton) + { + GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); + + CreateWorkshopItemDetailContainer( + content, + out var outerContainer, + onSelected: (itemOrPackage, selectedFrame) => + { + if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } + }, + onDeselected: () => PopulateInstalledModLists(), + out onInstalledInfoButtonHit, out var deselect); + + GUILayoutGroup mainLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + mainLayout.RectTransform.SetAsFirstChild(); + + var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); + topLeft.Stretch = true; + Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); + enabledCoreDropdown = Dropdown(topLeft, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }, + heightScale: 1.0f / 13.0f); + Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); + topRight.ChildAnchor = Anchor.CenterLeft; + + var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + void padTopRight(float width=1.0f) + { + new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); + } + + padTopRight(); + //TODO: put stuff here + padTopRight(width: 3.0f); + var refreshListsButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIReloadButton") + { + OnClicked = (b, o) => + { + PopulateInstalledModLists(); + return false; + }, + ToolTip = TextManager.Get("RefreshModLists") + }; + bulkUpdateButton + = new GUIButton( + new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), + text: "", style: "GUIUpdateButton") + { + OnClicked = (b, o) => + { + BulkDownloader.PrepareUpdates(); + return false; + }, + Enabled = false + }; + padTopRight(width: 0.1f); + + var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); + right.ChildAnchor = Anchor.TopRight; + + //enabled mods + Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Increase, + }; + enabledRegularModsList = enabledModsList; + + //disabled mods + Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); + var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) + { + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + HideDraggedElement = true, + PlaySoundOnSelect = true, + SoundOnDragStart = GUISoundType.Select, + SoundOnDragStop = GUISoundType.Decrease, + }; + disabledRegularModsList = disabledModsList; + + var centerButton = + new GUIButton( + new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, + anchor: Anchor.Center), + style: "GUIButtonToggleLeft") + { + PlaySoundOnSelect = false, + Visible = false, + OnClicked = (button, o) => + { + if (currentSwapFunc != null) + { + PlaySwapSound(); + currentSwapFunc.Invoke(); + } + return false; + } + }; + + enabledModsList.OnSelected = (frame, o) => + { + disabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); + + SetSwapFunc(enabledModsList, disabledModsList); + + return true; + }; + disabledModsList.OnSelected = (frame, o) => + { + enabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); + + SetSwapFunc(disabledModsList, enabledModsList); + + return true; + }; + + var filterContainer = new GUILayoutGroup(NewItemRectT(mainLayout, heightScale: 1.0f), isHorizontal: true) + { Stretch = true, RelativeSpacing = 0.01f }; + + void padFilterContainer(float width = 0.25f) + => new GUIFrame(new RectTransform((width, 1.0f), filterContainer!.RectTransform), style: null); + + GUIButton filterLayoutButton(string style) + => new GUIButton( + new RectTransform(Vector2.One, filterContainer!.RectTransform, scaleBasis: ScaleBasis.BothHeight), + "", style: style); + + padFilterContainer(width: 0.2f); + var loadPresetBtn = filterLayoutButton("OpenButton"); + loadPresetBtn.ToolTip = TextManager.Get("LoadModListPresetHeader"); + loadPresetBtn.OnClicked = OpenLoadPreset; + var savePresetBtn = filterLayoutButton("SaveButton"); + savePresetBtn.ToolTip = TextManager.Get("SaveModListPresetHeader"); + savePresetBtn.OnClicked = OpenSavePreset; + padFilterContainer(width: 0.05f); + var searchRectT = new RectTransform((0.5f, 1.0f), filterContainer.RectTransform); + var searchBox = CreateSearchBox(searchRectT); + modsListFilter = searchBox; + + var filterTickboxes = new Dictionary(); + modsListFilterTickboxes = filterTickboxes; + + var filterTickboxesDropdown + = filterLayoutButton("SetupVisibilityButton"); + var filterTickboxesContainer + = new GUIFrame(new RectTransform((0.3f, 0.2f), content.RectTransform, + scaleBasis: ScaleBasis.BothWidth), style: "InnerFrame"); + var filterTickboxesUpdater + = new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + filterTickboxesContainer.Visible = filterTickboxesDropdown.Selected; + filterTickboxesContainer.RectTransform.AbsoluteOffset + = (filterTickboxesDropdown.Rect.Location - content.Rect.Location) + + (filterTickboxesDropdown.Rect.Width / 2, 0) + - (filterTickboxesContainer.Rect.Size.ToVector2() * (0.5f, 1.0f)).ToPoint(); + filterTickboxesContainer.RectTransform.NonScaledSize + = new Point(filterTickboxes.Select(tb => (int)tb.Value.Font.MeasureString(tb.Value.GetChild().Text).X).Max(), + filterTickboxes.Select(tb => tb.Value.Rect.Height).Aggregate((a,b) => a+b)) + +(filterTickboxes.Values.First().Rect.Height * 4, filterTickboxes.Values.First().Rect.Height / 2); + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(filterTickboxesDropdown) + && !GUI.IsMouseOn(filterTickboxesContainer)) + { + filterTickboxesDropdown.Selected = false; + } + }); + + var filterTickboxesLayout + = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, filterTickboxesContainer.RectTransform, Anchor.Center)); + + void addFilterTickbox(Filter filter, string? style, bool selected) + { + var tickbox = new GUITickBox(NewItemRectT(filterTickboxesLayout!, heightScale: 0.5f), "") + { + Selected = selected, + OnSelected = _ => + { + UpdateModListItemVisibility(); + return true; + } + }; + filterTickboxes!.Add(filter, tickbox); + var text = new GUITextBlock(new RectTransform((1.0f, 1.0f), tickbox.RectTransform, Anchor.CenterRight) + { + AbsoluteOffset = (-tickbox.Box.Rect.Width * 2, 0), + }, + TextManager.Get($"ModFilter.{filter}")) + { + CanBeFocused = false + }; + var icon = new GUIFrame( + new RectTransform(Vector2.One, text.RectTransform, Anchor.CenterLeft, Pivot.CenterRight, + scaleBasis: ScaleBasis.BothHeight), style: style) + { + CanBeFocused = false + }; + } + + addFilterTickbox(Filter.ShowLocal, "WorkshopMenu.EditButton", selected: true); + addFilterTickbox(Filter.ShowWorkshop, "WorkshopMenu.DownloadedIcon", selected: true); + addFilterTickbox(Filter.ShowPublished, "WorkshopMenu.PublishedIcon", selected: true); + addFilterTickbox(Filter.ShowOnlySubs, null, selected: false); + addFilterTickbox(Filter.ShowOnlyItemAssemblies, null, selected: false); + + padFilterContainer(); + + new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + HandleDraggingAcrossModLists(enabledModsList, disabledModsList); + HandleDraggingAcrossModLists(disabledModsList, enabledModsList); + UpdateDraggingSounds(); + + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(enabledModsList) + && !GUI.IsMouseOn(disabledModsList) + && GUIContextMenu.CurrentContextMenu is null) + { + enabledModsList.Deselect(); + disabledModsList.Deselect(); + } + else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) + { + currentSwapFunc?.Invoke(); + } + }, + onDraw: (spriteBatch, component) => + { + enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + + void UpdateDraggingSounds() + { + if (draggedElement != null) + { + if (enabledModsList.DraggedElement == null && disabledModsList.DraggedElement == null) + { + SetDragOrigin(null); + } + CheckDragStopSound(enabledModsList); + CheckDragStopSound(disabledModsList); + } + else if (enabledModsList.DraggedElement != null) + { + SetDragOrigin(enabledModsList); + } + else if (disabledModsList.DraggedElement != null) + { + SetDragOrigin(disabledModsList); + } + + void SetDragOrigin(GUIListBox? listBox) + { + draggedElement = listBox?.DraggedElement; + draggedElementOrigin = listBox; + } + + void CheckDragStopSound(GUIListBox listBox) + { + listBox.PlaySoundOnDragStop = listBox.DraggedElement != null && draggedElementOrigin != listBox; + } + } + } + + protected override void UpdateModListItemVisibility() + { + string str = modsListFilter.Text; + enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) + .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); + } + + private bool ModMatchesTickboxes(ContentPackage p, GUIComponent guiItem) + { + var iconBtn = guiItem.GetChild()?.GetAllChildren().Last(); + + bool matches = false; + matches |= modsListFilterTickboxes[Filter.ShowLocal].Selected + && ContentPackageManager.LocalPackages.Contains(p); + matches |= modsListFilterTickboxes[Filter.ShowPublished].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier == "WorkshopMenu.PublishedIcon"); + matches |= modsListFilterTickboxes[Filter.ShowWorkshop].Selected + && (ContentPackageManager.WorkshopPackages.Contains(p) + && iconBtn?.Style?.Identifier != "WorkshopMenu.PublishedIcon"); + + if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.All(f => f is BaseSubFile || f is ItemAssemblyFile)) + { + //Both the subs-only tickbox and the item-assembly-only tickbox + //are enabled, and all files match either of them so show this mod + } + else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected + && p.Files.Any(f => !(f is BaseSubFile))) + { + matches = false; + } + else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected + && p.Files.Any(f => !(f is ItemAssemblyFile))) + { + matches = false; + } + + return matches; + } + + private void PrepareToShowModInfo(ContentPackage mod) + { + TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } + if (item is null) { return; } + onInstalledInfoButtonHit(item.Value); + }); + } + + public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) + { + bulkUpdateButton.Enabled = false; + bulkUpdateButton.ToolTip = ""; + ContentPackageManager.UpdateContentPackageList(); + + SwapDropdownValues(enabledCoreDropdown, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }); + + void addRegularModToList(RegularPackage mod, GUIListBox list) + { + var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), + style: "ListBoxElement") + { + UserData = mod + }; + + var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), + onUpdate: (f, component) => + { + var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) + if (parentList is null) { return; } + if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) + { + if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } + static void noop() { } + + List contextMenuOptions = new List(); + if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + contextMenuOptions.Add( + new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); + } + + var labelConditions + = (parentList == enabledRegularModsList, parentList.AllSelected.Count > 1); + Identifier swapLabel = (labelConditions switch + { + (true, true) => "EnableSelectedWorkshopMods", + (true, false) => "EnableWorkshopMod", + (false, true) => "DisableSelectedWorkshopMods", + (false, false) => "DisableWorkshopMod" + }).ToIdentifier(); + + contextMenuOptions.Add(new ContextMenuOption(swapLabel, + isEnabled: true, onSelected: currentSwapFunc ?? noop)); + + var selectedMods = parentList.AllSelected.Select(it => it.UserData) + .OfType().ToArray(); + if (selectedMods.All(ContentPackageManager.LocalPackages.Contains) && selectedMods.Length > 1) + { + contextMenuOptions.Add(new ContextMenuOption("MergeSelectedMods".ToIdentifier(), isEnabled: true, + onSelected: () => ModMerger.AskMerge(selectedMods))); + } + + GUIButton? iconBtn(GUIComponent component) => component.GetChild()?.GetAllChildren().Last(); + if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains) + && parentList.AllSelected.All(c => iconBtn(c)?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") + && selectedMods.Length > 0) + { + contextMenuOptions.Add(new ContextMenuOption( + (selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), + isEnabled: true, + onSelected: () => + { + TaskPool.Add($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))), + t => + { + if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } + items.ForEach(it => + { + if (!(it is { } item)) { return; } + + item.Unsubscribe(); + SteamManager.Workshop.Uninstall(item); + PopulateInstalledModLists(); + }); + }); + })); + } + + GUIContextMenu.CreateContextMenu( + pos: PlayerInput.MousePosition, + header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), + headerColor: null, + contextMenuOptions.ToArray()); + } + }); + + var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) + { + CanBeFocused = false + }; + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), + text: mod.Name) + { + CanBeFocused = false + }; + if (mod.Errors.Any()) + { + CreateModErrorInfo(mod, modFrame, modName); + } + if (ContentPackageManager.LocalPackages.Contains(mod)) + { + var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.EditButton") + { + OnClicked = (button, o) => + { + ToolBox.OpenFileWithShell(mod.Dir); + return false; + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: null) + { + CanBeSelected = false, + OnClicked = (button, o) => + { + PrepareToShowModInfo(mod); + return false; + } + }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } + TaskPool.Add( + $"DetermineUpdateRequired{mod.SteamWorkshopId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (!isUpToDate) + { + infoButton.CanBeSelected = true; + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); + bulkUpdateButton.Enabled = true; + bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); + } + }); + } + } + + void addRegularModsToList(IEnumerable mods, GUIListBox list) + { + list.ClearChildren(); + foreach (var mod in mods) + { + addRegularModToList(mod, list); + } + } + + var enabledMods = + (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) + ? ContentPackageManager.EnabledPackages.Regular + : enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType() + .Where(p => ContentPackageManager.RegularPackages.Contains(p))) + .ToArray(); + var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); + + addRegularModsToList(enabledMods, enabledRegularModsList); + if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } + + TaskPool.Add( + $"DetermineWorkshopModIcons", + SteamManager.Workshop.GetPublishedItems(), + t => + { + if (!t.TryGetResult(out ISet items)) { return; } + var ids = items.Select(it => it.Id).ToHashSet(); + + foreach (var child in enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children)) + { + var mod = child.UserData as RegularPackage; + if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } + + var btn = child.GetChild()?.GetAllChildren().Last(); + if (btn is null) { continue; } + if (btn.Style != null) { continue; } + + btn.ApplyStyle( + GUIStyle.GetComponentStyle( + ids.Contains(mod.SteamWorkshopId) + ? "WorkshopMenu.PublishedIcon" + : "WorkshopMenu.DownloadedIcon")); + btn.ToolTip = TextManager.Get( + ids.Contains(mod.SteamWorkshopId) + ? "PublishedWorkshopMod" + : "DownloadedWorkshopMod"); + btn.HoverCursor = CursorState.Default; + } + }); + + UpdateModListItemVisibility(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index dc9e38c57..7daabaacc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -151,7 +151,10 @@ namespace Barotrauma.Steam onDeselected: () => itemList?.Deselect(), out var select, out var deselect); - itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)) + { + PlaySoundOnSelect = true, + }; itemList.RectTransform.SetAsFirstChild(); workshopItemList = itemList; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs new file mode 100644 index 000000000..df5e42de0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -0,0 +1,256 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + readonly struct ModListPreset + { + public const string SavePath = "ModLists"; + + public enum ModType + { + Vanilla, + Local, + Workshop + } + + public readonly string Name; + public readonly CorePackage CorePackage; + public readonly ImmutableArray RegularPackages; + + public ModListPreset(XDocument doc) + { + Name = doc.Root!.GetAttributeString("name", ""); + + CorePackage corePackage = ContentPackageManager.VanillaCorePackage!; + List regularPackages = new List(); + void addPkg(ContentPackage pkg) + { + if (pkg is CorePackage core) { corePackage = core; } + else if (pkg is RegularPackage reg) { regularPackages.Add(reg); } + } + + foreach (var element in doc.Root!.Elements()) + { + ModType modType = Enum.TryParse(element.Name.LocalName, ignoreCase: true, out var mt) ? mt : ModType.Local; + + switch (modType) + { + case ModType.Vanilla: + CorePackage = ContentPackageManager.VanillaCorePackage!; + break; + case ModType.Workshop: + { + var id = element.GetAttributeUInt64("id", 0); + var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); + if (id != 0 && pkg != null) + { + addPkg(pkg); + } + } + break; + case ModType.Local: + { + var name = element.GetAttributeString("name", ""); + var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); + if (!name.IsNullOrEmpty() && pkg != null) + { + addPkg(pkg); + } + } + break; + } + } + + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public ModListPreset(string name, CorePackage corePackage, IReadOnlyList regularPackages) + { + Name = name; + CorePackage = corePackage; + RegularPackages = regularPackages.ToImmutableArray(); + } + + public RichString GetTooltip() + { + LocalizedString retVal = $"‖color:gui.orange‖{Name}‖end‖" //TODO: we need a RichString builder + + "\n " + TextManager.AddPunctuation(':', TextManager.Get("CorePackage")) + + "\n - " + CorePackage.Name; + if (RegularPackages.Any()) + { + retVal += "\n " + TextManager.AddPunctuation(':', TextManager.Get("RegularPackages")) + + "\n - " + + LocalizedString.Join("\n - ", RegularPackages.Select(p => (LocalizedString)p.Name)); + } + + return RichString.Rich(retVal); + } + + public void Save() + { + XDocument newDoc = new XDocument(); + XElement newRoot = new XElement("mods", new XAttribute("name", Name)); + newDoc.Add(newRoot); + + ModType determineType(ContentPackage pkg) + { + if (pkg == ContentPackageManager.VanillaCorePackage) { return ModType.Vanilla; } + if (ContentPackageManager.WorkshopPackages.Contains(pkg)) { return ModType.Workshop; } + return ModType.Local; + } + void writePkgElem(ContentPackage pkg) + { + var pkgType = determineType(pkg); + var pkgElem = new XElement(pkgType.ToString()); + switch (pkgType) + { + case ModType.Workshop: + pkgElem.SetAttributeValue("name", pkg.Name); + pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); + break; + case ModType.Local: + pkgElem.SetAttributeValue("name", pkg.Name); + break; + } + newRoot.Add(pkgElem); + } + writePkgElem(CorePackage); + RegularPackages.ForEach(writePkgElem); + + if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath); } + newDoc.SaveSafe(Path.Combine(SavePath, ToolBox.RemoveInvalidFileNameChars($"{Name}.xml"))); + } + } +} + +namespace Barotrauma.Steam +{ + sealed partial class MutableWorkshopMenu : WorkshopMenu + { + private bool OpenLoadPreset(GUIButton _, object __) + { + OpenLoadPreset(); + return false; + } + + private void OpenLoadPreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("LoadModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Load"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.6f)); + + var presetListBox = new GUIListBox(new RectTransform((1.0f, 0.7f), msgBox.Content.RectTransform)); + + (string Path, XDocument? Doc) tryLoadXml(string path) + => (path, XMLExtensions.TryLoadXml(path)); + + var presets = Directory.Exists(ModListPreset.SavePath) + ? Directory.GetFiles(ModListPreset.SavePath) + .Select(tryLoadXml) + .Where(d => d.Doc != null) + .ToArray() + : Array.Empty<(string Path, XDocument? Doc)>(); + + foreach (var doc in presets) + { + ModListPreset preset = new ModListPreset(doc.Doc!); + var presetFrame = new GUIFrame(new RectTransform((1.0f, 0.09f), presetListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = preset, + ToolTip = preset.GetTooltip() + }; + new GUITextBlock(new RectTransform(Vector2.One, presetFrame.RectTransform), preset.Name) + { + CanBeFocused = false + }; + var deleteBtn + = new GUIButton(new RectTransform((0.2f, 1.0f), presetFrame.RectTransform, Anchor.CenterRight), + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + File.Delete(doc.Path); + presetListBox.Content.RemoveChild(presetFrame); + return false; + } + }; + } + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (presetListBox.SelectedData is ModListPreset preset) + { + var allChildren = enabledRegularModsList.Content.Children + .Concat(disabledRegularModsList.Content.Children) + .ToArray(); + enabledRegularModsList.ClearChildren(); + disabledRegularModsList.ClearChildren(); + var toEnable = + allChildren.Where(c => c.UserData is RegularPackage p + && preset.RegularPackages.Contains(p)) + .OrderBy(c => c.UserData is RegularPackage p ? preset.RegularPackages.IndexOf(p) : int.MaxValue) + .ToArray(); + var toDisable = allChildren.Where(c => !toEnable.Contains(c)).ToArray(); + toEnable.ForEach(c => c.RectTransform.Parent = enabledRegularModsList.Content.RectTransform); + toDisable.ForEach(c => c.RectTransform.Parent = disabledRegularModsList.Content.RectTransform); + + enabledCoreDropdown.SelectItem(preset.CorePackage); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + private bool OpenSavePreset(GUIButton _, object __) + { + OpenSavePreset(); + return false; + } + + private void OpenSavePreset() + { + var msgBox = new GUIMessageBox( + TextManager.Get("SaveModListPresetHeader"), + "", + buttons: new [] { TextManager.Get("Save"), TextManager.Get("Cancel") }, + relativeSize: (0.4f, 0.2f)); + + var nameBox = new GUITextBox(new RectTransform((1.0f, 0.3f), msgBox.Content.RectTransform), ""); + + msgBox.Buttons[0].OnClicked = (button, o) => + { + if (nameBox.Text.IsNullOrEmpty()) + { + nameBox.Flash(GUIStyle.Red); + return false; + } + + if (enabledCoreDropdown.SelectedData is CorePackage corePackage) + { + ModListPreset preset = new ModListPreset(nameBox.Text, + corePackage, + enabledRegularModsList.Content.Children + .Select(c => c.UserData) + .OfType().ToArray()); + preset.Save(); + } + msgBox.Close(); + return false; + }; + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 49632af2b..c8563eb3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -3,9 +3,9 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; +using System.Threading.Tasks; using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam @@ -20,20 +20,20 @@ namespace Barotrauma.Steam Publish } + private enum Filter + { + ShowLocal, + ShowWorkshop, + ShowPublished, + ShowOnlySubs, + ShowOnlyItemAssemblies + } + private readonly GUILayoutGroup tabber; private readonly Dictionary tabContents; private readonly GUIFrame contentFrame; - private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); - - private readonly GUIDropDown enabledCoreDropdown; - private readonly GUIListBox enabledRegularModsList; - private readonly GUIListBox disabledRegularModsList; - private readonly Action onInstalledInfoButtonHit; - private readonly GUITextBox modsListFilter; - private readonly GUIButton bulkUpdateButton; - private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); @@ -41,7 +41,7 @@ namespace Barotrauma.Steam private readonly GUIListBox selfModsList; private uint memSubscribedModCount = 0; - + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -62,6 +62,7 @@ namespace Barotrauma.Steam out disabledRegularModsList, out onInstalledInfoButtonHit, out modsListFilter, + out modsListFilterTickboxes, out bulkUpdateButton); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -69,45 +70,6 @@ namespace Barotrauma.Steam SelectTab(Tab.InstalledMods); } - private void UpdateSubscribedModInstalls() - { - if (!SteamManager.IsInitialized) { return; } - - uint numSubscribedMods = SteamManager.GetNumSubscribedItems(); - if (numSubscribedMods == memSubscribedModCount) { return; } - memSubscribedModCount = numSubscribedMods; - - var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); - var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet(); - foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - if (!item.IsDownloading && !SteamManager.Workshop.IsInstalling(item)) - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(item); - } - } - - TaskPool.Add("RemoveUnsubscribedItems", SteamManager.Workshop.GetPublishedItems(), t => - { - if (!t.TryGetResult(out ISet publishedItems)) { return; } - - var allRequiredInstalled = subscribedIds.Union(publishedItems.Select(it => it.Id)).ToHashSet(); - bool needsRefresh = false; - foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) - { - Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); - SteamManager.Workshop.Uninstall(item); - needsRefresh = true; - } - - if (needsRefresh) - { - PopulateInstalledModLists(); - } - }); - } - private void SwitchContent(GUIFrame newContent) { contentFrame.Children.ForEach(c => c.Visible = false); @@ -161,460 +123,6 @@ namespace Barotrauma.Steam return content; } - private static (GUILayoutGroup Left, GUIFrame center, GUILayoutGroup Right) CreateSidebars( - GUIComponent parent, - float leftWidth = 0.3875f, - float centerWidth = 0.025f, - float rightWidth = 0.5875f, - bool split = false, - float height = 1.0f) - { - GUILayoutGroup layout = new GUILayoutGroup(new RectTransform((1.0f, height), parent.RectTransform), isHorizontal: true); - GUILayoutGroup left = new GUILayoutGroup(new RectTransform((leftWidth, 1.0f), layout.RectTransform), isHorizontal: false); - var center = new GUIFrame(new RectTransform((centerWidth, 1.0f), layout.RectTransform), style: null); - if (split) - { - new GUICustomComponent(new RectTransform(Vector2.One, center.RectTransform), - onDraw: (sb, c) => - { - sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); - }); - } - GUILayoutGroup right = new GUILayoutGroup(new RectTransform((rightWidth, 1.0f), layout.RectTransform), isHorizontal: false); - return (left, center, right); - } - - private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) - { - if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) - { - //move the dragged elements to the index determined previously - var draggedElement = from.DraggedElement; - - var selected = from.AllSelected.ToList(); - selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); - - float oldCount = to.Content.CountChildren; - float newCount = oldCount + selected.Count; - - var offset = draggedElement.RectTransform.AbsoluteOffset; - offset += from.Content.Rect.Location; - offset -= to.Content.Rect.Location; - - for (int i = 0; i < selected.Count; i++) - { - var c = selected[i]; - c.Parent.RemoveChild(c); - c.RectTransform.Parent = to.Content.RectTransform; - c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); - } - - from.DraggedElement = null; - from.Deselect(); - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - - //recalculate the dragged element's offset so it doesn't jump around - draggedElement.RectTransform.AbsoluteOffset = offset; - - to.DraggedElement = draggedElement; - - to.BarScroll *= (oldCount / newCount); - } - } - - private Action? currentSwapFunc = null; - - private void SetSwapFunc(GUIListBox from, GUIListBox to) - { - currentSwapFunc = () => - { - to.Deselect(); - var selected = from.AllSelected.ToArray(); - foreach (var frame in selected) - { - frame.Parent.RemoveChild(frame); - frame.RectTransform.Parent = to.Content.RectTransform; - } - from.RecalculateChildren(); - from.RectTransform.RecalculateScale(true); - to.RecalculateChildren(); - to.RectTransform.RecalculateScale(true); - to.Select(selected); - }; - } - - private void CreateInstalledModsTab( - out GUIDropDown enabledCoreDropdown, - out GUIListBox enabledRegularModsList, - out GUIListBox disabledRegularModsList, - out Action onInstalledInfoButtonHit, - out GUITextBox modsListFilter, - out GUIButton bulkUpdateButton) - { - GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); - - CreateWorkshopItemDetailContainer( - content, - out var outerContainer, - onSelected: (itemOrPackage, selectedFrame) => - { - if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } - }, - onDeselected: () => PopulateInstalledModLists(), - out onInstalledInfoButtonHit, out var deselect); - - GUILayoutGroup mainLayout = - new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); - mainLayout.RectTransform.SetAsFirstChild(); - - var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); - topLeft.Stretch = true; - Label(topLeft, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f); - enabledCoreDropdown = Dropdown(topLeft, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }, - heightScale: 1.0f / 13.0f); - Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); - topRight.ChildAnchor = Anchor.CenterLeft; - - var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - void padTopRight(float width=1.0f) - { - new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); - } - - padTopRight(); - //TODO: put stuff here - padTopRight(width: 3.0f); - var refreshListsButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIReloadButton") - { - OnClicked = (b, o) => - { - PopulateInstalledModLists(); - return false; - }, - ToolTip = TextManager.Get("RefreshModLists") - }; - bulkUpdateButton - = new GUIButton( - new RectTransform(Vector2.One, topRightButtons.RectTransform, scaleBasis: ScaleBasis.BothHeight), - text: "", style: "GUIUpdateButton") - { - OnClicked = (b, o) => - { - BulkDownloader.PrepareUpdates(); - return false; - }, - Enabled = false - }; - padTopRight(width: 0.1f); - - var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.8f); - right.ChildAnchor = Anchor.TopRight; - - //enabled mods - Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); - var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - enabledRegularModsList = enabledModsList; - - //disabled mods - Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); - var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) - { - CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, - CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, - HideDraggedElement = true - }; - disabledRegularModsList = disabledModsList; - - var centerButton = - new GUIButton( - new RectTransform(Vector2.One * 0.95f, center.RectTransform, scaleBasis: ScaleBasis.BothWidth, - anchor: Anchor.Center), - style: "GUIButtonToggleLeft") - { - Visible = false, - OnClicked = (button, o) => - { - currentSwapFunc?.Invoke(); - return false; - } - }; - - enabledModsList.OnSelected = (frame, o) => - { - disabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); - - SetSwapFunc(enabledModsList, disabledModsList); - - return true; - }; - disabledModsList.OnSelected = (frame, o) => - { - enabledModsList.Deselect(); - - centerButton.Visible = true; - centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); - - SetSwapFunc(disabledModsList, enabledModsList); - - return true; - }; - - var searchBox = CreateSearchBox(mainLayout, width: 0.5f); - modsListFilter = searchBox; - - new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), - onUpdate: (f, component) => - { - HandleDraggingAcrossModLists(enabledModsList, disabledModsList); - HandleDraggingAcrossModLists(disabledModsList, enabledModsList); - if (PlayerInput.PrimaryMouseButtonClicked() - && !GUI.IsMouseOn(enabledModsList) - && !GUI.IsMouseOn(disabledModsList) - && GUIContextMenu.CurrentContextMenu is null) - { - enabledModsList.Deselect(); - disabledModsList.Deselect(); - } - else if (!PlayerInput.IsCtrlDown() && !PlayerInput.IsShiftDown() && PlayerInput.DoubleClicked()) - { - currentSwapFunc?.Invoke(); - } - }, - onDraw: (spriteBatch, component) => - { - enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); - }); - } - - protected override void UpdateModListItemVisibility() - { - string str = modsListFilter.Text; - enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); - } - - private void PrepareToShowModInfo(ContentPackage mod) - { - TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId), - t => - { - if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } - if (item is null) { return; } - onInstalledInfoButtonHit(item.Value); - }); - } - - public void PopulateInstalledModLists(bool forceRefreshEnabled = false, bool refreshDisabled = true) - { - bulkUpdateButton.Enabled = false; - bulkUpdateButton.ToolTip = ""; - ContentPackageManager.UpdateContentPackageList(); - - SwapDropdownValues(enabledCoreDropdown, - (p) => p.Name, - ContentPackageManager.CorePackages.ToArray(), - ContentPackageManager.EnabledPackages.Core!, - (p) => { }); - - void addRegularModToList(RegularPackage mod, GUIListBox list) - { - var modFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), list.Content.RectTransform), - style: "ListBoxElement") - { - UserData = mod - }; - - var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), - onUpdate: (f, component) => - { - var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) - if (parentList is null) { return; } - if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) - { - if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } - static void noop() { } - - List contextMenuOptions = new List(); - if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - contextMenuOptions.Add( - new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); - } - - Identifier swapLabel - = ((parentList == enabledRegularModsList ? "Disable" : "Enable") - + (parentList.AllSelected.Count > 1 ? "SelectedWorkshopMods" : "WorkshopMod")) - .ToIdentifier(); - - contextMenuOptions.Add(new ContextMenuOption(swapLabel, - isEnabled: true, onSelected: currentSwapFunc ?? noop)); - - GUIContextMenu.CreateContextMenu( - pos: PlayerInput.MousePosition, - header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), - headerColor: null, - contextMenuOptions.ToArray()); - } - }); - - var frameContent = new GUILayoutGroup(new RectTransform((0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), - style: "GUIDragIndicator") - { - CanBeFocused = false - }; - - var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) - { - CanBeFocused = false - }; - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), - text: mod.Name) - { - CanBeFocused = false - }; - if (mod.Errors.Any()) - { - CreateModErrorInfo(mod, modFrame, modName); - } - if (ContentPackageManager.LocalPackages.Contains(mod)) - { - var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: "WorkshopMenu.EditButton") - { - OnClicked = (button, o) => - { - ToolBox.OpenFileWithShell(mod.Dir); - return false; - }, - ToolTip = TextManager.Get("OpenLocalModInExplorer") - }; - } - else if (ContentPackageManager.WorkshopPackages.Contains(mod)) - { - var infoButton = new GUIButton( - new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: null) - { - CanBeSelected = false, - OnClicked = (button, o) => - { - PrepareToShowModInfo(mod); - return false; - } - }; - if (!SteamManager.IsInitialized) - { - infoButton.Enabled = false; - } - TaskPool.Add( - $"DetermineUpdateRequired{mod.SteamWorkshopId}", - mod.IsUpToDate(), - t => - { - if (!t.TryGetResult(out bool isUpToDate)) { return; } - - if (!isUpToDate) - { - infoButton.CanBeSelected = true; - infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); - infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); - bulkUpdateButton.Enabled = true; - bulkUpdateButton.ToolTip = TextManager.Get("ModUpdatesAvailable"); - } - }); - } - } - - void addRegularModsToList(IEnumerable mods, GUIListBox list) - { - list.ClearChildren(); - foreach (var mod in mods) - { - addRegularModToList(mod, list); - } - } - - var enabledMods = - (forceRefreshEnabled || (enabledRegularModsList.Content.CountChildren + disabledRegularModsList.Content.CountChildren == 0) - ? ContentPackageManager.EnabledPackages.Regular - : enabledRegularModsList.Content.Children - .Select(c => c.UserData) - .OfType() - .Where(p => ContentPackageManager.RegularPackages.Contains(p))) - .ToArray(); - var disabledMods = ContentPackageManager.RegularPackages.Where(p => !enabledMods.Contains(p)); - - addRegularModsToList(enabledMods, enabledRegularModsList); - if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } - - TaskPool.Add( - $"DetermineWorkshopModIcons", - SteamManager.Workshop.GetPublishedItems(), - t => - { - if (!t.TryGetResult(out ISet items)) { return; } - var ids = items.Select(it => it.Id).ToHashSet(); - - foreach (var child in enabledRegularModsList.Content.Children - .Concat(disabledRegularModsList.Content.Children)) - { - var mod = child.UserData as RegularPackage; - if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } - - var btn = child.GetChild()?.GetAllChildren().Last(); - if (btn is null) { continue; } - if (btn.Style != null) { continue; } - - btn.ApplyStyle( - GUIStyle.GetComponentStyle( - ids.Contains(mod.SteamWorkshopId) - ? "WorkshopMenu.PublishedIcon" - : "WorkshopMenu.DownloadedIcon")); - btn.ToolTip = TextManager.Get( - ids.Contains(mod.SteamWorkshopId) - ? "PublishedWorkshopMod" - : "DownloadedWorkshopMod"); - btn.HoverCursor = CursorState.Default; - } - }); - - UpdateModListItemVisibility(); - } - private void CreatePopularModsTab(out GUIListBox popularModsList) { GUIFrame content = CreateNewContentFrame(Tab.PopularMods); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index e803a86e2..cbe5fca4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -106,10 +106,8 @@ namespace Barotrauma.Steam => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) { UserData = new ActionCarrier(id, action) }; - protected GUITextBox CreateSearchBox(GUILayoutGroup mainLayout, float width = 1.0f, float heightScale = 1.0f) + protected GUITextBox CreateSearchBox(RectTransform searchRectT) { - var searchRectT = NewItemRectT(mainLayout, heightScale: heightScale); - searchRectT.RelativeSize = (width, searchRectT.RelativeSize.Y); var searchHolder = new GUIFrame(searchRectT, style: null); var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, @@ -142,7 +140,8 @@ namespace Barotrauma.Steam const int maxErrorsToShow = 5; nameText.TextColor = GUIStyle.Red; uiElement.ToolTip = - TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.error)); + TextManager.GetWithVariable("contentpackagehaserrors", "[packagename]", mod.Name) + + '\n' + string.Join('\n', mod.Errors.Take(maxErrorsToShow).Select(e => e.Message)); if (mod.Errors.Count() > maxErrorsToShow) { uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs index ebe59aaf8..003aab946 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs @@ -1,3 +1,5 @@ +using System; + #nullable enable namespace Barotrauma.Steam @@ -7,5 +9,8 @@ namespace Barotrauma.Steam public WorkshopMenu(GUIFrame parent) { } protected abstract void UpdateModListItemVisibility(); + + protected bool ModNameMatches(ContentPackage p, string query) + => p.Name.Contains(query, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index f691db09d..2763d0211 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7842466de..d129a0104 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index fa39bbdbb..1f520e768 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,13 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable app.manifest + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index dec7d455e..77e6d7d8a 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6d151701f..c68077f24 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index c58fb75c8..9ad728290 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -647,6 +647,7 @@ namespace Barotrauma { msg.Write(false); } + msg.Write(HumanPrefabHealthMultiplier); msg.Write(Wallet.Balance); msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); msg.Write((byte)TeamID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 61c141015..a9a05313a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1676,7 +1676,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; } - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport; NewMessage(client.Name + (GameMain.GameSession.Map.AllowDebugTeleport ? " enabled" : " disabled") + " teleportation on the campaign map.", Color.White); GameMain.Server.SendConsoleMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", client); @@ -2274,7 +2274,6 @@ namespace Barotrauma Wallet wallet = targetCharacter is null ? campaign.Bank : targetCharacter.Wallet; wallet.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); - campaign.LastUpdateID++; } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 89ed9f0dc..a562798b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -37,7 +37,7 @@ namespace Barotrauma { if (forceMapUI == value) { return; } forceMapUI = value; - LastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); } } @@ -71,11 +71,43 @@ namespace Barotrauma get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } } - public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings settings) + private bool purchasedHullRepairs, purchasedLostShuttles, purchasedItemRepairs; + public override bool PurchasedHullRepairs + { + get { return purchasedHullRepairs; } + set + { + if (purchasedHullRepairs == value) { return; } + purchasedHullRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedLostShuttles + { + get { return purchasedLostShuttles; } + set + { + if (purchasedLostShuttles == value) { return; } + purchasedLostShuttles = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + public override bool PurchasedItemRepairs + { + get { return purchasedItemRepairs; } + set + { + if (purchasedItemRepairs == value) { return; } + purchasedItemRepairs = value; + IncrementLastUpdateIdForFlag(NetFlags.Misc); + } + } + + public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings startingSettings) { if (string.IsNullOrWhiteSpace(savePath)) { return; } - GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, settings, seed); + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, startingSettings, seed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); SaveUtil.SaveGame(GameMain.GameSession.SavePath); @@ -158,7 +190,7 @@ namespace Barotrauma public override void Start() { base.Start(); - lastUpdateID++; + IncrementAllLastUpdateIds(); } private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection; @@ -274,7 +306,7 @@ namespace Barotrauma protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { - lastUpdateID++; + IncrementAllLastUpdateIds(); switch (transitionType) { @@ -321,6 +353,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -329,7 +362,7 @@ namespace Barotrauma GameMain.Server.EndGame(TransitionType.None, wasSaved: false); LoadCampaign(GameMain.GameSession.SavePath); LastSaveID++; - LastUpdateID++; + IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; } @@ -360,14 +393,14 @@ namespace Barotrauma } partial void InitProjSpecific() - { - CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; }; - CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; }; - CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; - UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; - Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; - Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; - Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; + { + CargoManager.OnItemsInBuyCrateChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate); }; + CargoManager.OnPurchasedItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.PurchasedItems); }; + CargoManager.OnSoldItemsChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.SoldItems); }; + UpgradeManager.OnUpgradesChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager); }; + Map.OnLocationSelected += (loc, connection) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Map.OnMissionsSelected += (loc, mission) => { IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions); }; + Reputation.OnAnyReputationValueChanged += () => { IncrementLastUpdateIdForFlag(NetFlags.Reputation); }; //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; @@ -388,6 +421,7 @@ namespace Barotrauma discardedCharacters.Add(data); } characterData.Remove(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } } } @@ -402,6 +436,7 @@ namespace Barotrauma characterData.RemoveAll(cd => cd.MatchesClient(client)); var data = new CharacterCampaignData(client); characterData.Add(data); + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); return data; } @@ -413,6 +448,7 @@ namespace Barotrauma var matchingData = GetClientCharacterData(client); if (matchingData != null) { client.CharacterInfo = matchingData.CharacterInfo; } } + IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo); } public Dictionary GetAssignedJobs(IEnumerable connectedClients) @@ -517,127 +553,187 @@ namespace Barotrauma base.End(transitionType); } + private bool IsFlagRequired(Client c, NetFlags flag) + => !c.LastRecvCampaignUpdate.TryGetValue(flag, out var id) || NetIdUtils.IdMoreRecent(GetLastUpdateIdForFlag(flag), id); + public void ServerWrite(IWriteMessage msg, Client c) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); - Reputation reputation = Map?.CurrentLocation?.Reputation; + NetFlags requiredFlags = lastUpdateID.Keys.Where(k => IsFlagRequired(c, k)).Aggregate((NetFlags)0, (f1, f2) => f1 | f2); + + msg.Write((UInt16)requiredFlags); msg.Write(IsFirstRound); msg.Write(CampaignID); - msg.Write(lastUpdateID); msg.Write(lastSaveID); msg.Write(map.Seed); - msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); - - var selectedMissionIndices = map.GetSelectedMissionIndices(); - msg.Write((byte)selectedMissionIndices.Count()); - foreach (int selectedMissionIndex in selectedMissionIndices) + + if (requiredFlags.HasFlag(NetFlags.Misc)) { - msg.Write((byte)selectedMissionIndex); + msg.Write(GetLastUpdateIdForFlag(NetFlags.Misc)); + msg.Write(PurchasedHullRepairs); + msg.Write(PurchasedItemRepairs); + msg.Write(PurchasedLostShuttles); } - var subList = GameMain.NetLobbyScreen.GetSubList(); - List ownedSubmarineIndices = new List(); - for (int i = 0; i < subList.Count; i++) + if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) { - if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) + msg.Write(GetLastUpdateIdForFlag(NetFlags.MapAndMissions)); + msg.Write(ForceMapUI); + msg.Write(map.AllowDebugTeleport); + msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); + msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + + if (map.CurrentLocation != null) { - ownedSubmarineIndices.Add(i); - } - } - msg.Write((ushort)ownedSubmarineIndices.Count); - foreach (int index in ownedSubmarineIndices) - { - msg.Write((ushort)index); - } - - msg.Write(map.AllowDebugTeleport); - msg.Write(reputation != null); - if (reputation != null) { msg.Write(reputation.Value); } - - // hopefully we'll never have more than 128 factions - msg.Write((byte)Factions.Count); - foreach (Faction faction in Factions) - { - msg.Write(faction.Prefab.Identifier); - msg.Write(faction.Reputation.Value); - } - - msg.Write(ForceMapUI); - - msg.Write(PurchasedHullRepairs); - msg.Write(PurchasedItemRepairs); - msg.Write(PurchasedLostShuttles); - - if (map.CurrentLocation != null) - { - msg.Write((byte)map.CurrentLocation?.AvailableMissions.Count()); - foreach (Mission mission in map.CurrentLocation.AvailableMissions) - { - msg.Write(mission.Prefab.Identifier); - if (mission.Locations[0] == mission.Locations[1]) + msg.Write((byte)map.CurrentLocation.AvailableMissions.Count()); + foreach (Mission mission in map.CurrentLocation.AvailableMissions) { - msg.Write((byte)255); - } - else - { - Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; - LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + msg.Write(mission.Prefab.Identifier); + if (mission.Locations[0] == mission.Locations[1]) + { + msg.Write((byte)255); + } + else + { + Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; + LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); + msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + } } } - - // Store balance - bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); - msg.Write(hasStores); - if (hasStores) + else { - msg.Write((byte)map.CurrentLocation.Stores.Count); - foreach (var store in map.CurrentLocation.Stores.Values) + msg.Write((byte)0); + } + + var selectedMissionIndices = map.GetSelectedMissionIndices(); + msg.Write((byte)selectedMissionIndices.Count()); + foreach (int selectedMissionIndex in selectedMissionIndices) + { + msg.Write((byte)selectedMissionIndex); + } + + WriteStores(msg); + } + + if (requiredFlags.HasFlag(NetFlags.SubList)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.SubList)); + var subList = GameMain.NetLobbyScreen.GetSubList(); + List ownedSubmarineIndices = new List(); + for (int i = 0; i < subList.Count; i++) + { + if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name)) { - msg.Write(store.Identifier); - msg.Write((UInt16)store.Balance); + ownedSubmarineIndices.Add(i); } } + msg.Write((ushort)ownedSubmarineIndices.Count); + foreach (int index in ownedSubmarineIndices) + { + msg.Write((ushort)index); + } } - else + if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) { - msg.Write((byte)0); - // Store balance - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.UpgradeManager)); + msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); + foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + { + msg.Write(prefab.Identifier); + msg.Write(category.Identifier); + msg.Write((byte)level); + } + msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + { + msg.Write(itemSwap.ItemToRemove.ID); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + } } - WriteItems(msg, CargoManager.ItemsInBuyCrate); - WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); - WriteItems(msg, CargoManager.PurchasedItems); - WriteItems(msg, CargoManager.SoldItems); - - msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); - foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) + if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) { - msg.Write(prefab.Identifier); - msg.Write(category.Identifier); - msg.Write((byte)level); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate)); + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteStores(msg); } - msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); - foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) { - msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); + msg.Write(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate)); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteStores(msg); } - var characterData = GetClientCharacterData(c); - if (characterData?.CharacterInfo == null) + if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) { - msg.Write(false); + msg.Write(GetLastUpdateIdForFlag(NetFlags.PurchasedItems)); + WriteItems(msg, CargoManager.PurchasedItems); + WriteStores(msg); + } - else + if (requiredFlags.HasFlag(NetFlags.SoldItems)) { - msg.Write(true); - characterData.CharacterInfo.ServerWrite(msg); + msg.Write(GetLastUpdateIdForFlag(NetFlags.SoldItems)); + WriteItems(msg, CargoManager.SoldItems); + WriteStores(msg); + } + if (requiredFlags.HasFlag(NetFlags.Reputation)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.Reputation)); + Reputation reputation = Map?.CurrentLocation?.Reputation; + msg.Write(reputation != null); + if (reputation != null) { msg.Write(reputation.Value); } + + // hopefully we'll never have more than 128 factions + msg.Write((byte)Factions.Count); + foreach (Faction faction in Factions) + { + msg.Write(faction.Prefab.Identifier); + msg.Write(faction.Reputation.Value); + } + } + if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) + { + msg.Write(GetLastUpdateIdForFlag(NetFlags.CharacterInfo)); + var characterData = GetClientCharacterData(c); + if (characterData?.CharacterInfo == null) + { + msg.Write(false); + } + else + { + msg.Write(true); + characterData.CharacterInfo.ServerWrite(msg); + } + } + + void WriteStores(IWriteMessage msg) + { + if (map.CurrentLocation != null) + { + // Store balance + bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); + msg.Write(hasStores); + if (hasStores) + { + msg.Write((byte)map.CurrentLocation.Stores.Count); + foreach (var store in map.CurrentLocation.Stores.Values) + { + msg.Write(store.Identifier); + msg.Write((UInt16)store.Balance); + } + } + } + else + { + msg.Write((byte)0); + // Store balance + msg.Write(false); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index d7223e2c8..4145c0b35 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -102,6 +102,7 @@ namespace Barotrauma.Items.Components { msg.Write(autoPilot); msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); + msg.Write(user?.ID ?? Entity.NullEntityID); if (!autoPilot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index ea54be031..ed9a1efa2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Networking public UInt16 LastSentEntityEventID = 0; public UInt16 LastRecvEntityEventID = 0; - public UInt16 LastRecvCampaignUpdate = 0; + public readonly Dictionary LastRecvCampaignUpdate = new Dictionary(); public UInt16 LastRecvCampaignSave = 0; public (UInt16 saveId, float time) LastCampaignSaveSendTime; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index ade75cd83..54bbd573c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading.Tasks; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8b3b20d36..234e94be4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -155,7 +155,7 @@ namespace Barotrauma.Networking else { Log("Using SteamP2P networking.", ServerLog.MessageType.ServerMessage); - serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, serverSettings); + serverPeer = new SteamP2PServerPeer(ownerSteamId.Value, ownerKey.Value, serverSettings); } serverPeer.OnInitializationComplete = OnInitializationComplete; @@ -746,7 +746,7 @@ namespace Barotrauma.Networking string seed = inc.ReadString(); string subName = inc.ReadString(); string subHash = inc.ReadString(); - CampaignSettings settings = new CampaignSettings(inc); + CampaignSettings settings = INetSerializableStruct.Read(inc); var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); @@ -767,8 +767,7 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { - ServerSettings.RadiationEnabled = settings.RadiationEnabled; - ServerSettings.MaxMissionCount = settings.MaxMissionCount; + ServerSettings.CampaignSettings = settings; ServerSettings.SaveSettings(); MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } @@ -833,6 +832,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.EVENTMANAGER_RESPONSE: GameMain.GameSession?.EventManager.ServerRead(inc, connectedClient); break; + case ClientPacketHeader.UPDATE_CHARACTERINFO: + UpdateCharacterInfo(inc, connectedClient); + break; case ClientPacketHeader.ERROR: HandleClientError(inc, connectedClient); break; @@ -1050,9 +1052,11 @@ namespace Barotrauma.Networking if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1061,7 +1065,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1122,9 +1130,11 @@ namespace Barotrauma.Networking if (c.LastRecvCampaignSave > 0) { byte campaignID = inc.ReadByte(); - c.LastRecvCampaignUpdate = inc.ReadUInt16(); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = inc.ReadUInt16(); + } bool characterDiscarded = inc.ReadBoolean(); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { if (characterDiscarded) { campaign.DiscardClientCharacterData(c); } @@ -1133,7 +1143,11 @@ namespace Barotrauma.Networking if (campaign.CampaignID != campaignID) { c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); - c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); + foreach (MultiPlayerCampaign.NetFlags netFlag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + c.LastRecvCampaignUpdate[netFlag] = + (UInt16)(campaign.GetLastUpdateIdForFlag(netFlag) - 1); + } } } } @@ -1370,7 +1384,7 @@ namespace Barotrauma.Networking if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedOutpost && save) + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) { mpCampaign.SavePlayers(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); @@ -1672,8 +1686,7 @@ namespace Barotrauma.Networking outmsg.Write(c.LastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server outmsg.Write(c.LastSentEntityEventID); - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -1899,8 +1912,7 @@ namespace Barotrauma.Networking int campaignBytes = outmsg.LengthBytes; var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (outmsg.LengthBytes < MsgConstants.MTU - 500 && - campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode && - NetIdUtils.IdMoreRecent(campaign.LastUpdateID, c.LastRecvCampaignUpdate)) + campaign != null && campaign.Preset == GameMain.NetLobbyScreen.SelectedMode) { outmsg.Write(true); outmsg.WritePadBits(); @@ -2049,7 +2061,10 @@ namespace Barotrauma.Networking var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); msg.Write(campaign == null ? (UInt16)0 : campaign.LastSaveID); - msg.Write(campaign == null ? (UInt16)0 : campaign.LastUpdateID); + foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) + { + msg.Write(campaign == null ? (UInt16)0 : campaign.GetLastUpdateIdForFlag(flag)); + } connectedClients.ForEach(c => c.ReadyToStart = false); @@ -2077,7 +2092,7 @@ namespace Barotrauma.Networking } } - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Unsure), false); + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Empty), false); yield return CoroutineStatus.Success; } @@ -2195,7 +2210,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); - AutoItemPlacer.SpawnItems(); + AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet); CrewManager crewManager = campaign?.CrewManager; @@ -3203,18 +3218,27 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(Voting.ActiveVote.VoteType) == 1); - int max = GameMain.Server.ConnectedClients.Count(c => c.InGame); - // Required ratio cannot be met - if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) - { - Voting.ActiveVote.Finish(Voting, passed: false); - } - else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + if (inGameClients.Count() == 1) { Voting.ActiveVote.Finish(Voting, passed: true); - } + } + else + { + var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); + int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 1); + int max = eligibleClients.Count(); + // Required ratio cannot be met + if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: false); + } + else if (yes / (float)max >= serverSettings.VoteRequiredRatio) + { + Voting.ActiveVote.Finish(Voting, passed: true); + } + } } Client.UpdateKickVotes(connectedClients); @@ -3295,7 +3319,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); } Voting.StopSubmarineVote(true); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 2d1cb634c..8718bc4d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -16,14 +16,21 @@ namespace Barotrauma.Networking private set; } - public SteamP2PServerPeer(UInt64 steamId, ServerSettings settings) + private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Value); + + private UInt64 ReadSteamId(IReadMessage inc) + => inc.ReadUInt64() ^ ownerKey64; + private void WriteSteamId(IWriteMessage msg, UInt64 val) + => msg.Write(val ^ ownerKey64); + + public SteamP2PServerPeer(UInt64 steamId, int ownerKey, ServerSettings settings) { serverSettings = settings; connectedClients = new List(); pendingClients = new List(); - ownerKey = null; + this.ownerKey = ownerKey; OwnerSteamID = steamId; @@ -33,7 +40,7 @@ namespace Barotrauma.Networking public override void Start() { IWriteMessage outMsg = new WriteOnlyMessage(); - outMsg.Write(OwnerSteamID); + WriteSteamId(outMsg, OwnerSteamID); outMsg.Write((byte)DeliveryMethod.Reliable); outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage)); @@ -122,8 +129,8 @@ namespace Barotrauma.Networking { if (!started) { return; } - UInt64 senderSteamId = inc.ReadUInt64(); - UInt64 ownerSteamId = inc.ReadUInt64(); + UInt64 senderSteamId = ReadSteamId(inc); + UInt64 ownerSteamId = ReadSteamId(inc); PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); @@ -264,7 +271,7 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[16]; msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); - msgToSend.Write(conn.SteamID); + WriteSteamId(msgToSend, conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); msgToSend.Write((UInt16)length); @@ -281,7 +288,7 @@ namespace Barotrauma.Networking if (string.IsNullOrWhiteSpace(msg)) { return; } IWriteMessage msgToSend = new WriteOnlyMessage(); - msgToSend.Write(steamId); + WriteSteamId(msgToSend, steamId); msgToSend.Write((byte)DeliveryMethod.Reliable); msgToSend.Write((byte)(PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage)); msgToSend.Write(msg); @@ -318,7 +325,7 @@ namespace Barotrauma.Networking protected override void SendMsgInternal(NetworkConnection conn, DeliveryMethod deliveryMethod, IWriteMessage msg) { IWriteMessage msgToSend = new WriteOnlyMessage(); - msgToSend.Write(conn.SteamID); + WriteSteamId(msgToSend, conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write(msg.Buffer, 0, msg.LengthBytes); byte[] bufToSend = (byte[])msgToSend.Buffer.Clone(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index bc841e5a9..39f7833c9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -1,11 +1,9 @@ -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.IO; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -36,7 +34,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); private bool IsFlagRequired(Client c, NetFlags flag) - => LastUpdateIdForFlag[flag] > c.LastRecvLobbyUpdate; + => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys @@ -56,7 +54,7 @@ namespace Barotrauma.Networking { var property = netProperties[key]; property.SyncValue(); - if (property.LastUpdateID > c.LastRecvLobbyUpdate) + if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) { outMsg.Write(key); netProperties[key].Write(outMsg); @@ -257,7 +255,7 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("queryport", QueryPort); #endif doc.Root.SetAttributeValue("password", password ?? ""); - + doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); @@ -266,11 +264,12 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); - + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); + doc.Root.Add(CampaignSettings.Save()); System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { @@ -399,7 +398,7 @@ namespace Barotrauma.Networking ServerName = doc.Root.GetAttributeString("name", ""); if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); - + GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; //handle Random as the mission type, which is no longer a valid setting //MissionType.All offers equivalent functionality @@ -410,6 +409,14 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + + foreach (XElement element in doc.Root.Elements()) + { + if (element.Name.ToIdentifier() == nameof(Barotrauma.CampaignSettings)) + { + CampaignSettings = new CampaignSettings(element); + } + } } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 13b9156e4..4a3577fe5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -27,11 +27,13 @@ namespace Barotrauma public VoteState State { get; set; } public SubmarineInfo Sub; + public bool TransferItems; public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) { Sub = subInfo; + TransferItems = transferItems; DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; @@ -101,15 +103,12 @@ namespace Barotrauma private readonly Dictionary rejectedVoteTimes = new Dictionary(); - private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) + private void StartSubmarineVote(SubmarineInfo subInfo, bool transferItems, VoteType voteType, Client sender) { - if (ActiveVote == null) - { - sender.SetVote(voteType, 2); - } var subVote = new SubmarineVote( sender, subInfo, + transferItems, voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); @@ -152,10 +151,6 @@ namespace Barotrauma { return; } - if (ActiveVote == null) - { - starter.SetVote(VoteType.TransferMoney, 2); - } StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to)); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } @@ -205,11 +200,19 @@ namespace Barotrauma if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) { + var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); + var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter); + // Do not take unanswered into account for total - int yes = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - int no = GameMain.Server.ConnectedClients.Count(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); + int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); + int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); int total = Math.Max(yes + no, 1); - ActiveVote.Finish(this, passed: yes / (float)(total) >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio); + + bool passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + + ActiveVote.Finish(this, passed); } } @@ -293,12 +296,13 @@ namespace Barotrauma { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + bool transferItems = inc.ReadBoolean(); if (!ShouldRejectVote(sender, voteType)) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) { - StartSubmarineVote(subInfo, voteType, sender); + StartSubmarineVote(subInfo, transferItems, voteType, sender); } } } @@ -355,22 +359,24 @@ namespace Barotrauma { msg.Write((byte)ActiveVote.VoteType); if (ActiveVote.State != VoteState.None && ActiveVote.VoteType != VoteType.Unknown) - { - var yesClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 2); - msg.Write((byte)yesClients.Count); + { + var eligibleClients = GameMain.Server.ConnectedClients.Where(c => c.InGame && c != ActiveVote.VoteStarter); + + var yesClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 2); + msg.Write((byte)yesClients.Count()); foreach (Client c in yesClients) { msg.Write(c.ID); } - var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); - msg.Write((byte)noClients.Count); + var noClients = eligibleClients.Where(c => c.GetVote(ActiveVote.VoteType) == 1); + msg.Write((byte)noClients.Count()); foreach (Client c in noClients) { msg.Write(c.ID); } - msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.InGame)); + msg.Write((byte)eligibleClients.Count()); switch (ActiveVote.State) { @@ -384,6 +390,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: msg.Write((ActiveVote as SubmarineVote).Sub.Name); + msg.Write((ActiveVote as SubmarineVote).TransferItems); break; case VoteType.TransferMoney: var transferVote = (ActiveVote as TransferVote); @@ -405,8 +412,10 @@ namespace Barotrauma case VoteType.PurchaseSub: case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: - msg.Write((ActiveVote as SubmarineVote).Sub.Name); - msg.Write((short)(ActiveVote as SubmarineVote).DeliveryFee); + var subVote = ActiveVote as SubmarineVote; + msg.Write(subVote.Sub.Name); + msg.Write(subVote.TransferItems); + msg.Write((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index a1886f6b2..613378097 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.18.2.0 + 0.18.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable + true ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml new file mode 100644 index 000000000..a80fcbe81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index f13ea197e..82b3e7591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -34,14 +34,18 @@ namespace Barotrauma public float SoundRange { get { return soundRange; } - set + set { if (float.IsNaN(value)) { DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange); + if (soundRange > 0.0f && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -55,7 +59,11 @@ namespace Barotrauma DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); + if (sightRange > 0 && !Static && FadeOutTime > 0.0f) + { + NeedsUpdate = true; + } } } @@ -99,13 +107,33 @@ namespace Barotrauma /// public bool InDetectable { - get => inDetectable || (SoundRange <= 0 && SightRange <= 0); - set => inDetectable = value; + get + { + return inDetectable || (SoundRange <= 0 && SightRange <= 0); + } + set + { + inDetectable = value; + if (inDetectable) + { + NeedsUpdate = true; + } + } } + public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; + /// + /// Does the AI target do something that requires Update() to be called (e.g. static targets don't need to be updated) + /// + public bool NeedsUpdate + { + get; + private set; + } = true; + public TargetType Type { get; private set; } public enum TargetType @@ -190,14 +218,22 @@ namespace Barotrauma if (!Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active - if (!StaticSight && SightRange > 0) + if (!StaticSight && sightRange > 0) { DecreaseSightRange(deltaTime); } - if (!StaticSound && SoundRange > 0) + if (!StaticSound && soundRange > 0) { DecreaseSoundRange(deltaTime); } + if (sightRange <= 0 && soundRange <= 0) + { + NeedsUpdate = false; + } + } + else + { + NeedsUpdate = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 779a6fb30..8897ebecb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1440,14 +1440,33 @@ namespace Barotrauma } else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) { - reachTimer += deltaTime; - if (reachTimer > reachTimeOut) + Vector2 targetVelocity = Vector2.Zero; + Submarine targetSub = SelectedAiTarget.Entity.Submarine; + if (targetSub != null) { - reachTimer = 0; - IgnoreTarget(SelectedAiTarget); - State = AIState.Idle; - ResetAITarget(); - return; + targetVelocity = targetSub.Velocity; + } + else if (targetCharacter != null) + { + targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity; + } + else if (SelectedAiTarget.Entity is Item i && i.body != null) + { + targetVelocity = i.body.LinearVelocity; + } + float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared(); + float targetSpeed = targetVelocity.LengthSquared(); + if (mySpeed < 0.1f || mySpeed > targetSpeed) + { + reachTimer += deltaTime; + if (reachTimer > reachTimeOut) + { + reachTimer = 0; + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + ResetAITarget(); + return; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index a63a5e0ad..ff7c37218 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -866,8 +866,8 @@ namespace Barotrauma var container = i.GetComponent(); if (container == null) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } - var rootContainer = container.Item.GetRootContainer(); - if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } + var rootContainer = container.Item.GetRootContainer() ?? container.Item; + if (rootContainer.GetComponent() != null || rootContainer.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) @@ -882,7 +882,12 @@ namespace Barotrauma } else { - return isPreferencesDefined ? 0 : 1; + if (isPreferencesDefined) + { + // Use any valid locker as a fall back container. + return container.Item.HasTag("locker") ? 0.5f : 0; + } + return 1; } } } @@ -1950,11 +1955,10 @@ namespace Barotrauma enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } float dangerousItemsFactor = 1f; - foreach (Item item in Item.ItemList) + foreach (Item item in Item.DangerousItems) { - if (item.CurrentHull != hull) { continue; } - if (item.Prefab != null && item.Prefab.IsDangerous) - { + if (item.CurrentHull == hull) + { dangerousItemsFactor = 0; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 5e618f9e7..1a14bd206 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -245,7 +245,7 @@ namespace Barotrauma { get { - if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8807abfd1..a2567e72b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -48,16 +48,29 @@ namespace Barotrauma } else { - float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). - // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); - float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float reduction = isPriority ? 1 : 2; - float max = AIObjectiveManager.LowestOrderPriority - reduction; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; + if (operateObjective != null && objectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + { + // Prioritize leaks that we are already fixing + Priority = maxPriority; + } + else + { + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); + if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) + { + // Double the distance when the leak can be accessed from the current hull. + distanceFactor *= 2; + } + float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + } } return Priority; } @@ -202,7 +215,7 @@ namespace Barotrauma // This is an approximation, because we don't know the exact reach until the pose is taken. // And even then the actual range depends on the direction we are aiming to. // Found out that without any multiplier the value (209) is often too short. - return repairTool.Range + armLength * 1.3f; + return repairTool.Range + armLength * 2; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 762e8cd08..a63159e19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -185,6 +185,11 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } else { @@ -290,12 +295,25 @@ namespace Barotrauma { PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); } + else + { + PathSteering.ResetPath(); + PathSteering.Reset(); + } } } public void Wander(float deltaTime) { - if (character.IsClimbing) { return; } + if (character.IsClimbing) + { + if (character.AnimController.GetHeightFromFloor() < 0.1f) + { + character.AnimController.Anim = AnimController.Animation.None; + character.SelectedConstruction = null; + } + return; + } var currentHull = character.CurrentHull; if (!character.AnimController.InWater && currentHull != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 9b05c7e0a..652ce82a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -142,7 +142,7 @@ namespace Barotrauma } var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 27b75c722..ee2c985fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -238,7 +238,7 @@ namespace Barotrauma }; if (repairTool != null) { - objective.CloseEnough = repairTool.Range * 0.75f; + objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character); } return objective; }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index d9039091d..e4e900990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -1,8 +1,6 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using FarseerPhysics; -using Barotrauma.Extensions; namespace Barotrauma { @@ -90,6 +88,10 @@ namespace Barotrauma { steering = Vector2.Normalize(steering) * Math.Abs(speed); } + if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null) + { + steering = -steering; + } host.Steering = steering; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 81b328b03..f0818dd66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,8 +22,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - #warning TODO: this is kinda janky, this should probably be done better - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName)); + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); if (!character.VariantOf.IsEmpty) { _ragdollParams.ApplyVariantScale(character.Params.VariantFile); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 294285a87..b760a2b2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -74,7 +74,7 @@ namespace Barotrauma } } - public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count; + public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count; private bool frozen; public bool Frozen @@ -1850,36 +1850,30 @@ namespace Barotrauma } /// - /// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary. + /// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned. /// public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { - Limb limb = null; - if (HasMultipleLimbsOfSameType) + if (limbDictionary.TryGetValue(limbType, out Limb limb)) { - for (int i = 0; i < 10; i++) + if (excludeSevered && limb.IsSevered) { - limbDictionary.TryGetValue(limbType, out limb); - if (limb == null) + limb = null; + } + } + if (limb == null && HasMultipleLimbsOfSameType) + { + // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. + foreach (var l in limbs) + { + if (l.type != limbType) { continue; } + if (!excludeSevered || !l.IsSevered) { - // No limbs found - break; - } - if (!excludeSevered || !limb.IsSevered) - { - // Found a valid limb + limb = l; break; } } } - else - { - limbDictionary.TryGetValue(limbType, out limb); - } - if (excludeSevered && limb != null && limb.IsSevered) - { - limb = null; - } return limb; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d31d943a0..e1d53f45d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -117,7 +117,29 @@ namespace Barotrauma protected Key[] keys; - public HumanPrefab HumanPrefab; + private HumanPrefab humanPrefab; + public HumanPrefab HumanPrefab + { + get { return humanPrefab; } + set + { + if (humanPrefab == value) { return; } + humanPrefab = value; + + if (humanPrefab != null) + { + HumanPrefabHealthMultiplier = humanPrefab.HealthMultiplier; + if (GameMain.NetworkMember != null) + { + HumanPrefabHealthMultiplier *= humanPrefab.HealthMultiplierInMultiplayer; + } + } + else + { + HumanPrefabHealthMultiplier = 1.0f; + } + } + } private CharacterTeamType teamID; public CharacterTeamType TeamID @@ -1192,7 +1214,7 @@ namespace Barotrauma CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } - if (Params.Husk && speciesName != "husk") + if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk") { // Get the non husked name and find the ragdoll with it var matchingAffliction = AfflictionPrefab.List @@ -1392,7 +1414,7 @@ namespace Barotrauma if (inputType == InputType.Up || inputType == InputType.Down || inputType == InputType.Left || inputType == InputType.Right) { - var invertControls = CharacterHealth.GetAffliction("invertcontrols"); + var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()); if (invertControls != null) { switch (inputType) @@ -1652,14 +1674,9 @@ namespace Barotrauma } /// - /// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier + /// Health multiplier of the human prefab this character is an instance of (if any) /// - public float StaticHealthMultiplier { get; private set; } = 1; - - public void AddStaticHealthMultiplier(float newMultiplier) - { - StaticHealthMultiplier *= newMultiplier; - } + public float HumanPrefabHealthMultiplier { get; private set; } = 1; /// /// Speed reduction from the current limb specific damage. Min 0, max 1. @@ -4824,21 +4841,21 @@ namespace Barotrauma } } - private readonly List abilityFlags = new List(); + private AbilityFlags abilityFlags; public void AddAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Add(abilityFlag); + abilityFlags |= abilityFlag; } public void RemoveAbilityFlag(AbilityFlags abilityFlag) { - abilityFlags.Remove(abilityFlag); + abilityFlags &= ~abilityFlag; } public bool HasAbilityFlag(AbilityFlags abilityFlag) { - return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); + return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } private readonly Dictionary abilityResistances = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 93fe641e6..c8dca9919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -35,6 +35,7 @@ namespace Barotrauma if (newValue > _strength) { PendingAdditionStrength = Prefab.GrainBurst; + Duration = Prefab.Duration; } _strength = newValue; } @@ -60,6 +61,8 @@ namespace Barotrauma public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + public float Duration; + /// /// Which character gave this affliction /// @@ -75,6 +78,8 @@ namespace Barotrauma _strength = strength; Identifier = prefab.Identifier; + Duration = prefab.Duration; + foreach (var periodicEffect in prefab.PeriodicEffects) { PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); @@ -315,8 +320,7 @@ namespace Barotrauma public bool HasFlag(AbilityFlags flagType) { if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } - - return currentEffect.AfflictionAbilityFlags.Contains(flagType); + return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } private AfflictionPrefab.Effect GetViableEffect() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 4e277a25a..04d620a18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -235,7 +235,7 @@ namespace Barotrauma public Identifier[] BlockTransformation { get; private set; } public readonly Dictionary AfflictionStatValues = new Dictionary(); - public readonly HashSet AfflictionAbilityFlags = new HashSet(); + public AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); @@ -265,7 +265,7 @@ namespace Barotrauma break; case "abilityflag": var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); - AfflictionAbilityFlags.Add(flagType); + AfflictionAbilityFlags |= flagType; break; case "affliction": DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); @@ -354,6 +354,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// The affliction is automatically removed after this time. 0 = unlimited + /// + public readonly float Duration; + //how much karma changes when a player applies this affliction to someone (per strength of the affliction) public float KarmaChangeOnApplied; @@ -407,8 +412,10 @@ namespace Barotrauma !IsBuff && AfflictionType != "geneticmaterialbuff" && AfflictionType != "geneticmaterialdebuff"); - HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); - BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); + HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); + BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 5e4435b7d..bdd89998c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,12 +1,11 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using System.Globalization; -using Barotrauma.Abilities; namespace Barotrauma { @@ -148,7 +147,7 @@ namespace Barotrauma { max += Character.Info.Job.Prefab.VitalityModifier; } - max *= Character.StaticHealthMultiplier; + max *= Character.HumanPrefabHealthMultiplier; max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } @@ -700,6 +699,7 @@ namespace Barotrauma newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } existingAffliction.Strength = newStrength; + existingAffliction.Duration = existingAffliction.Prefab.Duration; if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); if (Vitality <= MinVitality) @@ -759,6 +759,15 @@ namespace Barotrauma if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } continue; } + if (affliction.Prefab.Duration > 0.0f) + { + affliction.Duration -= deltaTime; + if (affliction.Duration <= 0.0f) + { + afflictionsToRemove.Add(affliction); + continue; + } + } afflictionsToUpdate.Add(kvp); } foreach (KeyValuePair kvp in afflictionsToUpdate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index c2d64b348..7e8a539ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -112,12 +112,6 @@ namespace Barotrauma public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) { - npc.AddStaticHealthMultiplier(HealthMultiplier); - if (GameMain.NetworkMember != null) - { - npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer); - } - var humanAI = npc.AIController as HumanAIController; if (humanAI != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 608d7f16f..4638671f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -138,61 +138,53 @@ namespace Barotrauma ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) { return (T)ragdoll; } - string selectedFile = null; - - void tryFolderForSpecies(Identifier species, out string err) + Identifier ragdollSpecies = speciesName; + if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)) { - err = null; - string folder = GetFolder(species); + if (!prefab.VariantOf.IsEmpty) + { + ragdollSpecies = prefab.VariantOf; + } + string error = null; + string folder = GetFolder(ragdollSpecies); if (!Directory.Exists(folder)) { - err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - return; - } - - string[] files = Directory.GetFiles(folder); - if (files.None()) - { - err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(species); + error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); } else { - selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); - if (selectedFile == null) + string[] files = Directory.GetFiles(folder); + if (files.None()) { - err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; - selectedFile = GetDefaultFile(species); + error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); + } + else if (string.IsNullOrEmpty(fileName)) + { + // Files found, but none specified + selectedFile = GetDefaultFile(ragdollSpecies); + } + else + { + selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + if (selectedFile == null) + { + error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + selectedFile = GetDefaultFile(ragdollSpecies); + } } } + if (error != null) + { + DebugConsole.ThrowError(error); + } } - - tryFolderForSpecies(speciesName, out var error); - Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab) - ? prefab.VariantOf - : Identifier.Empty; - if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty) - { - tryFolderForSpecies(parentSpeciesName, out error); - } - - if (!error.IsNullOrEmpty()) - { - DebugConsole.ThrowError(error); - } - if (selectedFile == null) { throw new Exception("[RagdollParams] Selected file null!"); @@ -200,7 +192,7 @@ namespace Barotrauma DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); var characterPrefab = CharacterPrefab.Prefabs[speciesName]; T r = new T(); - if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) + if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies)) { if (!ragdolls.ContainsKey(r.Name)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index 7065cb683..426156bec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs index a1e03fcb6..782856d1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { abstract class AbilityConditionDataless : AbilityCondition { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 4c29a5b61..044d960a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 70ec6e1ae..5711c0ed5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 3f9090376..70b871963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,7 +1,4 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -19,10 +16,9 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - IEnumerable chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character); - - foreach (Character character in chosenCharacters) + foreach (Character character in Character.GetFriendlyCrew(Character)) { + if (!allowSelf && character == Character) { continue; } if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs index d6fc8b329..332c92e20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs index 4594c5e1e..693924271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index f8329aae2..94f7dfe02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 1f3795dea..656e5d751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index a12e2ce1e..b77f4332d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { @@ -30,7 +24,14 @@ namespace Barotrauma.Abilities private bool IsApplicable(AbilityObject abilityObject) { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition(abilityObject)); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition(abilityObject)) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 8682a47df..7cc1e24eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -49,7 +43,14 @@ namespace Barotrauma.Abilities private bool IsApplicable() { if (timesTriggered >= maxTriggerCount) { return false; } - return abilityConditions.All(c => c.MatchesCondition()); + foreach (var abilityCondition in abilityConditions) + { + if (!abilityCondition.MatchesCondition()) + { + return false; + } + } + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index bfa5b6869..0cf4b3420 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -1,8 +1,6 @@ -using System; +using Barotrauma.Abilities; +using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 4f853ba01..736f5053e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -2,12 +2,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -69,10 +67,10 @@ namespace Barotrauma .ToImmutableHashSet(); } - public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error, string? stackTrace = null) - => Result.Failure(error, stackTrace); + static Result fail(string error, Exception? exception = null) + => Result.Failure(new LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -95,11 +93,11 @@ namespace Barotrauma var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") - : Result.Success(file); + : Result.Success(file); } catch (Exception e) { - return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace()); + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e); } } @@ -125,5 +123,23 @@ namespace Barotrauma } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); + + public readonly struct LoadError + { + public readonly string Message; + public readonly Exception? Exception; + + public LoadError(string message, Exception? exception) + { + Message = message; + Exception = exception; + } + + public override string ToString() + => Message + + (Exception is { StackTrace: var stackTrace } + ? '\n' + stackTrace.CleanupStackTrace() + : string.Empty); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index da7f6be5a..bcb068006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public abstract class ContentPackage { - public static readonly Version MinimumHashCompatibleVersion = new Version(0, 17, 16, 0); + public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 3, 0); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -33,11 +33,11 @@ namespace Barotrauma public readonly Version GameVersion; public readonly string ModVersion; - public readonly Md5Hash Hash; + public Md5Hash Hash { get; private set; } public readonly DateTime? InstallTime; - public readonly ImmutableArray Files; - public readonly ImmutableArray<(string error, string? stackTrace)> Errors; + public ImmutableArray Files { get; private set; } + public ImmutableArray Errors { get; private set; } public async Task IsUpToDate() { @@ -55,7 +55,7 @@ namespace Barotrauma /// /// Does the content package include some content that needs to match between all players in multiplayer. /// - public readonly bool HasMultiplayerSyncedContent; + public bool HasMultiplayerSyncedContent { get; private set; } protected ContentPackage(XDocument doc, string path) { @@ -84,13 +84,13 @@ namespace Barotrauma .ToArray(); Files = fileResults - .OfType>() + .OfType>() .Select(f => f.Value) .ToImmutableArray(); Errors = fileResults - .OfType>() - .Select(f => (f.Error, f.StackTrace)) + .OfType>() + .Select(f => f.Error) .ToImmutableArray(); HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); @@ -127,18 +127,13 @@ namespace Barotrauma try { - if (doc.Root.GetAttributeBool("corepackage", false)) - { - return new CorePackage(doc, path); - } - else - { - return new RegularPackage(doc, path); - } + return doc.Root.GetAttributeBool("corepackage", false) + ? (ContentPackage)new CorePackage(doc, path) + : new RegularPackage(doc, path); } catch (Exception e) { - while (e.InnerException != null) { e = e.InnerException; } + e = e.GetInnermost(); DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); return null; } @@ -278,12 +273,42 @@ namespace Barotrauma Files.ForEach(f => f.UnloadFile()); } - public override int GetHashCode() + public void ReloadSubsAndItemAssemblies() { - byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4)); - return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3]; + XDocument doc = XMLExtensions.TryLoadXml(Path); + List newFileList = new List(); + XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); + + var fileResults = rootElement.Elements() + .Select(e => ContentFile.CreateFromXElement(this, e)) + .ToArray(); + + foreach (var result in fileResults) + { + switch (result) + { + case Success { Value: var file }: + if (file is BaseSubFile || file is ItemAssemblyFile) + { + newFileList.Add(file); + } + else + { + var existingFile = Files.FirstOrDefault(f => f.Path == file.Path); + newFileList.Add(existingFile ?? file); + } + break; + } + } + + UnloadFilesOfType(); + UnloadFilesOfType(); + Files = newFileList.ToImmutableArray(); + Hash = CalculateHash(); + LoadFilesOfType(); + LoadFilesOfType(); } - + public static bool PathAllowedAsLocalModFile(string path) { #if DEBUG @@ -305,21 +330,17 @@ namespace Barotrauma public void LogErrors() { - if (Errors.Any()) + if (!Errors.Any()) { - DebugConsole.AddWarning( - $"The following errors occurred while loading the content package\"{Name}\". The package might not work correctly.\n" + - string.Join('\n', Errors.Select(e => errorToStr(e.error, e.stackTrace)))); - static string errorToStr(string error, string? stackTrace) - { - string str = error; - if (stackTrace != null) - { - str += '\n' + stackTrace; - } - return str; - } + return; } + + DebugConsole.AddWarning( + $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + + string.Join('\n', Errors.Select(errorToStr))); + + static string errorToStr(ContentFile.LoadError error) + => error.ToString(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 68a695516..1d11503a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -430,9 +430,9 @@ namespace Barotrauma public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); - foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors) + foreach (ContentFile.LoadError error in VanillaCorePackage.Errors) { - DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace)); + DebugConsole.ThrowError(error.ToString()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index f5dba0b3d..b184da625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -27,7 +27,7 @@ namespace Barotrauma public string BaseUri => Element.BaseUri; - public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null."); + public XDocument? Document => Element.Document; public ContentXElement? FirstElement() => Elements().FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 01084658f..3722751b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1836,8 +1836,8 @@ namespace Barotrauma ThrowError($"No start item set identifier defined!"); return; } - AutoItemPlacer.StartItemSet = args[0].ToIdentifier(); - NewMessage($"Start item set changed to \"{AutoItemPlacer.StartItemSet}\""); + AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier(); + NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\""); }, isCheat: false)); //"dummy commands" that only exist so that the server can give clients permissions to use them diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 7e634941e..8ef199011 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -131,21 +131,22 @@ namespace Barotrauma MaxAttachableCount, } + [Flags] public enum AbilityFlags { - None, - MustWalk, - ImmuneToPressure, - IgnoredByEnemyAI, - MoveNormallyWhileDragging, - CanTinker, - CanTinkerFabricatorsAndDeconstructors, - TinkeringPowersDevices, - GainSkillPastMaximum, - RetainExperienceForNewCharacter, - AllowSecondOrderedTarget, - PowerfulCPR, - AlwaysStayConscious, + None = 0, + MustWalk = 0x1, + ImmuneToPressure = 0x2, + IgnoredByEnemyAI = 0x4, + MoveNormallyWhileDragging = 0x8, + CanTinker = 0x10, + CanTinkerFabricatorsAndDeconstructors = 0x20, + TinkeringPowersDevices = 0x40, + GainSkillPastMaximum = 0x80, + RetainExperienceForNewCharacter = 0x100, + AllowSecondOrderedTarget = 0x200, + PowerfulCPR = 0x400, + AlwaysStayConscious = 0x800, } [Flags] @@ -156,9 +157,24 @@ namespace Barotrauma Both = Bot | Player } + public enum StartingBalanceAmount + { + Low, + Medium, + High, + } + + public enum GameDifficulty + { + Easy, + Medium, + Hard, + Hellish + } + public enum NumberType { Int, Float } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index da7661209..d4d95ebbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -80,7 +80,7 @@ namespace Barotrauma } if (campaign is MultiPlayerCampaign mpCampaign) { - mpCampaign.LastUpdateID++; + mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); } if (prefab != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index c871e45ab..c3030d6df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -64,8 +64,6 @@ namespace Barotrauma campaign.GetWallet(client).Give(Amount); } } - - ((MultiPlayerCampaign)campaign).LastUpdateID++; #else campaign.Wallet.Give(Amount); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index a05b0a183..8d17092d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -49,6 +49,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier SpawnPointTag { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] + public bool RequireSpawnPointTag { get; set; } + private readonly HashSet targetModuleTags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] @@ -79,7 +82,7 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase)); + ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; } public override bool IsFinished(ref string goTo) @@ -110,22 +113,40 @@ namespace Barotrauma if (humanPrefab != null) { ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + if (spawnPos != null) { - if (newCharacter == null) { return; } - newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); - if (LootingIsStealing) + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.AllItems) + if (newCharacter == null) { return; } + newCharacter.HumanPrefab = humanPrefab; + newCharacter.TeamID = CharacterTeamType.FriendlyNPC; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + if (LootingIsStealing) { - item.SpawnedInCurrentOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.AllItems) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!TargetTag.IsEmpty && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } + } + } + else if (!SpeciesName.IsEmpty) + { + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter => + { if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); @@ -134,20 +155,9 @@ namespace Barotrauma }); } } - else if (!SpeciesName.IsEmpty) - { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => - { - if (!TargetTag.IsEmpty && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); - } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab)) + if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -178,7 +188,11 @@ namespace Barotrauma if (spawnInventory == null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned); + } } else { @@ -244,10 +258,10 @@ namespace Barotrauma SpawnType? spawnPointType = null; if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; } - return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); + return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } - public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false) + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { List potentialSpawnPoints = spawnLocation switch { @@ -274,18 +288,24 @@ namespace Barotrauma if (spawnpointTags != null && spawnpointTags.Any()) { var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag))) - .Where(wp => wp.ConnectedDoor == null && !wp.isObstructed); + .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - if (spawnPoints.Any()) + if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.Count == 0) + if (potentialSpawnPoints.None()) { - DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) + { + DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White); + } + else + { + DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})"); + } return null; } @@ -307,7 +327,7 @@ namespace Barotrauma validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); } - if (!validSpawnPoints.Any()) + if (validSpawnPoints.None()) { DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})"); return potentialSpawnPoints.GetRandomUnsynced(); @@ -320,7 +340,7 @@ namespace Barotrauma } //if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags - if (spawnpointTags == null || !spawnpointTags.Any()) + if (spawnpointTags == null || spawnpointTags.None()) { var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any()); if (spawnPoints.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index c295eaa49..94b7d0c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -260,9 +260,15 @@ namespace Barotrauma throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } + float extraDifficulty = 0; + if (GameMain.GameSession.Campaign?.Settings != null) + { + extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty; + } + float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100); var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => - level.Difficulty >= s.MinLevelDifficulty && - level.Difficulty <= s.MaxLevelDifficulty).ToArray(); + modifiedDifficulty >= s.MinLevelDifficulty && + modifiedDifficulty <= s.MaxLevelDifficulty).ToArray(); if (suitableSettings.Length == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 66bd8857f..7ca86a794 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -437,7 +437,7 @@ namespace Barotrauma { minDistance = 5000; } - else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) + else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation)) { minDistance = 3000; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 5d15cbed9..7764c444e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -10,7 +10,7 @@ namespace Barotrauma { public static bool OutputDebugInfo = false; - public static void SpawnItems() + public static void SpawnItems(Identifier? startItemSet = null) { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } @@ -23,7 +23,7 @@ namespace Barotrauma var sub = Submarine.MainSubs[i]; if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; } //1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs) - SpawnStartItems(sub); + SpawnStartItems(sub, startItemSet); //2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc) var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID); CreateAndPlace(subs); @@ -62,17 +62,23 @@ namespace Barotrauma CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); } - public static Identifier StartItemSet = new Identifier("normal"); + public static Identifier DefaultStartItemSet = new Identifier("normal"); /// /// Spawns the items defined in the start item set in the specified sub. /// - private static void SpawnStartItems(Submarine sub) + private static void SpawnStartItems(Submarine sub, Identifier? startItemSet) { - if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet)) + Identifier setIdentifier = startItemSet ?? DefaultStartItemSet; + if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet)) { - DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{StartItemSet}\"!"); - return; + DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!"); + if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet)) + { + DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!"); + return; + } + itemSet = defaultSet; } WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); ISpatialEntity initialSpawnPos; @@ -164,7 +170,7 @@ namespace Barotrauma var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier); foreach (ItemPrefab ip in itemPrefabs) { - if (!ip.PreferredContainers.Any()) { continue; } + if (ip.PreferredContainers.None()) { continue; } if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip))) { prefabsItemsCanSpawnIn.Add(ip); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 04ca98e30..717951a59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -10,63 +10,6 @@ using System.Xml.Linq; namespace Barotrauma { - internal struct CampaignSettings - { - public static CampaignSettings Empty => new CampaignSettings(); - - // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down - public static CampaignSettings Unsure => Empty; - public bool RadiationEnabled { get; set; } - - public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); - - private int maxMissionCount; - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); } - } - - public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; - public const int MinMissionCountLimit = 1; - - public CampaignSettings(IReadMessage inc) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = inc.ReadBoolean(); - MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit); - } - - public CampaignSettings(XElement element) - { - maxMissionCount = DefaultMaxMissionCount; - RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); - MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount); - } - - public void Serialize(IWriteMessage msg) - { - msg.Write(RadiationEnabled); - msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit); - } - - public int GetAddedMissionCount() - { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; - } - - public XElement Save() - { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount)); - } - } - abstract partial class CampaignMode : GameMode { [NetworkSerialize] @@ -149,9 +92,8 @@ namespace Barotrauma //key = dialog flag, double = Timing.TotalTime when the line was last said private readonly Dictionary dialogLastSpoken = new Dictionary(); - public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs; - public SubmarineInfo PendingSubmarineSwitch; + public bool TransferItemsOnSubSwitch { get; set; } protected Map map; public Map Map @@ -189,12 +131,16 @@ namespace Barotrauma protected set; } - protected CampaignMode(GameModePreset preset) + public virtual bool PurchasedHullRepairs { get; set; } + public virtual bool PurchasedLostShuttles { get; set; } + public virtual bool PurchasedItemRepairs { get; set; } + + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { Bank = new Wallet(Option.None()) { - Balance = InitialMoney + Balance = settings.InitialMoney }; CargoManager = new CargoManager(this); @@ -596,6 +542,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost == null) { Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -729,7 +676,6 @@ namespace Barotrauma } } - public void EndCampaign() { foreach (Character c in Character.CharacterList) @@ -741,7 +687,7 @@ namespace Barotrauma } foreach (LocationConnection connection in Map.Connections) { - connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f); + connection.Difficulty = connection.Biome.MaxDifficulty; connection.LevelData = new LevelData(connection) { IsBeaconActive = false @@ -750,6 +696,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { + location.LevelData = new LevelData(location, location.Biome.MaxDifficulty); location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -873,7 +820,7 @@ namespace Barotrauma const float MaxDist = 3000.0f; const float MinDist = 2500.0f; - if (!Level.IsLoadedOutpost) { return; } + if (!Level.IsLoadedFriendlyOutpost) { return; } Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); @@ -1058,7 +1005,10 @@ namespace Barotrauma public SubmarineInfo SwitchSubs() { - TransferItemsBetweenSubs(); + if (TransferItemsOnSubSwitch) + { + TransferItemsBetweenSubs(); + } RefreshOwnedSubmarines(); PendingSubmarineSwitch = null; return GameMain.GameSession.SubmarineInfo; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs new file mode 100644 index 000000000..430bb063a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal static class CampaignModePresets + { + public static readonly ImmutableArray List; + public static readonly ImmutableDictionary Definitions; + + private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml"); + + static CampaignModePresets() + { + if (!File.Exists(fileListPath) || !(XMLExtensions.TryLoadXml(fileListPath)?.Root is { } docRoot)) + { + List = ImmutableArray.Empty; + return; + } + + List list = new List(); + Dictionary definitions = new Dictionary(); + + foreach (XElement element in docRoot.Elements()) + { + Identifier name = element.NameAsIdentifier(); + + if (name == CampaignSettings.LowerCaseSaveElementName) + { + list.Add(new CampaignSettings(element)); + } + else if (name == nameof(CampaignSettingDefinitions)) + { + foreach (XElement subElement in element.Elements()) + { + definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); + } + } + } + + List = list.ToImmutableArray(); + Definitions = definitions.ToImmutableDictionary(); + } + } + + internal readonly struct CampaignSettingDefinitions + { + // Definitely not the best way to do this + private readonly ImmutableDictionary> values; + + public CampaignSettingDefinitions(XElement element) + { + var definitions = new Dictionary>(); + foreach (XAttribute attribute in element.Attributes()) + { + Identifier name = attribute.NameAsIdentifier(); + if (attribute.Value.Contains('.')) + { + definitions.Add(name, element.GetAttributeFloat(name.Value, 0)); + } + else + { + definitions.Add(name, element.GetAttributeInt(name.Value, 0)); + } + } + + values = definitions.ToImmutableDictionary(); + } + + public float GetFloat(Identifier identifier) + { + return values.TryGetValue(identifier, out Either value) && value.TryGet(out float range) ? range : 0.0f; + } + + public int GetInt(Identifier identifier) + { + return values.TryGetValue(identifier, out Either value) && value.TryGet(out int integer) ? integer : 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs new file mode 100644 index 000000000..1d96fa2fa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -0,0 +1,114 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class CampaignSettings : INetSerializableStruct, ISerializableEntity + { + public static CampaignSettings Empty => new CampaignSettings(element: null); + + public string Name => "CampaignSettings"; + + public const string LowerCaseSaveElementName = "campaignsettings"; + + [Serialize("", IsPropertySaveable.Yes)] + public string PresetName { get; set; } = string.Empty; + + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] + public bool RadiationEnabled { get; set; } + + private int maxMissionCount; + + [Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)] + public int MaxMissionCount + { + get => maxMissionCount; + set => maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); + } + + public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); + + [Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public StartingBalanceAmount StartingBalanceAmount { get; set; } + + [Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public GameDifficulty Difficulty { get; set; } + + [Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize] + public Identifier StartItemSet { get; set; } + + public int InitialMoney + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + { + return definition.GetInt(StartingBalanceAmount.ToIdentifier()); + } + return 8000; + + } + } + + public float ExtraEventManagerDifficulty + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 0; + } + } + + public float LevelDifficultyMultiplier + { + get + { + if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition)) + { + return definition.GetFloat(Difficulty.ToIdentifier()); + } + return 1.0f; + } + } + + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; + + public Dictionary SerializableProperties { get; private set; } + + // required for INetSerializableStruct + public CampaignSettings() + { + SerializableProperties = SerializableProperty.GetProperties(this); + } + + public CampaignSettings(XElement? element = null) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public XElement Save() + { + XElement saveElement = new XElement(LowerCaseSaveElementName); + SerializableProperty.SerializeProperties(this, saveElement, saveIfDefault: true); + return saveElement; + } + + private static int GetAddedMissionCount() + { + int count = 0; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); + } + return count; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index d8871d2bc..789a31c8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -29,7 +29,11 @@ namespace Barotrauma : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - missions.Add(Mission.LoadRandom(locations, seed, false, missionType)); + var mission = Mission.LoadRandom(locations, seed, false, missionType); + if (mission != null) + { + missions.Add(mission); + } } protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 181c73232..18a0f6fcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,19 +12,60 @@ namespace Barotrauma { public const int MinimumInitialMoney = 500; - private UInt16 lastUpdateID; - public UInt16 LastUpdateID + [Flags] + public enum NetFlags : UInt16 { - get - { -#if SERVER - if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; } -#endif - return lastUpdateID; - } - set { lastUpdateID = value; } + Misc = 0x1, + MapAndMissions = 0x2, + UpgradeManager = 0x4, + SubList = 0x8, + ItemsInBuyCrate = 0x10, + ItemsInSellFromSubCrate = 0x20, + PurchasedItems = 0x80, + SoldItems = 0x100, + Reputation = 0x200, + CharacterInfo = 0x800 } + private readonly Dictionary lastUpdateID; + + public UInt16 GetLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return 0; } + return lastUpdateID[flag]; + } + public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id) + { + if (!ValidateFlag(flag)) { return; } + lastUpdateID[flag] = id; + } + + public void IncrementLastUpdateIdForFlag(NetFlags flag) + { + if (!ValidateFlag(flag)) { return; } + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + public void IncrementAllLastUpdateIds() + { + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { + if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; } + lastUpdateID[flag]++; + } + } + + private bool ValidateFlag(NetFlags flag) + { + if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } +#if DEBUG + throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag."); +#else + return false; +#endif + } + + private UInt16 lastSaveID; public UInt16 LastSaveID { @@ -35,11 +76,11 @@ namespace Barotrauma #endif return lastSaveID; } - set + set { #if SERVER //trigger a campaign update to notify the clients of the changed save ID - lastUpdateID++; + IncrementLastUpdateIdForFlag(NetFlags.Misc); #endif lastSaveID = value; } @@ -52,23 +93,33 @@ namespace Barotrauma get; set; } - private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign) + private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings) { currentCampaignID++; + lastUpdateID = new Dictionary(); + foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags))) + { +#if SERVER + //server starts from a higher ID to ensure we send the initial state + lastUpdateID[flag] = 1; +#else + lastUpdateID[flag] = 0; +#endif + } CampaignID = currentCampaignID; CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); InitCampaignData(); } - public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings) + public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings); //only the server generates the map, the clients load it from a save file if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - campaign.map = new Map(campaign, mapSeed, settings); campaign.Settings = settings; + campaign.map = new Map(campaign, mapSeed); } campaign.InitProjSpecific(); return campaign; @@ -76,7 +127,7 @@ namespace Barotrauma public static MultiPlayerCampaign LoadNew(XElement element) { - MultiPlayerCampaign campaign = new MultiPlayerCampaign(); + MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty); campaign.Load(element); campaign.InitProjSpecific(); campaign.IsFirstRound = false; @@ -124,18 +175,17 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { - case "campaignsettings": + case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); #if CLIENT - GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount; - GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled; + GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings; #endif break; case "map": if (map == null) { //map not created yet, loading this campaign for the first time - map = Map.Load(this, subElement, Settings); + map = Map.Load(this, subElement); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 42a9dc8d0..fe1f4fad2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -72,7 +72,7 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -83,7 +83,7 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -207,7 +207,7 @@ namespace Barotrauma } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { - var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.Deduct(selectedSub.Price); @@ -218,7 +218,7 @@ namespace Barotrauma #if CLIENT else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { - var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); + var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings); if (selectedSub != null) { campaign.Bank.TryDeduct(selectedSub.Price); @@ -245,25 +245,15 @@ namespace Barotrauma } } - private void CreateDummyLocations(LocationType? forceLocationType = null) + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) { - dummyLocations = new Location[2]; - - string seed = ""; - if (GameMain.GameSession != null && GameMain.GameSession.Level != null) - { - seed = GameMain.GameSession.Level.Seed; - } - else if (GameMain.NetLobbyScreen != null) - { - seed = GameMain.NetLobbyScreen.LevelSeed; - } - + var dummyLocations = new Location[2]; MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); } + return dummyLocations; } public void LoadPreviousSave() @@ -275,7 +265,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -299,6 +289,7 @@ namespace Barotrauma } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; + Campaign!.TransferItemsOnSubSwitch = transferItems; } public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) @@ -309,6 +300,9 @@ namespace Barotrauma { GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); +#if SERVER + (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); +#endif } } @@ -345,7 +339,7 @@ namespace Barotrauma !missionPrefab.AllowedConnectionTypes.Any()) { LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); - CreateDummyLocations(locationType); + dummyLocations = CreateDummyLocations(levelSeed, locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } @@ -430,7 +424,7 @@ namespace Barotrauma Level? level = null; if (levelData != null) { - level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost); + level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost); } InitializeLevel(level); @@ -603,7 +597,7 @@ namespace Barotrauma Level.SpawnCorpses(); Level.PrepareBeaconStation(); } - AutoItemPlacer.SpawnItems(); + AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet); } if (GameMode is MultiPlayerCampaign mpCampaign) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index cb24eb337..a660bbbf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -360,9 +360,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item)) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List { InvSlotType.Any }, true, ignoreCondition)) { free = false; @@ -382,9 +379,6 @@ namespace Barotrauma { if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty()) { -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } -#endif bool removeFromOtherSlots = item.ParentInventory != this; if (placedInSlot == -1 && inWrongSlot) { @@ -454,9 +448,6 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } -#if CLIENT - if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; } -#endif //there's already an item in the slot if (slots[index].Any()) { @@ -480,9 +471,6 @@ namespace Barotrauma foreach (InvSlotType allowedSlot in pickable.AllowedSlots) { if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } - #if CLIENT - if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } - #endif for (int i = 0; i < capacity; i++) { if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index d8543526e..059832004 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -144,9 +144,16 @@ namespace Barotrauma.Items.Components } } - public bool Combine(GeneticMaterial otherGeneticMaterial, Character user) + public enum CombineResult { - if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } + None, + Refined, + Combined + } + + public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user) + { + if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; } float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; @@ -158,7 +165,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return true; + return CombineResult.Refined; } else { @@ -171,7 +178,7 @@ namespace Barotrauma.Items.Components { MakeTainted(); } - return false; + return CombineResult.Combined; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 7a624ac27..c615ae481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -697,14 +697,17 @@ namespace Barotrauma.Items.Components Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos; float dist = fromCharacterToLeak.Length(); float reach = AIObjectiveFixLeak.CalculateReach(this, character); - - if (dist > reach * 3) + if (dist > reach * 2) { // Too far away -> consider this done and hope the AI is smart enough to move closer Reset(); return true; } character.AIController.SteeringManager.Reset(); + if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } if (!character.AnimController.InWater) { // TODO: use the collider size? @@ -714,34 +717,25 @@ namespace Barotrauma.Items.Components humanAnim.Crouching = true; } } - if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) + if (!character.IsClimbing) { - // Steer closer - if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) { - // Swimming inside the sub - if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished)) + // Steer closer + Vector2 dir = Vector2.Normalize(fromCharacterToLeak); + if (!character.InWater) { - Vector2 dir = Vector2.Normalize(fromCharacterToLeak); - character.AIController.SteeringManager.SteeringManual(deltaTime, dir); - } - else - { - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + dir.Y = 0; } + character.AIController.SteeringManager.SteeringManual(deltaTime, dir); } - else + else if (dist < reach * 0.25f && !character.IsClimbing) { - // Swimming outside the sub - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + // Too close -> steer away + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } } - else if (dist < reach * 0.25f) - { - // Too close -> steer away - character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); - } - if (dist <= reach) + if (dist <= reach || character.IsClimbing) { // In range character.CursorPosition = leak.WorldPosition; @@ -815,7 +809,7 @@ namespace Barotrauma.Items.Components } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && - (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); + (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f); if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 903905ec6..e179980be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -187,7 +187,12 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } - private SlotRestrictions[] slotRestrictions; + private readonly ImmutableArray slotRestrictions; + + readonly List targets = new List(); + + private Vector2 prevContainedItemPositions; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { @@ -237,10 +242,11 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - slotRestrictions = new SlotRestrictions[totalCapacity]; + + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { - slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems)); } int subContainerIndex = capacity; @@ -268,11 +274,13 @@ namespace Barotrauma.Items.Components for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) { - slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems); + newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems)); } subContainerIndex += subCapacity; } capacity = totalCapacity; + slotRestrictions = newSlotRestrictions.ToImmutableArray(); + System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length); InitProjSpecific(element); } @@ -365,18 +373,21 @@ namespace Barotrauma.Items.Components return false; } - readonly List targets = new List(); - public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) { SpawnAlwaysContainedItems(); + alwaysContainedItemsSpawned = true; } if (item.ParentInventory is CharacterInventory ownerInventory) { - item.SetContainedItemPositions(); + if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f) + { + SetContainedItemPositions(); + prevContainedItemPositions = item.Position; + } if (AutoInject) { @@ -397,7 +408,7 @@ namespace Barotrauma.Items.Components item.body.Enabled && item.body.FarseerBody.Awake) { - item.SetContainedItemPositions(); + SetContainedItemPositions(); } else if (activeContainedItems.Count == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 4bf9d88fe..adda6abc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -147,7 +147,7 @@ namespace Barotrauma.Items.Components CancelUsing(user); user = null; } - if (!IsToggle) { IsActive = false; } + if (!IsToggle || item.Connections == null) { IsActive = false; } return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index c09526555..c4f04b034 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -231,28 +231,40 @@ namespace Barotrauma.Items.Components if (targetItem == otherItem) { continue; } if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier)) { - user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); - foreach (Character character in Character.GetFriendlyCrew(user)) - { - character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); - } - var geneticMaterial1 = targetItem.GetComponent(); var geneticMaterial2 = otherItem.GetComponent(); if (geneticMaterial1 != null && geneticMaterial2 != null) { - if (geneticMaterial1.Combine(geneticMaterial2, user)) + var result = geneticMaterial1.Combine(geneticMaterial2, user); + if (result == GeneticMaterial.CombineResult.Refined) { inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); Entity.Spawner.AddItemToRemoveQueue(otherItem); } + if (result != GeneticMaterial.CombineResult.None) + { + OnCombinedOrRefined(); + } allowRemove = false; return; } - inputContainer.Inventory.RemoveItem(otherItem); - OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddItemToRemoveQueue(otherItem); + else + { + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); + OnCombinedOrRefined(); + } + } + } + + void OnCombinedOrRefined() + { + user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 7e3e1da2e..a04b1eb25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -400,8 +400,8 @@ namespace Barotrauma.Items.Components private void IncreaseSkillLevel(Character user, float deltaTime) { if (user?.Info == null) { return; } - // Do not increase the helm skill when "steering" the sub in an outpost level - if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { return; } + // Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck) + if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index ae166ebf5..24a780d6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components @@ -81,6 +80,8 @@ namespace Barotrauma.Items.Components private ItemContainer? container; private float growthTickTimer; + private List? lightComponents; + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; @@ -107,10 +108,14 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); IsActive = true; #if CLIENT - lightComponent = item.GetComponent(); - if (lightComponent != null) + var lights = item.GetComponents(); + if (lights.Any()) { - lightComponent.Light.Enabled = false; + lightComponents = lights.ToList(); + foreach (var light in lightComponents) + { + light.Light.Enabled = false; + } } #endif container = item.GetComponent(); @@ -227,12 +232,17 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); #if CLIENT - if (lightComponent != null) + if (lightComponents != null && lightComponents.Count > 0) { bool hasSeed = false; - foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; } - - lightComponent.Light.Enabled = hasSeed; + foreach (Growable? seed in GrowableSeeds) + { + hasSeed |= seed != null; + } + foreach (var light in lightComponents) + { + light.Light.Enabled = hasSeed; + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index edb4d6242..7f8fc6789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -602,7 +602,7 @@ namespace Barotrauma.Items.Components private bool ShouldDeteriorate() { - if (Level.IsLoadedOutpost) { return false; } + if (Level.IsLoadedFriendlyOutpost) { return false; } if (LastActiveTime > Timing.TotalTime) { return true; } foreach (ItemComponent ic in item.Components) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6cbb632a9..e278206b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -13,12 +13,13 @@ namespace Barotrauma.Items.Components private float updateTimer; + [Flags] public enum TargetType { - Any, - Human, - Monster, - Wall + Human = 1, + Monster = 2, + Wall = 4, + Any = Human | Monster | Wall, } [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -179,6 +180,11 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); } + if (MotionDetected) + { + ApplyStatusEffects(ActionType.OnUse, deltaTime); + } + updateTimer -= deltaTime; if (updateTimer > 0.0f) { return; } @@ -199,8 +205,7 @@ namespace Barotrauma.Items.Components float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - if (item.CurrentHull == null && item.Submarine != null && - (Target == TargetType.Wall || Target == TargetType.Any)) + if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall)) { if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) { @@ -248,7 +253,7 @@ namespace Barotrauma.Items.Components } } - if (Target != TargetType.Wall) + if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Monster)) { foreach (Character c in Character.CharacterList) { @@ -258,14 +263,13 @@ namespace Barotrauma.Items.Components //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - switch (Target) + if (c.IsHuman) { - case TargetType.Human: - if (!c.IsHuman) { continue; } - break; - case TargetType.Monster: - if (c.IsHuman || c.IsPet) { continue; } - break; + if (!Target.HasFlag(TargetType.Human)) { continue; } + } + else if (!c.IsPet) + { + if (!Target.HasFlag(TargetType.Monster)) { continue; } } //do a rough check based on the position of the character's collider first diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 5854fccb9..e60825beb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma.Items.Components @@ -12,6 +13,46 @@ namespace Barotrauma.Items.Components { [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)] + public bool DistanceBasedForce { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)] + public bool ForceFluctuation { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)] + private float ForceFluctuationStrength + { + get + { + return forceFluctuationStrength; + } + set + { + forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f); + } + } + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)] + private float ForceFluctuationFrequency + { + get + { + return forceFluctuationFrequency; + } + set + { + forceFluctuationFrequency = Math.Max(value, 0.01f); + } + } + [Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)] + private float ForceFluctuationInterval + { + get + { + return forceFluctuationInterval; + } + set + { + forceFluctuationInterval = Math.Max(value, 0.01f); + } + } public PhysicsBody PhysicsBody { get; private set; } private float Radius { get; set; } @@ -38,11 +79,6 @@ namespace Barotrauma.Items.Components private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); private readonly bool triggerOnce; - private readonly bool distanceBasedForce; - private readonly bool forceFluctuation; - private readonly float forceFluctuationStrength; - private readonly float forceFluctuationFrequency; - private readonly float forceFluctuationInterval; private readonly List statusEffectTargets = new List(); /// /// Effects applied to entities inside the trigger @@ -53,6 +89,10 @@ namespace Barotrauma.Items.Components /// private readonly List attacks = new List(); + private float forceFluctuationStrength; + private float forceFluctuationFrequency; + private float forceFluctuationInterval; + public TriggerComponent(Item item, ContentXElement element) : base(item, element) { string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); @@ -61,15 +101,6 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); } triggerOnce = element.GetAttributeBool("triggeronce", false); - distanceBasedForce = element.GetAttributeBool("distancebasedforce", false); - forceFluctuation = element.GetAttributeBool("forcefluctuation", false); - forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f); - forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f); - forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f); - forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f); - forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f); - forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); - string parentDebugName = $"TriggerComponent in {item.Name}"; foreach (var subElement in element.Elements()) { @@ -153,14 +184,14 @@ namespace Barotrauma.Items.Components TriggerActive = triggerers.Any(); - if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { ForceFluctuationTimer += deltaTime; - if (ForceFluctuationTimer >= forceFluctuationInterval) + if (ForceFluctuationTimer >= ForceFluctuationInterval) { - float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel); + float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel); float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v); - CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount); + CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount); ForceFluctuationTimer = 0.0f; GameMain.NetworkMember?.CreateEntityEvent(this); } @@ -179,7 +210,7 @@ namespace Barotrauma.Items.Components LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); } - if (Force < 0.01f) + if (Math.Abs(Force) < 0.01f) { // Just ignore very minimal forces continue; @@ -205,7 +236,7 @@ namespace Barotrauma.Items.Components { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } - float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; + float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); if (force.LengthSquared() < 0.01f) { return; } @@ -227,5 +258,43 @@ namespace Barotrauma.Items.Components PhysicsBody.Submarine = item.Submarine; } } + + public override void ReceiveSignal(Signal signal, Connection connection) + { + base.ReceiveSignal(signal, connection); + switch (connection.Name) + { + case "set_force": + if (!FloatTryParse(signal, out float force)) { break; } + Force = force; + break; + case "set_distancebasedforce": + if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; } + DistanceBasedForce = distanceBasedForce; + break; + case "set_forcefluctuation": + if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; } + ForceFluctuation = forceFluctuation; + break; + case "set_forcefluctuationstrength": + if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; } + ForceFluctuationStrength = forceFluctuationStrength; + break; + case "set_forcefluctuationfrequency": + if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; } + ForceFluctuationFrequency = forceFluctuationFrequency; + break; + case "set_forcefluctuationinterval": + if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; } + ForceFluctuationInterval = forceFluctuationInterval; + break; + } + + static bool FloatTryParse(Signal signal, out float value) + { + return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value); + } + } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 32750626f..cf6c5cc42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -4,9 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics.Dynamics; @@ -20,8 +18,6 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; private Vector2 transformedBarrelPos; - - private LightComponent lightComponent; private float rotation, targetRotation; @@ -71,6 +67,8 @@ namespace Barotrauma.Items.Components public Character ActiveUser; private float resetActiveUserTimer; + private List lightComponents; + public float Rotation { get { return rotation; } @@ -168,10 +166,13 @@ namespace Barotrauma.Items.Components rotation = (minRotation + maxRotation) / 2; #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = rotation; - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + light.Rotation = rotation; + light.Light.Rotation = -rotation; + } } #endif } @@ -331,27 +332,39 @@ namespace Barotrauma.Items.Components if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } targetRotation = rotation; - FindLightComponent(); UpdateTransformedBarrelPos(); } - private void FindLightComponent() + private void FindLightComponents() { + if (lightComponents != null) + { + // Can't run again, because of reparenting. + return; + } foreach (LightComponent lc in item.GetComponents()) { + // Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it. if (lc?.Parent == this) { - lightComponent = lc; - break; + if (lightComponents == null) + { + lightComponents = new List(); + } + lightComponents.Add(lc); } } #if CLIENT - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Parent = null; - lightComponent.Rotation = Rotation - item.RotationRad; - lightComponent.Light.Rotation = -rotation; + foreach (var light in lightComponents) + { + // We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active) + light.Parent = null; + light.Rotation = Rotation - item.RotationRad; + light.Light.Rotation = -rotation; + } } #endif } @@ -428,7 +441,7 @@ namespace Barotrauma.Items.Components if (MathUtils.NearlyEqual(minRotation, maxRotation)) { - UpdateLightComponent(); + UpdateLightComponents(); return; } @@ -452,7 +465,7 @@ namespace Barotrauma.Items.Components } // Do not increase the weapons skill when operating a turret in an outpost level - if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost)) + if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost)) { user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); @@ -509,14 +522,17 @@ namespace Barotrauma.Items.Components aiFindTargetTimer -= deltaTime; } - UpdateLightComponent(); + UpdateLightComponents(); } - private void UpdateLightComponent() + private void UpdateLightComponents() { - if (lightComponent != null) + if (lightComponents != null) { - lightComponent.Rotation = Rotation - item.RotationRad; + foreach (var light in lightComponents) + { + light.Rotation = Rotation - item.RotationRad; + } } } @@ -1601,21 +1617,24 @@ namespace Barotrauma.Items.Components } break; case "toggle_light": - if (lightComponent != null && signal.value != "0") + if (lightComponents != null && signal.value != "0") { - lightComponent.IsOn = !lightComponent.IsOn; - UpdateLightComponent(); + foreach (var light in lightComponents) + { + light.IsOn = !light.IsOn; + } + UpdateLightComponents(); } break; case "set_light": - if (lightComponent != null) + if (lightComponents != null) { bool shouldBeOn = signal.value != "0"; - if (shouldBeOn != lightComponent.IsOn) + foreach (var light in lightComponents) { - lightComponent.IsOn = shouldBeOn; - UpdateLightComponent(); + light.IsOn = shouldBeOn; } + UpdateLightComponents(); } break; } @@ -1633,7 +1652,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - FindLightComponent(); + FindLightComponents(); targetRotation = rotation; if (!loadedBaseRotation.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index b783a1126..ee23c7f90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; -using System.Collections.Immutable; using Barotrauma.Abilities; namespace Barotrauma @@ -82,7 +81,20 @@ namespace Barotrauma public string Sound { get; private set; } public Point? SheetIndex { get; private set; } - public LightComponent LightComponent { get; set; } + public LightComponent LightComponent => LightComponents?.FirstOrDefault(); + + public List LightComponents + { + get + { + if (_lightComponents == null) + { + _lightComponents = new List(); + } + return _lightComponents; + } + } + private List _lightComponents; public int Variant { get; set; } @@ -338,11 +350,14 @@ namespace Barotrauma.Items.Components foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } - wearableSprites[i].LightComponent = new LightComponent(item, lightElement) + wearableSprites[i].LightComponents.Add(new LightComponent(item, lightElement) { Parent = this - }; - item.AddComponent(wearableSprites[i].LightComponent); + }); + foreach (var light in wearableSprites[i].LightComponents) + { + item.AddComponent(light); + } } i++; @@ -413,7 +428,10 @@ namespace Barotrauma.Items.Components IsActive = true; if (wearableSprite.LightComponent != null) { - wearableSprite.LightComponent.ParentBody = equipLimb.body; + foreach (var light in wearableSprite.LightComponents) + { + light.ParentBody = equipLimb.body; + } } limb[i] = equipLimb; @@ -467,7 +485,10 @@ namespace Barotrauma.Items.Components if (wearableSprites[i].LightComponent != null) { - wearableSprites[i].LightComponent.ParentBody = null; + foreach (var light in wearableSprites[i].LightComponents) + { + light.ParentBody = null; + } } equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]); @@ -494,7 +515,6 @@ namespace Barotrauma.Items.Components } item.SetTransform(picker.SimPosition, 0.0f); - item.SetContainedItemPositions(); item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8f7b0e4fd..c7c0b2425 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -24,13 +24,18 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { public static List ItemList = new List(); + + private static readonly HashSet dangerousItems = new HashSet(); + + public static IReadOnlyCollection DangerousItems { get { return dangerousItems; } } + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; private readonly HashSet tags; - private bool isWire, isLogic; + private readonly bool isWire, isLogic; private Hull currentHull; public Hull CurrentHull @@ -279,7 +284,10 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { SetContainedItemPositions(); - GetComponent()?.SetLightSourceTransform(); + foreach (var light in GetComponents()) + { + light.SetLightSourceTransform(); + } } #endif } @@ -1001,6 +1009,10 @@ namespace Barotrauma InsertToList(); ItemList.Add(this); + if (Prefab.IsDangerous) + { + dangerousItems.Add(this); + } DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1092,17 +1104,16 @@ namespace Barotrauma component.OnActiveStateChanged += (bool isActive) => { - bool hasSounds = false; + bool needsSoundUpdate = false; #if CLIENT - hasSounds = component.HasSounds; + needsSoundUpdate = component.NeedsSoundUpdate(); #endif //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it, - //nor status effects, sounds or conditionals that would need to run + //nor sounds or conditionals that would need to run if (!isActive && !component.UpdateWhenInactive && - !hasSounds && + !needsSoundUpdate && component.Parent == null && - (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) && - (component.statusEffectLists == null || !component.statusEffectLists.Any())) + (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any())) { if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); } } @@ -1499,6 +1510,11 @@ namespace Barotrauma public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null) { + if (effect.intervalTimer > 0.0f) + { + effect.intervalTimer -= deltaTime; + return; + } if (!isNetworkEvent && checkCondition) { if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; } @@ -1630,6 +1646,7 @@ namespace Barotrauma foreach (ItemComponent ic in components) { ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif @@ -1722,6 +1739,19 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { +#if SERVER + if (!(Submarine is { Loading: true })) + { + sendConditionUpdateTimer -= deltaTime; + if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) + { + SendPendingNetworkUpdates(); + } + } +#endif + + if (!isActive) { return; } + if (impactQueue != null) { while (impactQueue.TryDequeue(out float impact)) @@ -1730,22 +1760,11 @@ namespace Barotrauma } } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true)) - { - sendConditionUpdateTimer -= deltaTime; - if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f) - { - SendPendingNetworkUpdates(); - } - } - - if (aiTarget != null) + if (aiTarget != null && aiTarget.NeedsUpdate) { aiTarget.Update(deltaTime); } - if (!isActive) { return; } - ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); @@ -1846,7 +1865,10 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) + if (updateableComponents.Count == 0 && + (aiTarget == null || !aiTarget.NeedsUpdate) && + !hasStatusEffectsOfType[(int)ActionType.Always] && + (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1983,6 +2005,7 @@ namespace Barotrauma impactQueue ??= new ConcurrentQueue(); impactQueue.Enqueue(impact); + isActive = true; return true; } @@ -2501,11 +2524,9 @@ namespace Barotrauma if (ic.Use(deltaTime, character)) { ic.WasUsed = true; - #if CLIENT - ic.PlaySound(ActionType.OnUse, character); -#endif - + ic.PlaySound(ActionType.OnUse, character); +#endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); if (ic.DeleteOnUse) { remove = true; } @@ -2534,11 +2555,9 @@ namespace Barotrauma if (ic.SecondaryUse(deltaTime, character)) { ic.WasSecondaryUsed = true; - #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, character); #endif - ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); if (ic.DeleteOnUse) { remove = true; } @@ -3219,9 +3238,17 @@ namespace Barotrauma item.RecalculateConditionValues(); item.SetActiveSprite(); - if (submarine?.Info.GameVersion != null) + Version savedVersion = submarine?.Info.GameVersion; + if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase)) { - SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion); + //character inventories are loaded from the game session file - use the version number of the saved game session instead of the sub + //(the sub may have already been saved and up-to-date, even though the character inventories aren't) + savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0")); + } + + if (savedVersion != null) + { + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion); } foreach (ItemComponent component in item.components) @@ -3342,6 +3369,7 @@ namespace Barotrauma ic.ShallowRemove(); } ItemList.Remove(this); + dangerousItems.Remove(this); if (body != null) { @@ -3400,6 +3428,7 @@ namespace Barotrauma #endif } ItemList.Remove(this); + dangerousItems.Remove(this); if (body != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs index a93ea13c3..a8c23fb81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/StartItemSet.cs @@ -26,9 +26,15 @@ namespace Barotrauma public readonly ImmutableArray Items; + /// + /// The order in which the sets are displayed in menus + /// + public readonly int Order; + public StartItemSet(ContentXElement element, StartItemsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { Items = element.Elements().Select(e => new StartItem(e!)).ToImmutableArray(); + Order = element.GetAttributeInt("order", 0); } public override void Dispose() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 227f59f3d..574115e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -10,12 +10,11 @@ using System.Net; namespace Barotrauma { + #warning TODO: MapEntityPrefab should be constrained further to not include item assemblies, as assemblies are effectively not entities at all partial class ItemAssemblyPrefab : MapEntityPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); - private readonly XElement configElement; public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 61049099d..d6be0f566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -49,7 +49,7 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20, // Not used anywhere + BeaconStation = 0x20, Abyss = 0x40, AbyssCave = 0x80 } @@ -395,6 +395,13 @@ namespace Barotrauma /// public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost; + /// + /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1) + /// + public static bool IsLoadedFriendlyOutpost => + loaded?.Type == LevelData.LevelType.Outpost && + (loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1); + public LevelGenerationParams GenerationParams { get { return LevelData.GenerationParams; } @@ -421,7 +428,7 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } - public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); } @@ -433,11 +440,11 @@ namespace Barotrauma preSelectedStartOutpost = startOutpost, preSelectedEndOutpost = endOutpost }; - level.Generate(mirror); + level.Generate(mirror, startLocation, endLocation); return level; } - private void Generate(bool mirror) + private void Generate(bool mirror, Location startLocation, Location endLocation) { Loaded?.Remove(); Loaded = this; @@ -454,8 +461,8 @@ namespace Barotrauma if (LevelData.ForceOutpostGenerationParams == null) { - StartLocation = GameMain.GameSession?.StartLocation; - EndLocation = GameMain.GameSession?.EndLocation; + StartLocation = startLocation; + EndLocation = endLocation; } GenerateEqualityCheckValue(LevelGenStage.GenStart); @@ -509,7 +516,7 @@ namespace Barotrauma Rectangle pathBorders = borders; pathBorders.Inflate( -Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5), - -Math.Min(minMainPathWidth, borders.Height / 5)); + -Math.Min(minMainPathWidth * 2, borders.Height / 5)); if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); } if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); } @@ -1713,7 +1720,7 @@ namespace Barotrauma #endif } } - else + else if (abyssHeight > 30000) { //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth //but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth) @@ -3527,6 +3534,8 @@ namespace Barotrauma } else if (type == SubmarineType.BeaconStation) { + PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub)); + sub.ShowSonarMarker = false; sub.DockedTo.ForEach(s => s.ShowSonarMarker = false); sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; @@ -3940,7 +3949,7 @@ namespace Barotrauma //the submarine port has to be at the top of the sub if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; } float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X); - if (dist < closestDistance) + if (dist < closestDistance || subPort.MainDockingPort) { subPort = port; closestDistance = dist; @@ -4023,6 +4032,26 @@ namespace Barotrauma DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); return; } + + var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); @@ -4078,24 +4107,22 @@ namespace Barotrauma { if (!(GameMain.NetworkMember?.IsClient ?? false)) { - //empty the reactor - if (reactorContainer != null) + bool allowDisconnectedWires = true; + bool allowDamagedWalls = true; + if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) { - foreach (Item item in reactorContainer.Inventory.AllItems) - { - if (item.NonInteractable) { continue; } - Spawner.AddItemToRemoveQueue(item); - } + allowDisconnectedWires = info.AllowDisconnectedWires; + allowDamagedWalls = info.AllowDamagedWalls; } //remove wires float removeWireMinDifficulty = 20.0f; float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (removeWireProbability > 0.0f) + if (removeWireProbability > 0.0f && allowDisconnectedWires) { foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { - if (item.NonInteractable) { continue; } + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } Wire wire = item.GetComponent(); if (wire.Locked) { continue; } if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) @@ -4115,8 +4142,8 @@ namespace Barotrauma connection.ConnectionPanel.DisconnectedWires.Add(wire); wire.RemoveConnection(connection.Item); #if SERVER - connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); - wire.CreateNetworkEvent(); + connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel); + wire.CreateNetworkEvent(); #endif } } @@ -4124,23 +4151,25 @@ namespace Barotrauma } } - //break powered items - foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) + if (allowDamagedWalls) { - if (item.NonInteractable) { continue; } - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable))) { - item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + if (item.NonInteractable || item.InvulnerableToDamage) { continue; } + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + { + item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced); + } } - } - - //poke holes in the walls - foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) - { - if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) { - int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); - structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + { + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 10f625e3b..a6de99b67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -20,7 +20,7 @@ namespace Barotrauma public readonly string Seed; - public float Difficulty; + public readonly float Difficulty; public readonly Biome Biome; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 0ace84cde..9e550b39b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -432,7 +432,7 @@ namespace Barotrauma if (sub != null) { bool leaveBehind = false; - if (!sub.DockedTo.Contains(Submarine.MainSub)) + if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); if (Submarine.MainSub.AtEndExit) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index af68c1464..274935593 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -78,7 +78,7 @@ namespace Barotrauma /// /// Load a previously saved campaign map from XML /// - private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings) + private Map(CampaignMode campaign, XElement element) : this(campaign.Settings) { Seed = element.GetAttributeString("seed", "a"); Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); @@ -104,7 +104,7 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) { - Enabled = settings.RadiationEnabled + Enabled = campaign.Settings.RadiationEnabled }; break; } @@ -208,12 +208,12 @@ namespace Barotrauma /// /// Generate a new campaign map from the seed /// - public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings) + public Map(CampaignMode campaign, string seed) : this(campaign.Settings) { Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(); + Generate(campaign.Settings); if (Locations.Count == 0) { @@ -228,10 +228,7 @@ namespace Barotrauma foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) - { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - } + SetStartLocation(location); } //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) @@ -239,25 +236,36 @@ namespace Barotrauma foreach (Location location in Locations) { if (!location.Type.HasOutpost) { continue; } - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) - { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - } + SetStartLocation(location); } } - System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - if (StartLocation?.LevelData != null) + + void SetStartLocation(Location location) { - StartLocation.LevelData.Difficulty = 0; + if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + { + CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + } } - //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy - foreach (var locationConnection in StartLocation.Connections) + System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); + + int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); + if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium)) { - if (locationConnection.Difficulty > 0.0f) + if (StartLocation != null) { - locationConnection.Difficulty = 0.0f; - locationConnection.LevelData = new LevelData(locationConnection); + StartLocation.LevelData = new LevelData(StartLocation, 0); + } + + //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy + foreach (var locationConnection in StartLocation.Connections) + { + if (locationConnection.Difficulty > 0.0f) + { + locationConnection.Difficulty = 0.0f; + locationConnection.LevelData = new LevelData(locationConnection); + } } } @@ -276,7 +284,7 @@ namespace Barotrauma #region Generation - private void Generate() + private void Generate(CampaignSettings settings) { Connections.Clear(); Locations.Clear(); @@ -294,7 +302,6 @@ namespace Barotrauma Voronoi voronoi = new Voronoi(0.5f); List edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height); - float zoneWidth = Width / generationParams.DifficultyZones; Vector2 margin = new Vector2( Math.Min(10, Width * 0.1f), @@ -310,6 +317,7 @@ namespace Barotrauma voronoiSites.Clear(); Dictionary> locationsPerZone = new Dictionary>(); + bool possibleStartOutpostCreated = false; foreach (GraphEdge edge in edges) { if (edge.Point1 == edge.Point2) { continue; } @@ -344,12 +352,26 @@ namespace Barotrauma } LocationType forceLocationType = null; - foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + if (!possibleStartOutpostCreated) { - if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + float zoneWidth = Width / generationParams.DifficultyZones; + float threshold = zoneWidth * 0.1f; + if (position.X < threshold) { - forceLocationType = locationType; - break; + LocationType.Prefabs.TryGet("outpost", out forceLocationType); + possibleStartOutpostCreated = true; + } + } + + if (forceLocationType == null) + { + foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier)) + { + if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount) + { + forceLocationType = locationType; + break; + } } } @@ -455,9 +477,7 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 > zone2) { - int temp = zone2; - zone2 = zone1; - zone1 = temp; + (zone1, zone2) = (zone2, zone1); } if (generationParams.GateCount[zone1] == 0) { continue; } @@ -527,32 +547,43 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { - float difficulty = connection.CenterPos.X / Width * 100; - float minDifficulty = 0; - float maxDifficulty = 100; - var biome = connection.Biome; - if (biome != null) + if (connection.Locations.Any(l => l.IsGateBetweenBiomes)) { - minDifficulty = connection.Biome.MinDifficulty; - maxDifficulty = connection.Biome.MaxDifficulty; - if (connection.Locked) - { - connection.Difficulty = maxDifficulty; - } + connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty); + } + else + { + connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome); } - connection.Difficulty = MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); } CreateEndLocation(); foreach (Location location in Locations) { - location.LevelData = new LevelData(location, MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)); + location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); } foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } + + float CalculateDifficulty(float mapPosition, Biome biome) + { + float settingsFactor = settings.LevelDifficultyMultiplier; + float minDifficulty = 0; + float maxDifficulty = 100; + float difficulty = mapPosition / Width * 100; + System.Diagnostics.Debug.Assert(biome != null); + if (biome != null) + { + minDifficulty = biome.MinDifficulty; + maxDifficulty = biome.MaxDifficulty; + float diff = 1 - settingsFactor; + difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff); + } + return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty); + } } partial void GenerateLocationConnectionVisuals(); @@ -633,6 +664,11 @@ namespace Barotrauma if (EndLocation == null || previousToEndLocation == null) { return; } + if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) + { + previousToEndLocation.ChangeType(locationType); + } + //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { @@ -652,7 +688,7 @@ namespace Barotrauma } //removed all connections from the second-to-last location, need to reconnect it - if (!previousToEndLocation.Connections.Any()) + if (previousToEndLocation.Connections.None()) { Location connectTo = Locations.First(); foreach (Location location in Locations) @@ -759,6 +795,7 @@ namespace Barotrauma CurrentLocation = Locations[index]; CurrentLocation.Discover(); + CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) { var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation)); @@ -766,10 +803,8 @@ namespace Barotrauma { connection.Passed = true; } + OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } - - CurrentLocation.CreateStores(); - OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } public void SelectLocation(int index) @@ -789,6 +824,7 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); SelectedConnection = @@ -798,7 +834,10 @@ namespace Barotrauma { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectLocation(Location location) @@ -811,13 +850,17 @@ namespace Barotrauma return; } + Location prevSelected = SelectedLocation; SelectedLocation = location; SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); if (SelectedConnection?.Locked ?? false) { DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); } - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + if (prevSelected != SelectedLocation) + { + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + } } public void SelectMission(IEnumerable missionIndices) @@ -830,23 +873,24 @@ namespace Barotrauma return; } - CurrentLocation.SetSelectedMissionIndices(missionIndices); - - foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) + if (!missionIndices.SequenceEqual(GetSelectedMissionIndices())) { - if (selectedMission.Locations[0] != CurrentLocation || - selectedMission.Locations[1] != CurrentLocation) + CurrentLocation.SetSelectedMissionIndices(missionIndices); + foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) { - if (SelectedConnection == null) { return; } - //the destination must be the same as the destination of the mission - if (selectedMission.Locations[1] != SelectedLocation) + if (selectedMission.Locations[0] != CurrentLocation || + selectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.DeselectMission(selectedMission); + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (selectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.DeselectMission(selectedMission); + } } } + OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } - - OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } public void SelectRandomLocation(bool preferUndiscovered) @@ -1070,9 +1114,9 @@ namespace Barotrauma /// /// Load a previously saved map from an xml element /// - public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings) + public static Map Load(CampaignMode campaign, XElement element) { - Map map = new Map(campaign, element, settings); + Map map = new Map(campaign, element); map.LoadState(element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs new file mode 100644 index 000000000..7d9f33be6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + class BeaconStationInfo : ISerializableEntity + { + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedWalls { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDisconnectedWires { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float MinLevelDifficulty { get; set; } + + [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + public float MaxLevelDifficulty { get; set; } + + public string Name { get; private set; } + + public Dictionary SerializableProperties { get; private set; } + + public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public BeaconStationInfo(SubmarineInfo submarineInfo) + { + Name = $"BeaconStationInfo ({submarineInfo.Name})"; + SerializableProperties = SerializableProperty.DeserializeProperties(this); + } + + public BeaconStationInfo(BeaconStationInfo original) + { + Name = original.Name; + SerializableProperties = new Dictionary(); + foreach (KeyValuePair kvp in original.SerializableProperties) + { + SerializableProperties.Add(kvp.Key, kvp.Value); + if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null) + { + kvp.Value.TrySetValue(this, kvp.Value.GetValue(original)); + } + } + } + + public void Save(XElement element) + { + SerializableProperty.SerializeProperties(this, element); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8e598475e..1da36a5fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -830,43 +830,44 @@ namespace Barotrauma private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { - IEnumerable availableModules = null; + IEnumerable modulesWithCorrectFlags = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { - availableModules = modules + modulesWithCorrectFlags = modules .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - - if (prevModule != null) + var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + if (!suitableModules.Any()) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); + //no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + //still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true); + } + //still not found! Try if we can find a generic module that's not meant to attach to the previous module + if (!suitableModules.Any()) + { + suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false); + } } - if (availableModules.Count() == 0) { return null; } - - //try to search for modules made specifically for this location type first - var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); - - //if not found, search for modules suitable for any location type - if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) - { - modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); - } - - if (!modulesSuitableForLocationType.Any()) + if (!suitableModules.Any()) { if (allowDifferentLocationType) { + if (modulesWithCorrectFlags.Any()) + DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { @@ -875,7 +876,28 @@ namespace Barotrauma } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + } + + IEnumerable GetSuitable(IEnumerable modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific) + { + IEnumerable suitable = modules; + if (requireCorrectLocationType) + { + if (disallowNonLocationTypeSpecific) + { + suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); + } + else + { + suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } + } + if (requireAllowAttachToPrevious && prevModule != null) + { + suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule)); + } + return suitable; } } @@ -1590,10 +1612,6 @@ namespace Barotrauma { npc.CharacterHealth.Unkillable = true; } - else - { - npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); - } humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index b5f7d87ba..18321db98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1483,8 +1483,10 @@ namespace Barotrauma { if (item.Submarine != this) continue; if (item.ParentInventory != null || item.body != null) continue; - var lightComponent = item.GetComponent(); - if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); + foreach (var light in item.GetComponents()) + { + light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f); + } } } GenerateOutdoorNodes(); @@ -1555,7 +1557,7 @@ namespace Barotrauma element.Add(new XAttribute("cargocapacity", cargoCapacity)); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); - element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); + element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString())); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); if (Info.Type == SubmarineType.OutpostModule) @@ -1632,6 +1634,7 @@ namespace Barotrauma Type = Info.Type, FilePath = filePath, OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, + BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index fb29d2652..3d1ca403e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -39,7 +39,15 @@ namespace Barotrauma public SubmarineTag Tags { get; private set; } public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; - public string RecommendedCrewExperience; + + public enum CrewExperienceLevel + { + Unknown, + CrewExperienceLow, + CrewExperienceMid, + CrewExperienceHigh + } + public CrewExperienceLevel RecommendedCrewExperience; /// /// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match @@ -89,6 +97,7 @@ namespace Barotrauma public SubmarineClass SubmarineClass; public OutpostModuleInfo OutpostModuleInfo { get; set; } + public BeaconStationInfo BeaconStationInfo { get; set; } public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; @@ -280,6 +289,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } + if (original.BeaconStationInfo != null) + { + BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo); + } #if CLIENT PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif @@ -330,7 +343,24 @@ namespace Barotrauma CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1); RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); - RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); + var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier()); + // Backwards compatibility + if (recommendedCrewExperience == "Beginner") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow; + } + else if (recommendedCrewExperience == "Intermediate") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid; + } + else if (recommendedCrewExperience == "Experienced") + { + RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh; + } + else + { + Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience); + } if (SubmarineElement?.Attribute("type") != null) { @@ -341,6 +371,10 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement); + } } } @@ -359,20 +393,6 @@ namespace Barotrauma SubmarineClass = SubmarineClass.Undefined; } - //backwards compatibility (use text tags instead of the actual text) - if (RecommendedCrewExperience == "Beginner") - { - RecommendedCrewExperience = "CrewExperienceLow"; - } - else if (RecommendedCrewExperience == "Intermediate") - { - RecommendedCrewExperience = "CrewExperienceMid"; - } - else if (RecommendedCrewExperience == "Experienced") - { - RecommendedCrewExperience = "CrewExperienceHigh"; - } - RequiredContentPackages.Clear(); string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty()); foreach (string contentPackageName in contentPackageNames) @@ -528,6 +548,11 @@ namespace Barotrauma OutpostModuleInfo.Save(newElement); OutpostModuleInfo = new OutpostModuleInfo(this, newElement); } + else if (Type == SubmarineType.BeaconStation) + { + BeaconStationInfo.Save(newElement); + BeaconStationInfo = new BeaconStationInfo(this, newElement); + } XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); @@ -590,6 +615,7 @@ namespace Barotrauma List filePaths = new List(); foreach (BaseSubFile subFile in contentPackageSubs) { + if (!File.Exists(subFile.Path.Value)) { continue; } if (!filePaths.Any(fp => fp == subFile.Path)) { filePaths.Add(subFile.Path.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index b999e8e54..144a567ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Networking private static void UpdateRead() { - Span msgLengthSpan = stackalloc byte[3]; + Span msgLengthSpan = stackalloc byte[4 + 1]; while (!shutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -182,8 +182,11 @@ namespace Barotrauma.Networking if (!readBytes(msgLengthSpan)) { shutDown = true; break; } - int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8); - WriteStatus writeStatus = (WriteStatus)msgLengthSpan[2]; + int msgLength = msgLengthSpan[0] + | (msgLengthSpan[1] << 8) + | (msgLengthSpan[2] << 16) + | (msgLengthSpan[3] << 24); + WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4]; if (msgLength > 0) { @@ -225,12 +228,15 @@ namespace Barotrauma.Networking // when the function returns; placing it in the loop // this method is based around would lead to a stack // overflow real quick! - Span bytesToWrite = stackalloc byte[3 + msg.Length]; + Span bytesToWrite = stackalloc byte[4 + 1 + msg.Length]; bytesToWrite[0] = (byte)(msg.Length & 0xFF); bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - bytesToWrite[2] = (byte)writeStatus; - Span msgSlice = bytesToWrite.Slice(3, msg.Length); + bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF); + bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF); + + bytesToWrite[4] = (byte)writeStatus; + Span msgSlice = bytesToWrite.Slice(4 + 1, msg.Length); msg.AsSpan().CopyTo(msgSlice); @@ -284,6 +290,12 @@ namespace Barotrauma.Networking { if (shutDown) { return; } + if (msg.Length > 0x1fff_ffff) + { + //This message is extremely long and is close to breaking + //ChildServerRelay, so let's not allow this to go through! + return; + } msgsToWrite.Enqueue(msg); writeManualResetEvent.Set(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index b8c2d6419..afa43e536 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -28,6 +28,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization + UPDATE_CHARACTERINFO, + ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index b5aae541a..7c612b9ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -900,27 +900,13 @@ namespace Barotrauma.Networking private set; } - [Serialize(true, IsPropertySaveable.Yes)] - public bool RadiationEnabled - { - get; - set; - } - [Serialize(LootedMoneyDestination.Bank, IsPropertySaveable.Yes)] public LootedMoneyDestination LootedMoneyDestination { get; set; } [Serialize(999999, IsPropertySaveable.Yes)] public int MaximumMoneyTransferRequest { get; set; } - private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; - - [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] - public int MaxMissionCount - { - get { return maxMissionCount; } - set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } - } + public CampaignSettings CampaignSettings { get; set; } = CampaignSettings.Empty; private bool allowSubVoting; //Don't serialize: the value is set based on SubSelectionMode diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 99bb15486..8d73ee087 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -786,6 +786,9 @@ namespace Barotrauma case nameof(Character.HealthMultiplier): { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } + break; } return false; } @@ -799,6 +802,12 @@ namespace Barotrauma case nameof(Character.ObstructVision): { if (parentObject is Character character) { character.ObstructVision = value; return true; } } break; + case nameof(Character.HideFace): + { if (parentObject is Character character) { character.HideFace = value; return true; } } + break; + case nameof(Character.UseHullOxygen): + { if (parentObject is Character character) { character.UseHullOxygen = value; return true; } } + break; case nameof(LightComponent.IsOn): { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 368cb94ab..600b2161d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -490,6 +490,10 @@ namespace Barotrauma { font.Prefabs.ForEach(p => p.LoadFont()); } + foreach (var componentStyle in GUIStyle.ComponentStyles) + { + componentStyle.RefreshSize(); + } } GameMain.SoundManager?.ApplySettings(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 025127e42..94e6c7bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -94,7 +94,13 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { - if (this.type != type || !HasRequiredItems(entity)) { return; } + if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + if (!HasRequiredItems(entity)) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } if (!Stackable) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index fd364e4f8..eebf72549 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -132,7 +132,11 @@ namespace Barotrauma public enum SpawnPositionType { This, + //the inventory of the StatusEffect's target entity ThisInventory, + //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + SameInventory, + //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) ContainedInventory } @@ -308,11 +312,26 @@ namespace Barotrauma private readonly float lifeTime; private float lifeTimer; + public float intervalTimer; + public static readonly List DurationList = new List(); - public readonly bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// + public readonly bool CheckConditionalAlways; - public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? + /// + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// + public readonly bool Stackable = true; + + /// + /// The interval at which the effect is executed. The difference between delay and interval is that effects with a delay find the targets, check the conditions, etc + /// immediately when Apply is called, but don't apply the effects until the delay has passed. Effects with an interval check if the interval has passed when Apply is + /// called and apply the effects if it has, otherwise they do nothing. + /// + public readonly float Interval; #if CLIENT private readonly bool playSoundOnRequiredItemFailure = false; @@ -450,6 +469,8 @@ namespace Barotrauma TargetSlot = element.GetAttributeInt("targetslot", -1); + Interval = element.GetAttributeFloat("interval", 0.0f); + Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); @@ -556,6 +577,7 @@ namespace Barotrauma " - sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "delay": + case "interval": break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -1094,6 +1116,12 @@ namespace Barotrauma { if (this.type != type) { return; } + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } + currentTargets.Clear(); foreach (ISerializableEntity target in targets) { @@ -1195,7 +1223,11 @@ namespace Barotrauma lifeTimer -= deltaTime; if (lifeTimer <= 0) { return; } } - + if (intervalTimer > 0.0f) + { + intervalTimer -= deltaTime; + return; + } Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); if (useItemCount > 0) @@ -1717,6 +1749,26 @@ namespace Barotrauma } } break; + case ItemSpawnInfo.SpawnPositionType.SameInventory: + { + Inventory inventory = null; + if (entity is Character character) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + inventory = item.ParentInventory; + } + if (inventory != null) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + }); + } + } + break; case ItemSpawnInfo.SpawnPositionType.ContainedInventory: { Inventory thisInventory = null; @@ -1756,6 +1808,8 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + intervalTimer = Interval; + static Character CharacterFromTarget(ISerializableEntity target) { Character targetCharacter = target as Character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 46a58055b..7db7254a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -414,7 +414,7 @@ namespace Barotrauma.Steam { await Task.Yield(); Identifier extension = Path.GetExtension(from).ToIdentifier(); - if (extension == ".xml" && shouldCorrectPaths == ShouldCorrectPaths.Yes) + if (extension == ".xml") { try { @@ -427,10 +427,14 @@ namespace Barotrauma.Steam { throw new Exception($"Could not load \"{from}\": doc is null"); } - await CorrectPaths( - fileListDir: fileListDir, - modName: modName, - element: doc.Root ?? throw new NullReferenceException()); + + if (shouldCorrectPaths == ShouldCorrectPaths.Yes) + { + await CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: doc.Root ?? throw new NullReferenceException()); + } doc.SaveSafe(to); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index c9152e8c1..73c6d6860 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -15,6 +15,21 @@ namespace Barotrauma public bool IsNone() => this is None; public bool IsSome() => this is Some; + public bool TryUnwrap(out T outValue) + { + switch (this) + { + case Some { Value: var value }: + outValue = value; + return true; + case None _: + outValue = default; + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + public Option Select(Func selector) => this switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index b63e7a2bc..bb1950e0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -11,8 +11,8 @@ namespace Barotrauma public static Success Success(T value) => new Success(value); - public static Failure Failure(TError error, string? stackTrace) - => new Failure(error, stackTrace); + public static Failure Failure(TError error) + => new Failure(error); } public sealed class Success : Result @@ -34,14 +34,11 @@ namespace Barotrauma { public readonly TError Error; - public readonly string? StackTrace; - public override bool IsSuccess => false; - public Failure(TError error, string? stackTrace) + public Failure(TError error) { Error = error; - StackTrace = stackTrace; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index ea2b7c2e8..651f13975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -18,6 +18,23 @@ namespace Barotrauma.IO ".bat", ".sh", //shell scripts }.ToIdentifiers().ToImmutableArray(); + public ref struct Skipper + { + public void Dispose() + { + SkipValidationInDebugBuilds = false; + } + } + + /// + /// Skips validation for as long as the returned object remains in scope (remember to use using) + /// + public static Skipper SkipInDebugBuilds() + { + SkipValidationInDebugBuilds = true; + return new Skipper(); + } + /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index c73f40124..6ca456993 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -126,6 +126,10 @@ namespace Barotrauma public static void LoadGame(string filePath) { + //ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub) + //can happen if a gamesession is interrupted ungracefully (exception during loading) + Submarine.Unload(); + GameMain.GameSession = null; DebugConsole.Log("Loading save file: " + filePath); DecompressToDirectory(filePath, TempPath, null); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index ce02ef8f7..1cd44dc4e 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,76 @@ +--------------------------------------------------------------------------------------------------------- +v0.18.4.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Option to select whether to automatically transfer the items from the old sub to a new one when switching the sub. +- Fixed Physicorium Chaingun Ammunition Box having an incorrect sprite. +- Fixed crashing when trying to load a save that includes ItemContainers with different kinds of inventory slots (e.g. PUCS). +- Disposable suits no longer protect from pressure when broken. +- Made disposable suits play a sound and their lights flicker when the suit is about to break. +- Fixed inability to access the character tab when your character is dead in non-campaign modes, fixed creating a new character not doing anything mid-round. +- Fixed sprite editor crashing if you try to reload a texture twice. +- Fixed drone/shuttles getting left behind in the outpost when you buy and switch to a sub that has a one. +- Fixed crashing when closing the submarine preview window. + +Changes: +- Added some new campaign settings: starting balance, amount of starting items and difficulty. +- Added two new beacon stations. +- Made the descriptions of some materials (that used to just say "useful for crafting") more descriptive. +- Increased oxygen generator output in some vanilla subs. +- Made handheld sonar beacon sound less grating. +- Disallowed mirroring beds vertically. +- The client who initiated a vote cannot take part in that vote (except if they're the only client who can vote, in which case the vote automatically passes). +- Made flashlight flicker before the battery runs out. +- Added some lootable money to corpses found in wrecks. +- Removed the small equipment indicators next to the character portrait. +- Added a bunch of new UI sounds (tickbox toggling, confirming transactions, increase/decrease sounds for number inputs, cart sound for adding/removing items in store interfaces, selecting/clicking components, sliders and modlist). + + +Fixes: +- Split campaign state networking messages into multiple ones. Previously all the campaign-related data (map state, reputation, upgrades, purchased items, selected missions) was included in the same message, and whenever anything in the data changed, the server would send all of it to clients. This would cause performance and bandwidth issues in some situations, for example when reputation was changing rapidly. +- Fixed some pumps in Kastrull working without power. +- Fixed quick-reloading working incorrectly when trying to reload from a stack that doesn't fully fit in the weapon (e.g. when double clicking on a full stack of revolver rounds with a half-loaded revolver in hand). +- Fixed inability to quick-reload weapons with more than 1 inventory slot (e.g. autoshotgun). +- Fixed outpost NPCs having x3 more health than they should. +- Fixed morbusine not killing NPCs with higher-than-default health. +- Fixed crashing with the error message "couldn't find a valid ICU package installed on the system" on some Linux distributions. +- Fixed graphics errors when using Razer Cortex overlay. +- Fixed bots being unable to repair Winterhalter's top hatch. +- Fixed server crashing if you disable all mission types and try to start a mission round. +- Fixed Chinese/Japanese/Korean text not wrapping properly on terminals. +- Fixed bots sometimes walking towards a wall or holding the ladders when they are idling. +- Fixed "main docking port" property not being taken into account when placing outposts (= the outpost was placed with the assumption that the docking port closest to the sub's center is the main docking port). Sometimes caused the outpost to be placed too close to the level walls, preventing the sub from docking with it. +- Fixed ladders not being visible in the sub preview. +- Fixed some UI elements being too large when switching from a large resolution to a smaller one, or vice versa. +- Fixed weapon holder sprite depth. +- Fixed level editor's test mode generating a different level than the editor itself. + +Modding: +- Added "mod lists" which can be used to enable/disable sets of mods more easily. +- Option to choose which local mod(s) to add a submarine to when saving one in the submarine editor. +- Mods can be unsubscribed from by right-clicking on them in the mod list, and it's possible to unsubscribe from multiple ones at the same time by using ctrl+click or shift+click to select more than one. +- Local mods can be merged in the mod list by selecting the ones you want to merge and selecting "merge all selected" from the right-click context menu. +- Better filtering in the mod list: option to only show local mods, Workshop mods, published mods, submarines and/or item assemblies. +- Added "SameInventory" spawn position type to status effects (allows spawning items in the same inventory the entity applying the effect is in). +- Added support for multiple light components in wearables. + +--------------------------------------------------------------------------------------------------------- +v0.18.3.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed submarine not getting saved between levels in the multiplayer campaign. +- Fixed clients sometimes unnecessarily showing the "trying to automatically dock with the outpost" prompt even when the docking is being done manually. +- Added a new texture and icon for shotgun rubber shell. +- Added sprite for disposable suit on shelf. + +Changes: +- TriggerComponent now supports negative forces: negative force value will cause the it to pull triggerers towards it. + +Modding: +- Multiple TriggerComponent properties can now be modified through signals and CustomInterface components. + --------------------------------------------------------------------------------------------------------- v0.18.2.0 --------------------------------------------------------------------------------------------------------- @@ -148,6 +221,17 @@ Modding: - Added an extra tag to the "canned heat" talent to make it easier to add custom upgradeable tanks that aren't compatible with vanilla tools. - Option to make status effects drop the items contained inside the target item (usage example in the duffel bag). +--------------------------------------------------------------------------------------------------------- +v0.17.16.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Added some tutorial information to the data sent to GameAnalytics. + +Fixes: +- Fixed an exploit that allowed modified clients to execute console commands server-side without the appropriate permissions. +- Fixed NPCs spawning without any items when the system language is set to Turkish. + --------------------------------------------------------------------------------------------------------- v0.17.15.0 ---------------------------------------------------------------------------------------------------------