From 7a09cf326088bba799739bf89b5cc8f1a4ab9ae0 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 22 Apr 2022 21:48:04 +0900 Subject: [PATCH] Build 0.17.13.0 --- .../ClientSource/Characters/CharacterInfo.cs | 14 +--- .../ClientSource/GUI/CrewManagement.cs | 12 ++-- .../ClientSource/GUI/GUITextBlock.cs | 5 +- .../ClientSource/GUI/GUITextBox.cs | 69 +++++++++++++------ .../ClientSource/GUI/MedicalClinicUI.cs | 6 +- .../ClientSource/GUI/Store.cs | 48 ++++++++++--- .../ClientSource/GUI/SubmarineSelection.cs | 15 ++-- .../ClientSource/GUI/TabMenu.cs | 16 ++++- .../ClientSource/GUI/UpgradeStore.cs | 30 ++++---- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../GameSession/GameModes/CampaignMode.cs | 18 ++++- .../GameModes/MultiPlayerCampaign.cs | 35 ++++++++++ .../ClientSource/Items/Item.cs | 7 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 4 +- .../ClientSource/Map/LinkedSubmarine.cs | 23 ++++++- .../ClientSource/Networking/GameClient.cs | 2 + .../ClientSource/Screens/CampaignUI.cs | 8 +-- .../ClientSource/Screens/NetLobbyScreen.cs | 22 +++--- .../ClientSource/Screens/SubEditorScreen.cs | 4 ++ .../Mutable/MutableWorkshopMenu.cs | 7 ++ .../ClientSource/Utils/SpriteRecorder.cs | 5 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 5 +- .../Characters/CharacterNetworking.cs | 5 +- .../ServerSource/DebugConsole.cs | 2 +- .../ServerSource/GameSession/CargoManager.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 65 ++++++++++++++--- .../Components/Signal/ConnectionPanel.cs | 3 +- .../ServerSource/Items/Item.cs | 1 + .../ServerSource/Networking/RespawnManager.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/HumanAIController.cs | 2 +- .../SharedSource/Characters/CharacterInfo.cs | 6 +- .../Health/Afflictions/AfflictionPrefab.cs | 2 +- .../SharedSource/Characters/HumanPrefab.cs | 8 ++- .../SharedSource/Characters/Jobs/Job.cs | 27 ++++++-- .../Params/Animation/AnimationParams.cs | 4 ++ .../CharacterAbilityIncreaseSkill.cs | 2 +- .../CharacterAbilityModifyStatToSkill.cs | 5 +- .../SharedSource/DebugConsole.cs | 4 +- .../SharedSource/GameSession/CargoManager.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 17 ++++- .../SharedSource/GameSession/GameSession.cs | 4 +- .../SharedSource/GameSession/MedicalClinic.cs | 7 +- .../GameSession/UpgradeManager.cs | 4 +- .../Items/Components/ElectricalDischarger.cs | 10 ++- .../Items/Components/Holdable/IdCard.cs | 1 + .../Items/Components/Machines/Fabricator.cs | 53 ++++++++++---- .../Items/Components/Power/Powered.cs | 20 ++++-- .../SharedSource/Items/Inventory.cs | 5 +- .../SharedSource/Settings/GameSettings.cs | 6 +- .../StatusEffects/StatusEffect.cs | 2 +- .../SharedSource/Steam/Workshop.cs | 11 +-- Barotrauma/BarotraumaShared/changelog.txt | 35 ++++++++++ 58 files changed, 506 insertions(+), 184 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 9a8d2b202..d96f35a87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -116,7 +116,7 @@ namespace Barotrauma Stretch = true }; - var skills = Job.Skills; + var skills = Job.GetSkills().ToList(); skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; @@ -560,17 +560,7 @@ namespace Barotrauma ch.SetPersonalityTrait(); if (ch.Job != null) { - foreach (KeyValuePair skill in skillLevels) - { - Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key); - if (matchingSkill == null) - { - ch.Job.Skills.Add(new Skill(skill.Key, skill.Value)); - continue; - } - matchingSkill.Level = skill.Value; - } - ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); + ch.Job.OverrideSkills(skillLevels); } ch.ExperiencePoints = inc.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 37935913a..aa17718c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -172,7 +172,7 @@ namespace Barotrauma { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => TextManager.FormatCurrency(campaign.Wallet.Balance) + TextGetter = () => TextManager.FormatCurrency(campaign.GetBalance()) }; var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -344,7 +344,7 @@ namespace Barotrauma Color? jobColor = null; if (characterInfo.Job != null) { - skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.Skills.OrderByDescending(s => s.Level).FirstOrDefault(); + skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.GetSkills().OrderByDescending(s => s.Level).FirstOrDefault(); jobColor = characterInfo.Job.Prefab.UIColor; } @@ -547,8 +547,8 @@ namespace Barotrauma GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true); GUILayoutGroup skillNameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), skillGroup.RectTransform)); GUILayoutGroup skillLevelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), skillGroup.RectTransform)); - List characterSkills = characterInfo.Job.Skills; - blockHeight = 1.0f / characterSkills.Count; + var characterSkills = characterInfo.Job.GetSkills(); + blockHeight = 1.0f / characterSkills.Count(); foreach (Skill skill in characterSkills) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier)); @@ -630,7 +630,7 @@ namespace Barotrauma total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = TextManager.FormatCurrency(total); - bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); + bool enoughMoney = campaign == null || campaign.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); } @@ -652,7 +652,7 @@ namespace Barotrauma int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); - if (!campaign.Wallet.CanAfford(total)) { return false; } + if (!campaign.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index f295fdc0a..e4c047bff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -395,14 +395,13 @@ namespace Barotrauma origin = TextSize * 0.5f; origin.X = 0; - if (textAlignment.HasFlag(Alignment.Left) && !overflowClipActive) + if (textAlignment.HasFlag(Alignment.Left)) { textPos.X = padding.X; } - if (textAlignment.HasFlag(Alignment.Right) || overflowClipActive) + if (textAlignment.HasFlag(Alignment.Right)) { textPos.X = rect.Width - padding.Z; - //origin.X = TextSize.X; } if (textAlignment.HasFlag(Alignment.Top)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index b4edb64ce..2bcb6bbb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -443,24 +443,7 @@ namespace Barotrauma if (CaretEnabled) { - if (textBlock.OverflowClipActive) - { - float left = textBlock.Rect.X + textBlock.Padding.X; - if (CaretScreenPos.X < left) - { - float diff = left - CaretScreenPos.X; - textBlock.TextPos = new Vector2(textBlock.TextPos.X + diff, textBlock.TextPos.Y); - CalculateCaretPos(); - } - - float right = textBlock.Rect.Right - textBlock.Padding.Z; - if (CaretScreenPos.X > right) - { - float diff = CaretScreenPos.X - right; - textBlock.TextPos = new Vector2(textBlock.TextPos.X - diff, textBlock.TextPos.Y); - CalculateCaretPos(); - } - } + HandleCaretBoundsOverflow(); caretTimer += deltaTime; caretVisible = ((caretTimer * 1000.0f) % 1000) < 500; if (caretVisible && caretPosDirty) @@ -486,6 +469,29 @@ namespace Barotrauma textBlock.State = State; } + private void HandleCaretBoundsOverflow() + { + if (textBlock.OverflowClipActive) + { + CalculateCaretPos(); + float left = textBlock.Rect.X + textBlock.Padding.X; + if (CaretScreenPos.X < left) + { + float diff = left - CaretScreenPos.X; + textBlock.TextPos = new Vector2(textBlock.TextPos.X + diff, textBlock.TextPos.Y); + CalculateCaretPos(); + } + + float right = textBlock.Rect.Right - textBlock.Padding.Z; + if (CaretScreenPos.X > right) + { + float diff = CaretScreenPos.X - right; + textBlock.TextPos = new Vector2(textBlock.TextPos.X - diff, textBlock.TextPos.Y); + CalculateCaretPos(); + } + } + } + private void DrawCaretAndSelection(SpriteBatch spriteBatch, GUICustomComponent customComponent) { if (!Visible) { return; } @@ -554,15 +560,33 @@ namespace Barotrauma { RemoveSelectedText(); } - Vector2 textPos = textBlock.TextPos; - bool wasOverflowClipActive = textBlock.OverflowClipActive; + using var _ = new TextPosPreservation(this); if (SetText(Text.Insert(CaretIndex, input))) { CaretIndex = Math.Min(Text.Length, CaretIndex + input.Length); OnTextChanged?.Invoke(this, Text); + } + } + + private readonly ref struct TextPosPreservation + { + private readonly GUITextBox textBox; + private GUITextBlock textBlock => textBox.TextBlock; + private readonly bool wasOverflowClipActive; + private readonly Vector2 textPos; + + public TextPosPreservation(GUITextBox tb) + { + textBox = tb; + wasOverflowClipActive = tb.TextBlock.OverflowClipActive; + textPos = tb.TextBlock.TextPos; + } + + public void Dispose() + { if (textBlock.OverflowClipActive && wasOverflowClipActive && !MathUtils.NearlyEqual(textBlock.TextPos, textPos)) { - textBlock.TextPos = textPos + Vector2.UnitX * Font.MeasureString(input).X * TextBlock.TextScale; + textBlock.TextPos = textPos; } } } @@ -577,6 +601,8 @@ namespace Barotrauma switch (command) { case '\b' when !Readonly: //backspace + { + using var _ = new TextPosPreservation(this); if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) { SetText(string.Empty, false); @@ -595,6 +621,7 @@ namespace Barotrauma } OnTextChanged?.Invoke(this, Text); break; + } case (char)0x3: // ctrl-c CopySelectedText(); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 93f8803cb..4cedb1806 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -271,7 +271,7 @@ namespace Barotrauma healList.PriceBlock.Text = TextManager.FormatCurrency(totalCost); healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; - if (medicalClinic.GetWallet().CanAfford(totalCost)) + if (medicalClinic.GetBalance() >= totalCost) { healList.PriceBlock.TextColor = GUIStyle.TextColorNormal; if (medicalClinic.PendingHeals.Any()) @@ -467,7 +467,7 @@ namespace Barotrauma GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { - TextGetter = () => TextManager.FormatCurrency(medicalClinic.GetWallet().Balance), + TextGetter = () => TextManager.FormatCurrency(medicalClinic.GetBalance()), AutoScaleVertical = true, TextScale = 1.1f }; @@ -577,7 +577,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")) { - Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetWallet().CanAfford(medicalClinic.GetTotalCost()), + Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetBalance() >= medicalClinic.GetTotalCost(), OnClicked = (button, _) => { button.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index bdad817ee..9d5467427 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -71,7 +71,8 @@ namespace Barotrauma private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; - private Wallet PlayerWallet => campaignUI.Campaign.Wallet; + private int Balance => campaignUI.Campaign.GetBalance(); + private bool IsBuying => activeTab switch { StoreTab.Buy => true, @@ -207,7 +208,7 @@ namespace Barotrauma { if (CurrentLocation?.Stores != null) { - if (CurrentLocation.GetStore(identifier) is { } store) + if (!identifier.IsEmpty && CurrentLocation.GetStore(identifier) is { } store) { ActiveStore = store; if (storeNameBlock != null) @@ -223,14 +224,45 @@ namespace Barotrauma else { ActiveStore = null; - string msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + string errorId, msg; + if (identifier.IsEmpty) + { + errorId = "Store.SelectStore:IdentifierEmpty"; + msg = $"Error selecting store at {CurrentLocation}: identifier is empty."; + } + else + { + errorId = "Store.SelectStore:StoreDoesntExist"; + msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + } DebugConsole.ShowError(msg); - GameAnalyticsManager.AddErrorEventOnce("Store.SelectStore:StoreDoesntExist", GameAnalyticsManager.ErrorSeverity.Error, msg); + GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); } } else { ActiveStore = null; + string errorId = "", msg = ""; + if (campaignUI.Campaign.Map == null) + { + errorId = "Store.SelectStore:MapNull"; + msg = $"Error selecting store with identifier \"{identifier}\": Map is null."; + } + else if (CurrentLocation == null) + { + errorId = "Store.SelectStore:CurrentLocationNull"; + msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation is null."; + } + else if (CurrentLocation.Stores == null) + { + errorId = "Store.SelectStore:StoresNull"; + msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation.Stores is null."; + } + if (!msg.IsNullOrEmpty()) + { + DebugConsole.ShowError(msg); + GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); + } } RefreshItemsToSell(); Refresh(); @@ -712,7 +744,7 @@ namespace Barotrauma private LocalizedString GetMerchantBalanceText() => TextManager.FormatCurrency(ActiveStore?.Balance ?? 0); - private LocalizedString GetPlayerBalanceText() => TextManager.FormatCurrency(PlayerWallet.Balance); + private LocalizedString GetPlayerBalanceText() => TextManager.FormatCurrency(Balance); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount) { @@ -2037,7 +2069,7 @@ namespace Barotrauma totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); - if (itemsToPurchase.None() || !PlayerWallet.CanAfford(totalPrice)) { return false; } + if (itemsToPurchase.None() || Balance < totalPrice) { return false; } CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, true); GameMain.Client?.SendCampaignState(); var dialog = new GUIMessageBox( @@ -2091,7 +2123,7 @@ namespace Barotrauma if (IsBuying) { shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal); - shoppingCrateTotal.TextColor = !PlayerWallet.CanAfford(buyTotal) ? Color.Red : Color.White; + shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White; } else { @@ -2137,7 +2169,7 @@ namespace Barotrauma ActiveShoppingCrateList.Content.RectTransform.Children.Any() && activeTab switch { - StoreTab.Buy => PlayerWallet.CanAfford(buyTotal), + StoreTab.Buy => Balance >= buyTotal, StoreTab.Sell => CurrentLocation != null && sellTotal <= ActiveStore.Balance, StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, _ => false diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index d4461b472..cdba1431a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System.Linq; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +using System.Globalization; namespace Barotrauma { @@ -31,7 +32,7 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyName; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; @@ -85,12 +86,10 @@ namespace Barotrauma { initialized = true; currentSubText = TextManager.Get("currentsub"); - deliveryFeeText = TextManager.Get("deliveryfee"); deliveryText = TextManager.Get("requestdeliverybutton"); switchText = TextManager.Get("switchtosubmarinebutton"); purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); purchaseOnlyText = TextManager.Get("purchase"); - priceText = TextManager.Get("price"); if (transferService) { deliveryFee = CalculateDeliveryFee(); @@ -327,12 +326,12 @@ namespace Barotrauma }; submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName; - submarineDisplays[i].submarineClass.Text = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}"))}"; + submarineDisplays[i].submarineClass.Text = TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}")); if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); - submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); + submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("price", "[amount]", amountString); } else { @@ -341,7 +340,7 @@ namespace Barotrauma if (deliveryFee > 0) { LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); - submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); + submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("deliveryfee", "[amount]", amountString); } else { @@ -577,7 +576,7 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (!GameMain.GameSession.Campaign.Wallet.CanAfford(deliveryFee) && deliveryFee > 0) + if (!GameMain.GameSession.Campaign.CanAfford(deliveryFee) && deliveryFee > 0) { new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", ("[currencyname]", currencyName), @@ -625,7 +624,7 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (!GameMain.GameSession.Campaign.Wallet.CanAfford(selectedSubmarine.Price)) + if (!GameMain.GameSession.Campaign.CanAfford(selectedSubmarine.Price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyName), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 74c244c3a..8530291a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -698,6 +698,12 @@ namespace Barotrauma Color = Color.Transparent }; + frame.OnSecondaryClicked += (component, data) => + { + NetLobbyScreen.CreateModerationContextMenu(client); + return true; + }; + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { AbsoluteSpacing = 2 @@ -1057,7 +1063,7 @@ namespace Barotrauma return; } - bool hasMoneyPermissions = campaign.AllowedToManageCampaign(ClientPermissions.ManageMoney); + bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets(); salarySlider.Enabled = hasMoneyPermissions; Wallet otherWallet; @@ -1402,6 +1408,12 @@ namespace Barotrauma private void CreateMissionInfo(GUIFrame infoFrame) { + if (Level.Loaded?.LevelData == null) + { + DebugConsole.ThrowError("Failed to display mission info in the tab menu (no level loaded).\n" + Environment.StackTrace); + return; + } + infoFrame.ClearChildren(); GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = (int)(0.0245f * missionFrame.Rect.Height); @@ -2016,7 +2028,7 @@ namespace Barotrauma { parent.Content.ClearChildren(); List skillNames = new List(); - foreach (Skill skill in character.Info.Job.Skills) + foreach (Skill skill in character.Info.Job.GetSkills()) { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index cbdf71c9f..ed431aa6b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -41,7 +41,7 @@ namespace Barotrauma private readonly CampaignUI campaignUI; private CampaignMode? Campaign => campaignUI.Campaign; - private Wallet PlayerWallet => Campaign?.Wallet ?? Wallet.Invalid; + private int PlayerBalance => Campaign?.GetBalance() ?? 0; private UpgradeTab selectedUpgradeTab = UpgradeTab.Upgrade; private GUIMessageBox? currectConfirmation; @@ -295,7 +295,7 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.FormatCurrency(PlayerWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => TextManager.FormatCurrency(PlayerWallet.Balance) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.FormatCurrency(PlayerBalance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => TextManager.FormatCurrency(PlayerBalance) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => @@ -435,14 +435,14 @@ namespace Barotrauma return false; } - if (PlayerWallet.CanAfford(hullRepairCost)) + if (PlayerBalance >= hullRepairCost) { LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (PlayerWallet.Balance >= hullRepairCost) + if (PlayerBalance >= hullRepairCost) { - PlayerWallet.TryDeduct(hullRepairCost); + Campaign.TryPurchase(null, hullRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; button.Enabled = false; @@ -470,14 +470,14 @@ namespace Barotrauma CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => { - if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { - PlayerWallet.TryDeduct(itemRepairCost); + Campaign.TryPurchase(null, itemRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; button.Enabled = false; @@ -516,14 +516,14 @@ namespace Barotrauma return false; } - if (PlayerWallet.CanAfford(shuttleRetrieveCost) && !Campaign.PurchasedLostShuttles) + if (PlayerBalance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (PlayerWallet.Balance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (PlayerBalance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { - PlayerWallet.TryDeduct(shuttleRetrieveCost); + Campaign.TryPurchase(null, shuttleRetrieveCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; button.Enabled = false; @@ -581,13 +581,13 @@ 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 = PlayerWallet.Balance >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); if (disableElement) { - frameChild.Enabled = PlayerWallet.Balance >= price && !isDisabled; + frameChild.Enabled = PlayerBalance >= price && !isDisabled; } if (!HasPermission) @@ -972,7 +972,7 @@ namespace Barotrauma buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } - if (PlayerWallet.CanAfford(price)) + if (PlayerBalance >= price) { buyButton.Enabled = true; buyButton.OnClicked += (button, o) => @@ -1602,7 +1602,7 @@ namespace Barotrauma if (button != null) { button.Enabled = currentLevel < prefab.MaxLevel; - if (WaitForServerUpdate || !campaign.Wallet.CanAfford(price)) + if (WaitForServerUpdate || campaign.GetBalance() < price) { button.Enabled = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index b819f816c..d980f20c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1104,10 +1104,10 @@ namespace Barotrauma var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText")); var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List<(LocalizedString Caption, LocalizedString Url)> links = new List<(LocalizedString, LocalizedString)>() + List<(LocalizedString Caption, string Url)> links = new List<(LocalizedString, string)>() { - (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), - (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), + (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value), + (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl").Fallback("https://discordapp.com/invite/undertow").Value), }; foreach (var link in links) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 6b0927e40..4f9411739 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Barotrauma @@ -28,6 +29,8 @@ namespace Barotrauma protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; + public static CancellationTokenSource StartRoundCancellationToken { get; private set; } + public bool ForceMapUI { get; @@ -99,6 +102,16 @@ namespace Barotrauma GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); } + public static bool AllowedToManageWallets() + { + if (GameMain.Client == null) { return true; } + + return + GameMain.Client.HasPermission(ClientPermissions.ManageMoney) || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Client.IsServerOwner; + } + public override void Draw(SpriteBatch spriteBatch) { if (overlayColor.A > 0) @@ -245,10 +258,11 @@ namespace Barotrauma GUI.ClearCursorWait(); + StartRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); - Rand.ThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; + Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); @@ -258,7 +272,7 @@ namespace Barotrauma roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; - }); + }, StartRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2e35a7dac..0bde737f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -37,6 +37,16 @@ namespace Barotrauma public Wallet PersonalWallet => Character.Controlled?.Wallet ?? Wallet.Invalid; public override Wallet Wallet => GetWallet(); + public override int GetBalance(Client client = null) + { + if (!AllowedToManageWallets()) + { + return PersonalWallet.Balance; + } + + return PersonalWallet.Balance + Bank.Balance; + } + public override Wallet GetWallet(Client client = null) { return PersonalWallet; @@ -913,6 +923,31 @@ namespace Barotrauma } } + public override bool TryPurchase(Client client, int price) + { + if (!AllowedToManageCampaign(ClientPermissions.ManageCampaign)) + { + return PersonalWallet.TryDeduct(price); + } + + int balance = PersonalWallet.Balance; + + if (balance >= price) + { + return PersonalWallet.TryDeduct(price); + } + + if (balance + Bank.Balance >= price) + { + int remainder = price - balance; + if (balance > 0) { PersonalWallet.Deduct(balance); } + Bank.Deduct(remainder); + return true ; + } + + return false; + } + public override void Save(XElement element) { //do nothing, the clients get the save files from the server diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index b993de620..7a16fca75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1550,9 +1550,10 @@ namespace Barotrauma DebugConsole.Log($"Received entity spawn message for item \"{itemName}\" (identifier: {itemIdentifier}, id: {itemId})"); - var itemPrefab = string.IsNullOrEmpty(itemIdentifier) ? - MapEntityPrefab.Find(itemName, null, showErrorMessages: false) as ItemPrefab : - MapEntityPrefab.Find(itemName, itemIdentifier, showErrorMessages: false) as ItemPrefab; + ItemPrefab itemPrefab = + string.IsNullOrEmpty(itemIdentifier) ? + ItemPrefab.Find(itemName, Identifier.Empty) : + ItemPrefab.Find(itemName, itemIdentifier.ToIdentifier()); Vector2 pos = Vector2.Zero; Submarine sub = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 01fb485ac..ddc286e99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -18,8 +18,8 @@ namespace Barotrauma foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) { - var entityPrefab = MapEntityPrefab.FindByIdentifier(identifier); - if (entityPrefab is CoreEntityPrefab) { continue; } + var entityPrefab = FindByIdentifier(identifier); + if (entityPrefab is CoreEntityPrefab || entityPrefab == null) { continue; } var drawRect = new Rectangle( (int)(rect.X * scale) + drawArea.Center.X, (int)((rect.Y) * scale) - drawArea.Center.Y, (int)(rect.Width * scale), (int)(rect.Height * scale)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 897e86a23..573c648c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -1,10 +1,12 @@ -using Barotrauma.IO; +using System; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -107,6 +109,25 @@ namespace Barotrauma } var pathContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true); + + string filePath = this.filePath; + if (filePath.StartsWith("Submarines")) + { + //this is the old submarines path, try to find a local mod that has a submarine with this name + string subName = Path.GetFileNameWithoutExtension(filePath); + string foundPath = ContentPackageManager.LocalPackages.Concat(ContentPackageManager.VanillaCorePackage.ToEnumerable()) + .SelectMany(p => p.GetFiles()) + .FirstOrDefault(f => Path.GetFileNameWithoutExtension(f.Path.Value).Equals(subName, StringComparison.OrdinalIgnoreCase)) + ?.Path.Value; + if (foundPath.IsNullOrEmpty()) + { + //no such sub found among the local mods, just guess the correct path + foundPath = Path.Combine(ContentPackage.LocalModsDir, subName, $"{subName}.sub"); + } + + filePath = foundPath; + } + var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUIStyle.SmallFont); var reloadButton = new GUIButton(new RectTransform(new Vector2(0.25f / pathBox.RectTransform.RelativeSize.X, 1.0f), pathBox.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index f0c44e9d1..32bbbda1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2709,6 +2709,8 @@ namespace Barotrauma.Networking SteamManager.LeaveLobby(); } + CampaignMode.StartRoundCancellationToken?.Cancel(); + clientPeer?.Close(); clientPeer = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index c1864e3cc..e3d4bff42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -149,7 +149,7 @@ namespace Barotrauma } else { - if (Campaign.Wallet.TryDeduct(CampaignMode.HullRepairCost)) + if (Campaign.TryPurchase(null, CampaignMode.HullRepairCost)) { GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; @@ -194,7 +194,7 @@ namespace Barotrauma } else { - if (Campaign.Wallet.TryDeduct(CampaignMode.ItemRepairCost)) + if (Campaign.TryPurchase(null, CampaignMode.ItemRepairCost)) { GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; @@ -246,7 +246,7 @@ namespace Barotrauma } else { - if (Campaign.Wallet.TryDeduct(CampaignMode.ShuttleReplaceCost)) + if (Campaign.TryPurchase(null, CampaignMode.ShuttleReplaceCost)) { GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; @@ -736,7 +736,7 @@ namespace Barotrauma public static LocalizedString GetMoney() { - return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Wallet.Balance)); + return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.GetBalance())); } private void UpdateMaxMissions(Location location) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index a0cb2b9f4..783e29c8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1559,7 +1559,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); - foreach (Skill skill in characterInfo.Job.Skills) + foreach (Skill skill in characterInfo.Job.GetSkills()) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), @@ -2190,17 +2190,17 @@ namespace Barotrauma canKick = canBan = canPromo = false; } - List options = new List(); - - options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate - { - Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); - })); - - options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: delegate + List options = new List { - GameMain.NetLobbyScreen?.SelectPlayer(client); - })); + new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate + { + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + }), + new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: delegate + { + GameMain.NetLobbyScreen?.SelectPlayer(client); + }) + }; // Creates sub context menu options for all the ranks List rankOptions = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index e6d934426..73e46e383 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1167,6 +1167,9 @@ namespace Barotrauma #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } + allEntityList.Content.RectTransform.SortChildren((i1, i2) => + string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData)?.Name.Value, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name.Value, StringComparison.Ordinal)); + } private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComponent parent) @@ -1906,6 +1909,7 @@ namespace Barotrauma } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } + MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 51db74fde..429ce9e19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -93,10 +93,17 @@ namespace Barotrauma.Steam 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(); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index 7990dee16..6fdfc7098 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -214,9 +214,9 @@ namespace Barotrauma //try to place commands of the same texture //contiguously for optimal buffer generation //while maintaining the same visual result - for (int i=1;i= 0; j--) { @@ -244,6 +244,7 @@ namespace Barotrauma //requires a vertex buffer to be rendered CrossThread.RequestExecutionOnMainThread(() => { + if (isDisposed) { return; } if (commandList.Count == 0) { return; } int startIndex = 0; for (int i = 1; i < commandList.Count; i++) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 64fa7bcd4..9844c624a 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 03c645144..55f0440fc 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index f230e5ea3..085574ed2 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 248ee0e88..a13c31686 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index d8d93ac0c..58cb9e5a4 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 096e49741..a63d4e033 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -63,8 +63,9 @@ namespace Barotrauma { msg.Write(Job.Prefab.Identifier); msg.Write((byte)Job.Variant); - msg.Write((byte)Job.Skills.Count); - foreach (Skill skill in Job.Skills) + var skills = Job.GetSkills(); + msg.Write((byte)skills.Count()); + foreach (Skill skill in skills) { msg.Write(skill.Identifier); msg.Write(skill.Level); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 9d2dc5c0a..c58fb75c8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -434,8 +434,9 @@ namespace Barotrauma } else { - msg.Write((byte)Info.Job.Skills.Count); - foreach (Skill skill in Info.Job.Skills) + var skills = Info.Job.GetSkills(); + msg.Write((byte)skills.Count()); + foreach (Skill skill in skills) { msg.Write(skill.Identifier); msg.Write(skill.Level); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 7638769f0..ff2bc8a71 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -2391,7 +2391,7 @@ namespace Barotrauma if (isMax) { level = 100; } if (skillIdentifier == "all") { - foreach (Skill skill in character.Info.Job.Skills) + foreach (Skill skill in character.Info.Job.GetSkills()) { character.Info.SetSkillLevel(skill.Identifier, level); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 5b041b615..454f1627d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -35,7 +35,7 @@ namespace Barotrauma int itemValue = sellValues[item.ItemPrefab]; if (store.Balance < itemValue || item.Removed) { continue; } store.Balance += itemValue; - campaign.GetWallet(client).TryDeduct(itemValue); + campaign.TryPurchase(client, itemValue); storeSpecificItems.Remove(item); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d91e1d0d7..965530ee9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -178,6 +178,14 @@ namespace Barotrauma GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); } + public bool AllowedToManageWallets(Client client) + { + return + client.HasPermission(ClientPermissions.ManageCampaign) || + client.HasPermission(ClientPermissions.ManageMoney) || + IsOwner(client); + } + public void SaveExperiencePoints(Client client) { ClearSavedExperiencePoints(client); @@ -430,7 +438,7 @@ namespace Barotrauma } public bool CanPurchaseSub(SubmarineInfo info, Client client) - => GetWallet(client).CanAfford(info.Price) && GetCampaignSubs().Contains(info); + => CanAfford(info.Price, client) && GetCampaignSubs().Contains(info); private readonly List discardedCharacters = new List(); public void DiscardClientCharacterData(Client client) @@ -755,8 +763,8 @@ namespace Barotrauma { switch (purchasedHullRepairs) { - case true when personalWallet.CanAfford(hullRepairCost): - personalWallet.Deduct(hullRepairCost); + case true when GetBalance(sender) >= hullRepairCost: + TryPurchase(sender, hullRepairCost); PurchasedHullRepairs = true; GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); break; @@ -771,8 +779,8 @@ namespace Barotrauma { switch (purchasedItemRepairs) { - case true when personalWallet.CanAfford(itemRepairCost): - personalWallet.Deduct(itemRepairCost); + case true when GetBalance(sender) >= itemRepairCost: + TryPurchase(sender, itemRepairCost); PurchasedItemRepairs = true; GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); break; @@ -789,7 +797,7 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && personalWallet.TryDeduct(shuttleRetrieveCost)) + else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost)) { PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); @@ -972,7 +980,7 @@ namespace Barotrauma switch (transfer.Sender) { case Some { Value: var id }: - if (id != sender.CharacterID && !AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) { return; } + if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } Wallet wallet = GetWalletByID(id); if (wallet is InvalidWallet) { return; } @@ -980,7 +988,7 @@ namespace Barotrauma TransferMoney(wallet); break; case None _: - if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) + if (!AllowedToManageWallets(sender)) { if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) { @@ -1025,7 +1033,7 @@ namespace Barotrauma { NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); - if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) { return; } + if (!AllowedToManageWallets(sender)) { return; } Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); @@ -1231,6 +1239,45 @@ namespace Barotrauma } } + public override bool TryPurchase(Client client, int price) + { + Wallet wallet = GetWallet(client); + if (!AllowedToManageWallets(client)) + { + return wallet.TryDeduct(price); + } + + int balance = wallet.Balance; + + if (balance >= price) + { + return wallet.TryDeduct(price); + } + + if (balance + Bank.Balance >= price) + { + int remainder = price - balance; + if (balance > 0) { wallet.Deduct(balance); } + Bank.Deduct(remainder); + return true ; + } + + return false; + } + + public override int GetBalance(Client client = null) + { + if (client is null) { return 0; } + + Wallet wallet = GetWallet(client); + if (!AllowedToManageWallets(client)) + { + return wallet.Balance; + } + + return wallet.Balance + Bank.Balance; + } + public override void Save(XElement element) { element.Add(new XAttribute("campaignid", CampaignID)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index a92cc70d9..2e27864a2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -188,11 +188,10 @@ namespace Barotrauma.Items.Components //already connected, no need to do anything if (Connections[i].Wires.Contains(newWire)) { continue; } - Connections[i].TryAddLink(newWire); newWire.Connect(Connections[i], true, true); + Connections[i].TryAddLink(newWire); var otherConnection = newWire.OtherConnection(Connections[i]); - if (otherConnection == null) { GameServer.Log(GameServer.CharacterLogName(c.Character) + " connected a wire to " + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index f7db7648e..fec0329b1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -79,6 +79,7 @@ namespace Barotrauma Vector2? worldPosition = applyStatusEffectEventData.WorldPosition; Character targetCharacter = applyStatusEffectEventData.TargetCharacter; + if (targetCharacter != null && targetCharacter.Removed) { targetCharacter = null; } byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 429d8b164..b11700a19 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -520,7 +520,7 @@ namespace Barotrauma.Networking public static void ReduceCharacterSkills(CharacterInfo characterInfo) { if (characterInfo?.Job == null) { return; } - foreach (Skill skill in characterInfo.Job.Skills) + foreach (Skill skill in characterInfo.Job.GetSkills()) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null) { continue; } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index e7ce723c1..367f7785c 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.12.0 + 0.17.13.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 08fa162ea..651c3bbf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -838,7 +838,7 @@ namespace Barotrauma 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; } + if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 1d384e608..41fed0163 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1033,7 +1033,7 @@ namespace Barotrauma if (Name == null || Job == null) { return 0; } int salary = 0; - foreach (Skill skill in Job.Skills) + foreach (Skill skill in Job.GetSkills()) { salary += (int)(skill.Level * skill.PriceMultiplier); } @@ -1076,10 +1076,10 @@ namespace Barotrauma { if (Job == null) { return; } - var skill = Job.Skills.Find(s => s.Identifier == skillIdentifier); + var skill = Job.GetSkill(skillIdentifier); if (skill == null) { - Job.Skills.Add(new Skill(skillIdentifier, level)); + Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false); OnSkillChanged(skillIdentifier, 0.0f, level); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 065a7cc10..4bc5d873b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -384,7 +384,7 @@ namespace Barotrauma foreach (var itemPrefab in ItemPrefab.Prefabs) { float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType)); - if (suitability > 0.0f) + if (!MathUtils.NearlyEqual(suitability, 0.0f)) { yield return new KeyValuePair(itemPrefab.Identifier, suitability); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index f48d7aa79..46a7da94f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -154,10 +154,14 @@ namespace Barotrauma public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { + if (ItemSets == null || !ItemSets.Any()) { return; } var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync); - foreach (XElement itemElement in spawnItems.GetChildElements("item")) + if (spawnItems != null) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + foreach (XElement itemElement in spawnItems.GetChildElements("item")) + { + InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 077e8a3da..f9f45d936 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -17,8 +17,6 @@ namespace Barotrauma public JobPrefab Prefab => prefab; - public List Skills => skills.Values.ToList(); - public int Variant; public Skill PrimarySkill { get; } @@ -80,7 +78,12 @@ namespace Barotrauma var prefab = JobPrefab.Random(randSync); var variant = Rand.Range(0, prefab.Variants, randSync); return new Job(prefab, randSync, variant); - } + } + + public IEnumerable GetSkills() + { + return skills.Values; + } public float GetSkillLevel(Identifier skillIdentifier) { @@ -89,6 +92,22 @@ namespace Barotrauma return skill?.Level ?? 0.0f; } + public Skill GetSkill(Identifier skillIdentifier) + { + if (skillIdentifier.IsEmpty) { return null; } + skills.TryGetValue(skillIdentifier, out Skill skill); + return skill; + } + + public void OverrideSkills(Dictionary newSkills) + { + skills.Clear(); + foreach (var newSkill in newSkills) + { + skills.Add(newSkill.Key, new Skill(newSkill.Key, newSkill.Value)); + } + } + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax) { if (skills.TryGetValue(skillIdentifier, out Skill skill)) @@ -171,7 +190,7 @@ namespace Barotrauma character.Inventory.TryPutItem(item, null, item.AllowedSlots); } - Wearable wearable = ((List)item.Components)?.Find(c => c is Wearable) as Wearable; + Wearable wearable = item.GetComponent(); if (wearable != null) { if (Variant > 0 && Variant <= wearable.Variants) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index f6a9d1d4c..f2859aaa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -293,6 +293,10 @@ namespace Barotrauma { return Create(fullPath, speciesName, animationType); } + if (type == typeof(HumanCrouchParams)) + { + return Create(fullPath, speciesName, animationType); + } if (type == typeof(FishWalkParams)) { return Create(fullPath, speciesName, animationType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index 966bac5f4..cee2198ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -47,7 +47,7 @@ namespace Barotrauma.Abilities { if (skillIdentifier == "random") { - var skill = character.Info?.Job?.Skills?.GetRandomUnsynced(); + var skill = character.Info?.Job?.GetSkills()?.GetRandomUnsynced(); if (skill == null) { return; } character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease, gainedFromAbility: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs index 3311a5ff6..26cec9b48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -30,11 +30,12 @@ namespace Barotrauma.Abilities if (useAll && Character.Info?.Job != null) { - foreach (Skill skill in Character.Info.Job.Skills) + var skills = Character.Info.Job.GetSkills(); + foreach (Skill skill in skills) { skillTotal += Character.GetSkillLevel(skill.Identifier); } - skillTotal /= Character.Info.Job.Skills.Count; + skillTotal /= skills.Count(); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 077da45bd..f1beaffcc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -824,7 +824,7 @@ namespace Barotrauma if (isMax) { level = 100; } if (skillIdentifier == "all") { - foreach (Skill skill in character.Info.Job.Skills) + foreach (Skill skill in character.Info.Job.GetSkills()) { character.Info.SetSkillLevel(skill.Identifier, level); } @@ -844,7 +844,7 @@ namespace Barotrauma { return new[] { - Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), + Character.Controlled?.Info?.Job?.GetSkills()?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), new[]{ "max" }, Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 2df3d5961..2492beea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -316,7 +316,7 @@ namespace Barotrauma } // Exchange money int itemValue = item.Quantity * buyValues[item.ItemPrefab]; - campaign.GetWallet(client).TryDeduct(itemValue); + campaign.TryPurchase(client, itemValue); GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); store.Balance += itemValue; if (removeFromCrate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index fa1aeaf55..019a75046 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -227,6 +227,21 @@ namespace Barotrauma return Bank; } + public virtual bool TryPurchase(Client client, int price) + { + return GetWallet(client).TryDeduct(price); + } + + public virtual int GetBalance(Client client = null) + { + return GetWallet(client).Balance; + } + + public bool CanAfford(int cost, Client client = null) + { + return GetBalance(client) >= cost; + } + /// /// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level, /// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end @@ -766,7 +781,7 @@ namespace Barotrauma public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } - if (!GetWallet(client).TryDeduct(characterInfo.Salary)) { return false; } + if (!TryPurchase(client, characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 57973c4ac..4704bf3e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -292,7 +292,7 @@ namespace Barotrauma if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) { - Campaign!.GetWallet(client).TryDeduct(cost); + Campaign!.TryPurchase(client, cost); } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; @@ -303,7 +303,7 @@ namespace Barotrauma public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.GetWallet(client).TryDeduct(newSubmarine.Price)) { return; } + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, newSubmarine.Price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 3204ee2a5..91b63855f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -213,7 +213,7 @@ namespace Barotrauma if (!force) { if (IsOutpostInCombat()) { return HealRequestResult.Refused; } - if (!GetWallet(client).TryDeduct(totalCost)) { return HealRequestResult.InsufficientFunds; } + if (!(campaign?.TryPurchase(client, totalCost) ?? false)) { return HealRequestResult.InsufficientFunds; } } ImmutableArray crew = GetCrewCharacters(); @@ -314,10 +314,7 @@ namespace Barotrauma private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; - public Wallet GetWallet(Client? c = null) - { - return campaign?.GetWallet(c) ?? Wallet.Invalid; - } + public int GetBalance() => campaign?.GetBalance() ?? 0; public static ImmutableArray GetCrewCharacters() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index f85b6bafe..ac3b984b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) + if (Campaign.TryPurchase(client, price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -313,7 +313,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) + if (Campaign.TryPurchase(client, price)) { PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove)); if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index bcee8a722..76dd68595 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -18,6 +17,8 @@ namespace Barotrauma.Items.Components const int MaxNodes = 100; const float MaxNodeDistance = 150.0f; + private bool waitForVoltageRecalculation; + public struct Node { public Vector2 WorldPosition; @@ -120,6 +121,7 @@ namespace Barotrauma.Items.Components CurrPowerConsumption = powerConsumption; Voltage = 0.0f; + waitForVoltageRecalculation = true; charging = true; timer = Duration; IsActive = true; @@ -134,6 +136,12 @@ namespace Barotrauma.Items.Components #if CLIENT frameOffset = Rand.Int(electricitySprite.FrameCount); #endif + if (waitForVoltageRecalculation) + { + waitForVoltageRecalculation = false; + return; + } + if (timer <= 0.0f) { IsActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index e0c965cf4..796bcf372 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -98,6 +98,7 @@ namespace Barotrauma.Items.Components OwnerName = info.Name; OwnerJobId = info.Job?.Prefab.Identifier ?? Identifier.Empty; + item.AddTag($"jobid:{OwnerJobId}"); OwnerTagSet = info.Head.Preset.TagSet; OwnerHairIndex = head.HairIndex; OwnerBeardIndex = head.BeardIndex; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 1b93ba4b9..a35141def 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -315,6 +315,15 @@ namespace Barotrauma.Items.Components } } + private Client GetUsingClient() + { +#if SERVER + return GameMain.Server.ConnectedClients.Find(c => c.Character == user); +#elif CLIENT + return null; +#endif + } + private void Fabricate() { RefreshAvailableIngredients(); @@ -327,9 +336,20 @@ namespace Barotrauma.Items.Components if (fabricatedItem.RequiredMoney > 0) { if (user == null) { return; } - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign) { - user.Wallet.Deduct(fabricatedItem.RequiredMoney); +#if CLIENT + mpCampaign.TryPurchase(null, fabricatedItem.RequiredMoney); +#elif SERVER + if (GetUsingClient() is { } client) + { + mpCampaign.TryPurchase(client, fabricatedItem.RequiredMoney); + } + else + { + user.Wallet.Deduct(fabricatedItem.RequiredMoney); + } +#endif } else if (GameMain.GameSession?.GameMode is CampaignMode campaign) { @@ -530,6 +550,10 @@ namespace Barotrauma.Items.Components { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); } + if (!fabricatedItem.TargetItem.Tags.Contains(fabricatedItem.TargetItem.Identifier)) + { + floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, fabricatedItem.TargetItem.Identifier); + } quality = (int)floatQuality; const int MaxCraftingSkill = 100; @@ -548,17 +572,22 @@ namespace Barotrauma.Items.Components if (fabricableItem.RequiredMoney > 0) { - if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + switch (GameMain.GameSession?.GameMode) { - if (character?.Wallet == null || character.Wallet.Balance < fabricableItem.RequiredMoney) { return false; } - } - else if (GameMain.GameSession?.GameMode is CampaignMode campaign) - { - if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; } - } - else - { - return false; + case MultiPlayerCampaign mpCampaign: + { + if (!mpCampaign.CanAfford(fabricableItem.RequiredMoney, GetUsingClient())) { return false; } + + break; + } + case CampaignMode campaign: + { + if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; } + + break; + } + default: + return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index b6d481651..c016836b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -565,16 +565,20 @@ namespace Barotrauma.Items.Components //Iterate through all connections in the group to get their minmax power and sum them foreach (Connection c in scrGroup.Connections) { - Powered device = c.Item.GetComponent(); - scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load); + foreach (var device in c.Item.GetComponents()) + { + scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load); + } } //Iterate through all connections to get their final power out provided the min max information float addedPower = 0; foreach (Connection c in scrGroup.Connections) { - Powered device = c.Item.GetComponent(); - addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load); + foreach (var device in c.Item.GetComponents()) + { + addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load); + } } //Add the power to the grid @@ -591,10 +595,12 @@ namespace Barotrauma.Items.Components grid.Voltage = newVoltage; //Iterate through all connections on that grid and run their gridResolved function - foreach (Connection con in grid.Connections) + foreach (Connection c in grid.Connections) { - Powered device = con.Item.GetComponent(); - device?.GridResolved(con); + foreach (var device in c.Item.GetComponents()) + { + device?.GridResolved(c); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index dedbe948c..a4281fe28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -758,8 +758,11 @@ namespace Barotrauma { for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } + if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } } + } + if (otherInventory.visualSlots != null) + { for (int j = 0; j < otherInventory.capacity; j++) { if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 7292d5bf0..d1aac1552 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -43,7 +43,7 @@ namespace Barotrauma { Config config = new Config { - Language = LanguageIdentifier.None, + Language = TextManager.DefaultLanguage, SubEditorUndoBuffer = 32, MaxAutoSaves = 8, AutoSaveIntervalSeconds = 300, @@ -99,6 +99,10 @@ namespace Barotrauma Config retVal = fallback ?? GetDefault(); retVal.DeserializeElement(element); + if (retVal.Language == LanguageIdentifier.None) + { + retVal.Language = TextManager.DefaultLanguage; + } retVal.Graphics = GraphicsSettings.FromElements(element.GetChildElements("graphicsmode", "graphicssettings"), retVal.Graphics); retVal.Audio = AudioSettings.FromElements(element.GetChildElements("audio"), retVal.Audio); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index b630c8b11..017a10301 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1454,7 +1454,7 @@ namespace Barotrauma Identifier GetRandomSkill() { - return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandomUnsynced() ?? Identifier.Empty; + return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index f313f89fc..09e9c36ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -250,12 +250,15 @@ namespace Barotrauma.Steam public static void DeleteFailedCopies() { - foreach (var dir in Directory.EnumerateDirectories(ContentPackage.WorkshopModsDir, "**")) + if (Directory.Exists(ContentPackage.WorkshopModsDir)) { - string copyingIndicatorPath = Path.Combine(dir, ContentPackageManager.CopyIndicatorFileName); - if (File.Exists(copyingIndicatorPath)) + foreach (var dir in Directory.EnumerateDirectories(ContentPackage.WorkshopModsDir, "**")) { - Directory.Delete(dir, recursive: true); + string copyingIndicatorPath = Path.Combine(dir, ContentPackageManager.CopyIndicatorFileName); + if (File.Exists(copyingIndicatorPath)) + { + Directory.Delete(dir, recursive: true); + } } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d21b87768..4de5432a3 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,38 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.13.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Players who are allowed to manage money in the multiplayer campaign can use money directly from the bank without having to transfer it to their wallet first. +- Managing money always requires permissions: unlike other campaign-related permissions, not having anyone with permissions on the server doesn't give everyone permissions. + +Fixes: +- Fixed deconstructors, fabricators and research stations not being powered in some colony modules. +- Fixed power connections not getting recalculated server-side when disconnecting and reconnecting a wire. +- Fixed client context menu not working in the tab menu if the client's not controlling a character. +- Fixed preview image disappearing when saving a sub. +- Fixed custom jobid tags not working on ID cards. +- Fixed crashing with the error "Coroutine Barotrauma.SinglePlayerCampaign+d__16 threw an exception" when trying to give items to a human prefab instance that has no item sets configured. +- Fixed installed mods list not refreshing when uninstalling an unsubscribed mod. +- Fixed outpost reactors using mechanical skill for repairs instead of electrical. +- Fixed outdated Dugong preview image. +- Fixed Deadeye Carbine firing an inconsistent number of rounds per burst in multiplayer. +- Fixed crashing when launching the server with a fresh config file due to the language being set to None. +- Fixed crashing on startup if the workshop mod directory doesn't exist. +- Fixed "Canned Heat" not having an effect on oxygenite tanks. +- Fixed entity list's search results being in a random order in the sub editor. +- Fixed crashing when trying to view an item assembly that contains entities that can't be found in the sub editor. +- Fixed bots not taking medical items' negative effects into account when determining which meds to use, often leading to overdoses/suffocation when using opiates. +- Fixed bots trying to clean up items into deconstructors. +- Fixed clients failing to spawn items with console commands when there's a structure prefab with the same identifier (e.g. ladders). +- Fixed characters being unable to gain skills added by a mod if the job doesn't initially have those skills defined. +- Automatically correct linked submarine paths in the submarine editor. +- Fixed electrical discharge coils sometimes working with insufficient power. +- Fixed crashing when creating a humanoid character in character editor. +- Fixed cargo missions putting cargo in non-interactable and hidden-in-game containers. +- Fixed only the first Powered component being considered when determining how much power an item is supplying to the grid. Prevented alien generators from working. +- Fixed textbox's text position breaking when there's overflow and you're editing in the middle of the string. + --------------------------------------------------------------------------------------------------------- v0.17.12.0 ---------------------------------------------------------------------------------------------------------