diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 8f5f6b791..2057d9581 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -73,7 +73,6 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { - // TODO: move this into the character editor //var mouthPos = ragdoll.GetMouthPosition(); //if (mouthPos != null) //{ @@ -173,6 +172,7 @@ namespace Barotrauma public float DefaultSpriteDepth { get; private set; } + public WearableSprite HairWithHatSprite { get; set; } public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } @@ -236,8 +236,8 @@ namespace Barotrauma public string HitSoundTag => Params?.Sound?.Tag; - private List wearableTypeHidingSprites = new List(); - private List wearableTypesToHide = new List(); + private readonly List wearableTypeHidingSprites = new List(); + private readonly HashSet wearableTypesToHide = new HashSet(); private bool enableHuskSprite; public bool EnableHuskSprite { @@ -895,7 +895,15 @@ namespace Barotrauma foreach (WearableSprite wearable in OtherWearables) { if (wearable.Type == WearableType.Husk) { continue; } - if (wearableTypesToHide.Contains(wearable.Type)) { continue; } + if (wearableTypesToHide.Contains(wearable.Type)) + { + if (wearable.Type == WearableType.Hair && HairWithHatSprite != null) + { + DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); + depthStep += step; + } + continue; + } DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; @@ -1195,6 +1203,9 @@ namespace Barotrauma HuskSprite?.Sprite.Remove(); HuskSprite = null; + HairWithHatSprite?.Sprite.Remove(); + HairWithHatSprite = null; + HerpesSprite?.Sprite.Remove(); HerpesSprite = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 7498d3d57..e4b05779f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -32,7 +32,8 @@ namespace Barotrauma public void ClientExecute(string[] args) { - if (!CheatsEnabled && IsCheat) + bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); + if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); #if USE_STEAM @@ -743,7 +744,7 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; + float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); @@ -1894,7 +1895,12 @@ namespace Barotrauma ThrowError($"\"{args[0]}\" is not a valid Level.PositionType. Available options are: {string.Join(", ", enums)}"); return; } - debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType)); + bool fullLog = false; + if (args.Length > 1) + { + bool.TryParse(args[1], out fullLog); + } + debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType), fullLog: fullLog); } else { @@ -2409,7 +2415,7 @@ namespace Barotrauma TextManager.CheckForDuplicates(args[0]); })); - commands.Add(new Command("writetocsv", "Writes the default language (English) to a .csv file.", (string[] args) => + commands.Add(new Command("writetocsv|xmltocsv", "Writes the default language (English) to a .csv file.", (string[] args) => { TextManager.WriteToCSV(); NPCConversation.WriteToCSV(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 6da7dc016..5f3d68929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1091,6 +1091,29 @@ namespace Barotrauma var maxVersion = new Version(attribute.Value); if (GameMain.Version > maxVersion) { return false; } break; + case "buildconfiguration": + switch (attribute.Value.ToString().ToLowerInvariant()) + { + case "debug": +#if DEBUG + return true; +#else + break; +#endif + case "unstable": +#if UNSTABLE + return true; +#else + break; +#endif + case "release": +#if !DEBUG && !UNSTABLE + return true; +#else + break; +#endif + } + return false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 6c0c03e91..92b4362c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -439,7 +439,7 @@ namespace Barotrauma for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); - if (!child.Visible) { continue; } + if (child == null || !child.Visible) { continue; } if (RectTransform != null) { callback(i, new Point(x, y)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 191a8392d..8c2e11dc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -43,36 +43,42 @@ namespace Barotrauma private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh; private Point resolutionWhenCreated; - private bool hadPermissions; private Dictionary OwnedItems { get; } = new Dictionary(); private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; private int PlayerMoney => campaignUI.Campaign.Money; - private bool HasPermissions => campaignUI.Campaign.AllowedToManageCampaign(); + private bool IsBuying => activeTab switch { StoreTab.Buy => true, StoreTab.Sell => false, - StoreTab.SellFromSub => false, + StoreTab.SellSub => false, _ => throw new NotImplementedException() }; private GUIListBox ActiveShoppingCrateList => activeTab switch { StoreTab.Buy => shoppingCrateBuyList, StoreTab.Sell => shoppingCrateSellList, - StoreTab.SellFromSub => shoppingCrateSellFromSubList, + StoreTab.SellSub => shoppingCrateSellFromSubList, _ => throw new NotImplementedException() }; - private bool IsTabUnavailable(StoreTab tab) => !tabLists.ContainsKey(tab); - public enum StoreTab { + /// + /// Buy items from the store + /// Buy, + /// + /// Sell items from the character inventory + /// Sell, - SellFromSub + /// + /// Sell items from the sub + /// + SellSub } private enum SortingMethod @@ -84,11 +90,117 @@ namespace Barotrauma CategoryAsc } + #region Permissions + + private bool hadPermissions, hadBuyPermissions, hadSellInventoryPermissions, hadSellSubPermissions; + + private bool HasPermissions + { + get => GetPermissions(); + set => hadPermissions = value; + } + private bool HasBuyPermissions + { + get => HasPermissions || GetPermissions(StoreTab.Buy); + set => hadBuyPermissions = value; + } + private bool HasSellInventoryPermissions + { + get => HasPermissions || GetPermissions(StoreTab.Sell); + set => hadSellInventoryPermissions = value; + } + private bool HasSellSubPermissions + { + get => HasPermissions || GetPermissions(StoreTab.SellSub); + set => hadSellSubPermissions = value; + } + + private bool GetPermissions(StoreTab? tab = null) + { + if (!tab.HasValue) + { + return campaignUI.Campaign.AllowedToManageCampaign() || campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.CampaignStore); + } + else + { + return tab.Value switch + { + StoreTab.Buy => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.BuyItems), + StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems), + StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems), + _ => false, + }; + } + } + + private void UpdatePermissions(StoreTab? tab = null) + { + HasPermissions = GetPermissions(); + if (!tab.HasValue) + { + HasBuyPermissions = GetPermissions(StoreTab.Buy); + HasSellInventoryPermissions = GetPermissions(StoreTab.Sell); + HasSellSubPermissions = GetPermissions(StoreTab.SellSub); + } + else + { + switch (tab.Value) + { + case StoreTab.Buy: + HasBuyPermissions = GetPermissions(tab.Value); + break; + case StoreTab.Sell: + HasSellInventoryPermissions = GetPermissions(tab.Value); + break; + case StoreTab.SellSub: + HasSellSubPermissions = GetPermissions(tab.Value); + break; + } + } + } + + private bool HasTabPermissions(StoreTab tab) + { + return tab switch + { + StoreTab.Buy => HasBuyPermissions, + StoreTab.Sell => HasSellInventoryPermissions, + StoreTab.SellSub => HasSellSubPermissions, + _ => false + }; + } + + private bool HasActiveTabPermissions() + { + return HasTabPermissions(activeTab); + } + + private bool HavePermissionsChanged(StoreTab? tab = null) + { + if (!tab.HasValue) + { + return hadPermissions != HasPermissions; + } + else + { + bool hadTabPermissions = tab.Value switch + { + StoreTab.Buy => hadBuyPermissions, + StoreTab.Sell => hadSellInventoryPermissions, + StoreTab.SellSub => hadSellSubPermissions, + _ => false + }; + return hadTabPermissions != HasTabPermissions(tab.Value); + } + } + + #endregion + public Store(CampaignUI campaignUI, GUIComponent parentComponent) { this.campaignUI = campaignUI; this.parentComponent = parentComponent; - hadPermissions = HasPermissions; + UpdatePermissions(); CreateUI(); campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; if (CurrentLocation?.Reputation != null) @@ -109,7 +221,7 @@ namespace Barotrauma public void Refresh(bool updateOwned = true) { - hadPermissions = HasPermissions; + UpdatePermissions(); if (updateOwned) { UpdateOwnedItems(); } RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); @@ -122,7 +234,7 @@ namespace Barotrauma if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateBuyList(); RefreshStoreBuyList(); - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Buy); storeBuyList.Enabled = hasPermissions; shoppingCrateBuyList.Enabled = hasPermissions; needsBuyingRefresh = false; @@ -133,7 +245,7 @@ namespace Barotrauma if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateSellList(); RefreshStoreSellList(); - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Sell); storeSellList.Enabled = hasPermissions; shoppingCrateSellList.Enabled = hasPermissions; needsSellingRefresh = false; @@ -141,13 +253,11 @@ namespace Barotrauma private void RefreshSellingFromSub(bool updateOwned = true, bool updateItemsToSellFromSub = true) { - if (IsTabUnavailable(StoreTab.SellFromSub)) { return; } if (updateOwned) { UpdateOwnedItems(); } if (updateItemsToSellFromSub) RefreshItemsToSellFromSub(); RefreshShoppingCrateSellFromSubList(); RefreshStoreSellFromSubList(); - // TODO: Separate permissions from regular campaign permissions - var hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.SellSub); storeSellFromSubList.Enabled = hasPermissions; shoppingCrateSellFromSubList.Enabled = hasPermissions; needsSellingFromSubRefresh = false; @@ -261,7 +371,7 @@ namespace Barotrauma { StoreTab.Buy => CurrentLocation.StoreCurrentBalance + buyTotal, StoreTab.Sell => CurrentLocation.StoreCurrentBalance - sellTotal, - StoreTab.SellFromSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, + StoreTab.SellSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, _ => throw new NotImplementedException(), }; if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) @@ -325,10 +435,9 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { - if (tab == StoreTab.SellFromSub && GameMain.IsMultiplayer) { continue; } string text = tab switch { - StoreTab.SellFromSub => TextManager.Get("submarine"), + StoreTab.SellSub => TextManager.Get("submarine"), _ => TextManager.Get("campaignstoretab." + tab) }; var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform), @@ -456,16 +565,13 @@ namespace Barotrauma storeRequestedGoodGroup = CreateDealsGroup(storeSellList); tabLists.Add(StoreTab.Sell, storeSellList); - if (GameMain.IsSingleplayer) + storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) { - storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) - { - AutoHideScrollBar = false, - Visible = false - }; - storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); - tabLists.Add(StoreTab.SellFromSub, storeSellFromSubList); - } + AutoHideScrollBar = false, + Visible = false + }; + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); + tabLists.Add(StoreTab.SellSub, storeSellFromSubList); // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ @@ -526,10 +632,7 @@ namespace Barotrauma var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - if (GameMain.IsSingleplayer) - { - shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; - } + shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { @@ -569,16 +672,16 @@ namespace Barotrauma clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.DecreaseQuantity, - Enabled = HasPermissions, + Enabled = HasActiveTabPermissions(), ForceUpperCase = true, OnClicked = (button, userData) => { - if (!HasPermissions) { return false; } + if (!HasActiveTabPermissions()) { return false; } var itemsToRemove = activeTab switch { StoreTab.Buy => new List(CargoManager.ItemsInBuyCrate), StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), _ => throw new NotImplementedException(), }; itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); @@ -637,7 +740,6 @@ namespace Barotrauma private void ChangeStoreTab(StoreTab tab) { - if (IsTabUnavailable(tab)) { return; } activeTab = tab; foreach (GUIButton tabButton in storeTabButtons) { @@ -680,7 +782,7 @@ namespace Barotrauma } shoppingCrateSellList.Visible = true; break; - case StoreTab.SellFromSub: + case StoreTab.SellSub: storeBuyList.Visible = false; storeSellList.Visible = false; if (storeSellFromSubList != null) @@ -699,7 +801,6 @@ namespace Barotrauma private void FilterStoreItems(MapEntityCategory? category, string filter) { - if (IsTabUnavailable(activeTab)) { return; } selectedItemCategory = category; var list = tabLists[activeTab]; filter = filter?.ToLower(); @@ -733,7 +834,7 @@ namespace Barotrauma float prevBuyListScroll = storeBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasBuyPermissions; HashSet existingItemFrames = new HashSet(); int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; @@ -816,7 +917,7 @@ namespace Barotrauma { float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(StoreTab.Sell); HashSet existingItemFrames = new HashSet(); if ((storeRequestedGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) @@ -894,7 +995,7 @@ namespace Barotrauma { float prevSellListScroll = storeSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; - bool hasPermissions = HasPermissions; + bool hasPermissions = HasSellSubPermissions; HashSet existingItemFrames = new HashSet(); if ((storeRequestedSubGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) @@ -938,12 +1039,12 @@ namespace Barotrauma if (itemFrame == null) { var parentComponent = isRequestedGood ? storeRequestedSubGoodGroup : storeSellFromSubList as GUIComponent; - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellFromSub, forceDisable: !hasPermissions); + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellSub, forceDisable: !hasPermissions); } else { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; - SetQuantityLabelText(StoreTab.SellFromSub, itemFrame); + SetQuantityLabelText(StoreTab.SellSub, itemFrame); SetOwnedLabelText(itemFrame); SetPriceGetters(itemFrame, false); } @@ -961,8 +1062,8 @@ namespace Barotrauma removedItemFrames.AddRange(storeRequestedSubGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (activeTab == StoreTab.SellFromSub) { FilterStoreItems(); } - SortItems(StoreTab.SellFromSub); + if (activeTab == StoreTab.SellSub) { FilterStoreItems(); } + SortItems(StoreTab.SellSub); storeSellFromSubList.BarScroll = prevSellListScroll; shoppingCrateSellFromSubList.BarScroll = prevShoppingCrateScroll; @@ -1062,17 +1163,14 @@ namespace Barotrauma private void RefreshShoppingCrateList(List items, GUIListBox listBox, StoreTab tab) { - bool hasPermissions = HasPermissions; + bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; foreach (PurchasedItem item in items) { - PriceInfo priceInfo = item.ItemPrefab.GetPriceInfo(CurrentLocation); - if (priceInfo == null) { continue; } - - var itemFrame = listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier); + if (!(item.ItemPrefab.GetPriceInfo(CurrentLocation) is { } priceInfo)) { continue; } GUINumberInput numInput = null; - if (itemFrame == null) + if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; @@ -1100,10 +1198,21 @@ namespace Barotrauma } suppressBuySell = false; - var price = tab == StoreTab.Buy ? - CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo) : - CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); - totalPrice += item.Quantity * price; + try + { + int price = tab switch + { + StoreTab.Buy => CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + _ => throw new NotImplementedException() + }; + totalPrice += item.Quantity * price; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } } var removedItemFrames = listBox.Content.Children.Except(existingItemFrames).ToList(); @@ -1119,7 +1228,7 @@ namespace Barotrauma case StoreTab.Sell: sellTotal = totalPrice; break; - case StoreTab.SellFromSub: + case StoreTab.SellSub: sellFromSubTotal = totalPrice; break; } @@ -1135,7 +1244,7 @@ namespace Barotrauma private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList, StoreTab.Sell); - private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellFromSub); + private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellSub); private void SortItems(GUIListBox list, SortingMethod sortingMethod) { @@ -1286,14 +1395,12 @@ namespace Barotrauma private void SortItems(StoreTab tab, SortingMethod sortingMethod) { - if (IsTabUnavailable(tab)) { return; } tabSortingMethods[tab] = sortingMethod; SortItems(tabLists[tab], sortingMethod); } private void SortItems(StoreTab tab) { - if (IsTabUnavailable(tab)) { return; } SortItems(tab, tabSortingMethods[tab]); } @@ -1301,12 +1408,6 @@ namespace Barotrauma private GUIComponent CreateItemFrame(PurchasedItem pi, GUIComponent parentComponent, StoreTab containingTab, bool forceDisable = false) { - var tooltip = pi.ItemPrefab.Name; - if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) - { - tooltip += "\n" + pi.ItemPrefab.Description; - } - GUIListBox parentListBox = parentComponent as GUIListBox; int width = 0; RectTransform parent = null; @@ -1320,7 +1421,11 @@ namespace Barotrauma width = parentComponent.Rect.Width; parent = parentComponent.RectTransform; } - + string tooltip = pi.ItemPrefab.Name; + if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) + { + tooltip += $"\n{pi.ItemPrefab.Description}"; + } GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement") { ToolTip = tooltip, @@ -1338,8 +1443,7 @@ namespace Barotrauma var iconRelativeWidth = 0.0f; var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth; - Sprite itemIcon = pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite; - if (itemIcon != null) + if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite) is { } itemIcon) { iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true) @@ -1425,7 +1529,7 @@ namespace Barotrauma { if (suppressBuySell) { return; } PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; - if (!HasPermissions) + if (!HasActiveTabPermissions()) { numberInput.IntValue = purchasedItem.Quantity; return; @@ -1528,11 +1632,16 @@ namespace Barotrauma OwnedItems.Clear(); // Add items on the sub(s) - Submarine.MainSub?.GetItems(true) - .Where(i => i.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached) && - i.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null)) && - ItemAndAllContainersInteractable(i)) - .ForEach(i => AddToOwnedItems(i.Prefab)); + if (Submarine.MainSub?.GetItems(true) is List subItems) + { + foreach (var subItem in subItems) + { + if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } + if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } + if (!ItemAndAllContainersInteractable(subItem)) { continue; } + AddToOwnedItems(subItem.Prefab); + } + } // Add items in character inventories foreach (var item in Item.ItemList) @@ -1664,14 +1773,22 @@ namespace Barotrauma private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { - var list = mode switch + List list = null; + try { - StoreTab.Buy => CurrentLocation.StoreStock, - StoreTab.Sell => itemsToSell, - StoreTab.SellFromSub => itemsToSellFromSub, - _ => throw new NotImplementedException() - }; - if (list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) + list = mode switch + { + StoreTab.Buy => CurrentLocation?.StoreStock, + StoreTab.Sell => itemsToSell, + StoreTab.SellSub => itemsToSellFromSub, + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + } + if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { if (mode == StoreTab.Buy) { @@ -1691,8 +1808,8 @@ namespace Barotrauma private bool ModifyBuyQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasBuyPermissions) { return false; } if (quantity > 0) { var itemInCrate = CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab); @@ -1703,13 +1820,13 @@ namespace Barotrauma } CargoManager.ModifyItemQuantityInBuyCrate(item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); - return false; + return true; } private bool ModifySellQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasSellInventoryPermissions) { return false; } if (quantity > 0) { // Make sure there's enough available to sell @@ -1718,45 +1835,68 @@ namespace Barotrauma if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; } } CargoManager.ModifyItemQuantityInSellCrate(item.ItemPrefab, quantity); - //GameMain.Client?.SendCampaignState(); - return false; + return true; } private bool ModifySellFromSubQuantity(PurchasedItem item, int quantity) { - if (item == null || item.ItemPrefab == null) { return false; } - if (!HasPermissions) { return false; } + if (item?.ItemPrefab == null) { return false; } + if (!HasSellSubPermissions) { return false; } if (quantity > 0) { // Make sure there's enough available to sell var itemToSell = CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; - if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellFromSub)) { return false; } + if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellSub)) { return false; } } CargoManager.ModifyItemQuantityInSellFromSubCrate(item.ItemPrefab, quantity); - // TODO: GameMain.Client?.SendCampaignState(); - return false; + GameMain.Client?.SendCampaignState(); + return true; } - private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) => activeTab switch + private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) { - StoreTab.Buy => ModifyBuyQuantity(item, quantity), - StoreTab.Sell => ModifySellQuantity(item, quantity), - StoreTab.SellFromSub => ModifySellFromSubQuantity(item, quantity), - _ => throw new NotImplementedException(), - }; + if (item == null) { return false; } + try + { + return activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, quantity), + StoreTab.Sell => ModifySellQuantity(item, quantity), + StoreTab.SellSub => ModifySellFromSubQuantity(item, quantity), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error adding an item to the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } + } - private bool ClearFromShoppingCrate(PurchasedItem item) => activeTab switch + private bool ClearFromShoppingCrate(PurchasedItem item) { - StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity), - StoreTab.Sell => ModifySellQuantity(item, -item.Quantity), - StoreTab.SellFromSub => ModifySellFromSubQuantity(item, -item.Quantity), - _ => throw new NotImplementedException(), - }; + if (item == null) { return false; } + try + { + return activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity), + StoreTab.Sell => ModifySellQuantity(item, -item.Quantity), + StoreTab.SellSub => ModifySellFromSubQuantity(item, -item.Quantity), + _ => throw new NotImplementedException(), + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } + } private bool BuyItems() { - if (!HasPermissions) { return false; } + if (!HasBuyPermissions) { return false; } var itemsToPurchase = new List(CargoManager.ItemsInBuyCrate); var itemsToRemove = new List(); @@ -1788,15 +1928,24 @@ namespace Barotrauma private bool SellItems() { - if (!HasPermissions) { return false; } - var itemsToSell = activeTab switch + if (!HasActiveTabPermissions()) { return false; } + List itemsToSell; + try { - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), - _ => throw new NotImplementedException() - }; + itemsToSell = activeTab switch + { + StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), + StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return false; + } var itemsToRemove = new List(); - var totalValue = 0; + int totalValue = 0; foreach (PurchasedItem item in itemsToSell) { if (item?.ItemPrefab?.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) @@ -1811,11 +1960,7 @@ namespace Barotrauma itemsToRemove.ForEach(i => itemsToSell.Remove(i)); if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } CargoManager.SellItems(itemsToSell, activeTab); - if (activeTab == StoreTab.Sell) - { - // TODO: Implement selling sub items in multiplayer - GameMain.Client?.SendCampaignState(); - } + GameMain.Client?.SendCampaignState(); return false; } @@ -1831,7 +1976,7 @@ namespace Barotrauma int total = activeTab switch { StoreTab.Sell => sellTotal, - StoreTab.SellFromSub => sellFromSubTotal, + StoreTab.SellSub => sellFromSubTotal, _ => throw new NotImplementedException(), }; shoppingCrateTotal.Text = GetCurrencyFormatted(total); @@ -1863,21 +2008,29 @@ namespace Barotrauma } } - private void SetConfirmButtonStatus() => confirmButton.Enabled = - HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && - activeTab switch - { - StoreTab.Buy => buyTotal <= PlayerMoney, - StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, - StoreTab.SellFromSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, - _ => throw new NotImplementedException(), - }; + private void SetConfirmButtonStatus() + { + confirmButton.Enabled = + HasActiveTabPermissions() && + ActiveShoppingCrateList.Content.RectTransform.Children.Any() && + activeTab switch + { + StoreTab.Buy => buyTotal <= PlayerMoney, + StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, + StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, + _ => false + }; + } - private void SetClearAllButtonStatus() => clearAllButton.Enabled = - HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any(); + private void SetClearAllButtonStatus() + { + clearAllButton.Enabled = + HasActiveTabPermissions() && + ActiveShoppingCrateList.Content.RectTransform.Children.Any(); + } private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f; - private readonly float timerUpdateInterval = 1.5f; + private const float timerUpdateInterval = 1.5f; public void Update(float deltaTime) { @@ -1914,10 +2067,10 @@ namespace Barotrauma if (needsItemsToSellRefresh) { RefreshItemsToSell(); } if (needsItemsToSellFromSubRefresh) { RefreshItemsToSellFromSub(); } - if (needsRefresh || hadPermissions != HasPermissions) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsBuyingRefresh) { RefreshBuying(); } - if (needsSellingRefresh) { RefreshSelling(); } - if (needsSellingFromSubRefresh) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + if (needsRefresh || HavePermissionsChanged()) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } + if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); } + if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) { RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 0536aa334..258c55b37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -167,6 +167,7 @@ namespace Barotrauma //TODO: move this somewhere else public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine? drawnSubmarine, IEnumerable applicableCategories) { + var subItems = GetSubItems(); foreach (GUIComponent component in categoryList.Content.Children) { if (!(component.UserData is CategoryData data)) { continue; } @@ -178,7 +179,7 @@ namespace Barotrauma var customizeButton = component.FindChild("customizebutton", true); if (customizeButton != null) { - customizeButton.Visible = HasSwappableItems(data.Category); + customizeButton.Visible = HasSwappableItems(data.Category, subItems); } } @@ -719,16 +720,19 @@ namespace Barotrauma private bool customizeTabOpen; - private static bool HasSwappableItems(UpgradeCategory category) + private static bool HasSwappableItems(UpgradeCategory category, List? subItems = null) { if (Submarine.MainSub == null) { return false; } - return Submarine.MainSub.GetItems(true).Any(i => + subItems ??= GetSubItems(); + return subItems.Any(i => i.Prefab.SwappableItem != null && !i.HiddenInGame && i.AllowSwapping && (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); } + private static List GetSubItems() => Submarine.MainSub?.GetItems(true) ?? new List(); + private void SelectUpgradeCategory(List prefabs, UpgradeCategory category, Submarine submarine) { if (selectedUpgradeCategoryLayout == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index f64fef7c7..2d8f290ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using System; using System.Collections.Generic; using System.Linq; @@ -7,37 +8,6 @@ namespace Barotrauma { partial class CargoManager { - private class SoldEntity - { - public enum SellStatus - { - /// - /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. - /// - Confirmed, - /// - /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. - /// - Unconfirmed, - /// - /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. - /// - Local - } - - public Item Item { get; } - public SellStatus Status { get; set; } - - private SoldEntity(Item item, SellStatus status) - { - Item = item; - Status = status; - } - - public static SoldEntity CreateInSinglePlayer(Item item) => new SoldEntity(item, SellStatus.Confirmed); - public static SoldEntity CreateInMultiPlayer(Item item) => new SoldEntity(item, SellStatus.Local); - } - private List SoldEntities { get; } = new List(); // The bag slot is intentionally left out since we want to be able to sell items from there @@ -67,31 +37,6 @@ namespace Barotrauma } } - public IEnumerable GetSellableItemsFromSub() - { - if (Submarine.MainSub == null) { return new List(); } - var confirmedSoldEntities = GetConfirmedSoldEntities(); - return Submarine.MainSub.GetItems(true).FindAll(item => - { - if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } - if (item.GetRootInventoryOwner() is Character) { return false; } - if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } - if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } - if (!ItemAndAllContainersInteractable(item)) { return false; } - return true; - }).Distinct(); - - static bool ItemAndAllContainersInteractable(Item item) - { - do - { - if (!item.IsPlayerTeamInteractable) { return false; } - item = item.Container; - } while (item != null); - return true; - } - } - private IEnumerable GetConfirmedSoldEntities() { // Only consider items which have been: @@ -100,24 +45,6 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } - private bool IsItemSellable(Item item, IEnumerable confirmedSoldEntities) - { - if (!item.Prefab.CanBeSold) { return false; } - if (item.SpawnedInCurrentOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - if (item.OwnInventory?.Container is ItemContainer itemContainer) - { - var containedItems = item.ContainedItems; - if (containedItems.None()) { return true; } - // Allow selling the item if contained items are unsellable and set to be removed on deconstruct - if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } - // Otherwise there must be no contained items or the contained items must be confirmed as sold - if (!containedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } - } - return true; - } - public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); @@ -125,15 +52,21 @@ namespace Barotrauma OnItemsInBuyCrateChanged?.Invoke(); } + public void SetItemsInSubSellCrate(List items) + { + ItemsInSellFromSubCrate.Clear(); + ItemsInSellFromSubCrate.AddRange(items); + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + public void SetSoldItems(List items) { SoldItems.Clear(); SoldItems.AddRange(items); - - foreach (SoldEntity se in SoldEntities) + foreach (var se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } - if (SoldItems.Any(si => si.ID == se.Item.ID && si.ItemPrefab == se.Item.Prefab && (GameMain.Client == null || GameMain.Client.ID == si.SellerID))) + if (SoldItems.Any(si => Match(si, se, true))) { se.Status = SoldEntity.SellStatus.Confirmed; } @@ -142,13 +75,28 @@ namespace Barotrauma se.Status = SoldEntity.SellStatus.Unconfirmed; } } - + foreach (var si in SoldItems) + { + if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; } + if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; } + if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; } + soldEntityMatch.SetItem(item); + soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed; + } OnSoldItemsChanged?.Invoke(); + + static bool Match(SoldItem soldItem, SoldEntity soldEntity, bool matchId) + { + if (soldItem.ItemPrefab != soldEntity.ItemPrefab) { return false; } + if (matchId && (soldEntity.Item == null || soldItem.ID != soldEntity.Item.ID)) { return false; } + if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.ID) { return false; } + return true; + } } public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) { - PurchasedItem itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); + var itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemToSell != null) { itemToSell.Quantity += changeInQuantity; @@ -186,74 +134,69 @@ namespace Barotrauma public void SellItems(List itemsToSell, Store.StoreTab sellingMode) { - var sellableItems = sellingMode switch + IEnumerable sellableItems; + try { - Store.StoreTab.Sell => GetSellableItems(Character.Controlled), - Store.StoreTab.SellFromSub => GetSellableItemsFromSub(), - _ => throw new System.NotImplementedException(), - }; + sellableItems = sellingMode switch + { + Store.StoreTab.Sell => GetSellableItems(Character.Controlled), + Store.StoreTab.SellSub => GetSellableItemsFromSub(), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error selling items: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + return; + } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; - var sellerId = GameMain.Client?.ID ?? 0; - + byte sellerId = GameMain.Client?.ID ?? 0; // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in itemsToSell) { - var itemValue = item.Quantity * sellValues[item.ItemPrefab]; - + int itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item if (Location.StoreCurrentBalance < itemValue) { continue; } - // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab); - if (matchingItems.Count() <= item.Quantity) + int count = Math.Min(item.Quantity, matchingItems.Count()); + SoldItem.SellOrigin origin = sellingMode == Store.StoreTab.Sell ? SoldItem.SellOrigin.Character : SoldItem.SellOrigin.Submarine; + if (origin == SoldItem.SellOrigin.Character || GameMain.IsSingleplayer) { - foreach (Item i in matchingItems) + for (int i = 0; i < count; i++) { - SoldItems.Add(new SoldItem(i.Prefab, i.ID, canAddToRemoveQueue, sellerId)); - SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(i) : SoldEntity.CreateInMultiPlayer(i)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(i); } + var matchingItem = matchingItems.ElementAt(i); + SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); + SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); + if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } } } else { - for (int i = 0; i < item.Quantity; i++) + // When selling from the sub in multiplayer, the server will determine the items that are sold + for (int i = 0; i < count; i++) { - var matchingItem = matchingItems.ElementAt(i); - SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId)); - SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(matchingItem) : SoldEntity.CreateInMultiPlayer(matchingItem)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } + SoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); + SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local)); } } - // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; - GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); // Remove from the sell crate - // TODO: Simplify duplicate logic? - if (sellingMode == Store.StoreTab.Sell && ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } inventoryItem) + if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { - inventoryItem.Quantity -= item.Quantity; - if (inventoryItem.Quantity < 1) + itemToSell.Quantity -= item.Quantity; + if (itemToSell.Quantity < 1) { - ItemsInSellCrate.Remove(inventoryItem); - } - } - else if(sellingMode == Store.StoreTab.SellFromSub && ItemsInSellFromSubCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } subItem) - { - subItem.Quantity -= item.Quantity; - if (subItem.Quantity < 1) - { - ItemsInSellFromSubCrate.Remove(subItem); + (sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Remove(itemToSell); } } } - OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 38d4ab93f..845af7489 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -145,12 +145,14 @@ namespace Barotrauma string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); // add to local history ChatBox.ChatManager.Store(text); + WifiComponent headset = null; + ChatMessageType messageType = + ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled, out headset)) ? ChatMessageType.Radio : ChatMessageType.Default; AddSinglePlayerChatMessage( Character.Controlled.Info.Name, - msg, - ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled)) ? ChatMessageType.Radio : ChatMessageType.Default, + msg, messageType, Character.Controlled); - if (ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent headset)) + if (messageType == ChatMessageType.Radio && headset != null) { Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item); headset.TransmitSignal(s, sentFromChat: true); @@ -819,7 +821,7 @@ namespace Barotrauma } else { - OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -836,7 +838,7 @@ namespace Barotrauma } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -2533,6 +2535,7 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); } + shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIFrame(new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { @@ -2573,7 +2576,7 @@ namespace Barotrauma private void CreateOrderNodes(OrderCategory orderCategory) { - var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport); + var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, @@ -2725,6 +2728,7 @@ namespace Barotrauma contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); } } + contextualOrders.RemoveAll(o => !IsOrderAvailable(o.Order)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) @@ -3483,6 +3487,20 @@ namespace Barotrauma return character?.Info?.GetManualOrderPriority(order) ?? CharacterInfo.HighestManualOrderPriority; } + private bool IsOrderAvailable(Order order) + { + if (order == null) { return false; } + switch (order.Identifier.ToLowerInvariant()) + { + case "assaultenemy": + Character character = characterContext ?? Character.Controlled; + if (character?.Submarine == null) { return false; } + return character.Submarine.GetConnectedSubs().Any(s => s.TeamID != character.TeamID); + default: + return true; + } + } + #region Crew Member Assignment Logic private bool CanOpenManualAssignment(GUIComponent node) { @@ -3559,7 +3577,7 @@ namespace Barotrauma bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); ToggleReportButton("reportbreach", hasLeaks); - bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled)); + bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled, false)); ToggleReportButton("reportintruders", hasIntruders); foreach (GUIComponent reportButton in ReportButtonFrame.Children) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 32be3708e..6fa04747c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -97,17 +97,16 @@ namespace Barotrauma /// /// There is a server-side implementation of the method in /// - public bool AllowedToManageCampaign() + public bool AllowedToManageCampaign(ClientPermissions permissions = ClientPermissions.ManageCampaign) { - //allow ending the round if the client has permissions, is the owner, the only client in the server, + //allow managing the round if the client has permissions, is the owner, the only client in the server, //or if no-one has management permissions if (GameMain.Client == null) { return true; } return - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Client.HasPermission(permissions) || GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || - GameMain.Client.ConnectedClients.None(c => - c.InGame && (c.IsOwner || c.HasPermission(ClientPermissions.ManageCampaign))); + GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); } public override void Draw(SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 3b1fc8b2b..167fab3e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -545,14 +545,21 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); + } + + msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); + foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) + { + msg.Write(pi.ItemPrefab.Identifier); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.SoldItems.Count); @@ -562,6 +569,7 @@ namespace Barotrauma msg.Write((UInt16)si.ID); msg.Write(si.Removed); msg.Write(si.SellerID); + msg.Write((byte)si.Origin); } msg.Write((ushort)UpgradeManager.PurchasedUpgrades.Count); @@ -640,6 +648,15 @@ namespace Barotrauma buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } + UInt16 subSellCrateItemCount = msg.ReadUInt16(); + List subSellCrateItems = new List(); + for (int i = 0; i < subSellCrateItemCount; i++) + { + string itemPrefabIdentifier = msg.ReadString(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + } + UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) @@ -657,7 +674,8 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId)); + byte origin = msg.ReadByte(); + soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); } ushort pendingUpgradeCount = msg.ReadUInt16(); @@ -678,13 +696,9 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; - string itemToInstallIdentifier = msg.ReadString(); ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - - if (itemToRemove == null) { continue; } - + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } @@ -728,12 +742,11 @@ namespace Barotrauma campaign.Map.SelectMission(selectedMissionIndices); campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); + campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.CargoManager.SetSoldItems(soldItems); if (storeBalance.HasValue) { campaign.Map.CurrentLocation.StoreCurrentBalance = storeBalance.Value; } campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); - campaign.UpgradeManager.PurchasedUpgrades.Clear(); foreach (var purchasedItemSwap in purchasedItemSwaps) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 16a2fa540..c02cb3613 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1116,6 +1116,7 @@ namespace Barotrauma.Items.Components foreach (DockingPort dockingPort in DockingPort.List) { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (dockingPort.Item.HiddenInGame) { continue; } if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index be62c6110..ee079c02a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -682,7 +682,7 @@ namespace Barotrauma.Items.Components enterOutpostPrompt?.Close(); } } - else if (DockingSources.Any(d => d.Docked)) + else if (connectedPorts.Any(d => d.Docked)) { dockingButton.Text = undockText; dockingContainer.Visible = true; @@ -819,7 +819,7 @@ namespace Barotrauma.Items.Components Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); + connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true); } checkConnectedPortsTimer = CheckConnectedPortsInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 436e99507..e98bf3f58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -199,6 +199,26 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { snapped = msg.ReadBoolean(); + + if (!snapped) + { + UInt16 targetId = msg.ReadUInt16(); + UInt16 sourceId = msg.ReadUInt16(); + byte limbIndex = msg.ReadByte(); + + Item target = Entity.FindEntityByID(targetId) as Item; + if (target == null) { return; } + var source = Entity.FindEntityByID(sourceId); + if (source is Character sourceCharacter && limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length) + { + Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; + Attach(sourceLimb, target); + } + else if (source is ISpatialEntity spatialEntity) + { + Attach(spatialEntity, target); + } + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index e0b26ad02..f4b055a39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -135,7 +135,13 @@ namespace Barotrauma.Items.Components wireSprite = overrideSprite ?? defaultWireSprite; } + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + Draw(spriteBatch, editing, Vector2.Zero, itemDepth); + } + + public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float itemDepth = -1) { if (sections.Count == 0 && !IsActive || Hidden) { @@ -156,6 +162,8 @@ namespace Barotrauma.Items.Components drawOffset = sub.DrawPosition + sub.HiddenSubPosition; } + drawOffset += offset; + float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index ffeed153b..fa70e6f8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; namespace Barotrauma { @@ -208,9 +207,9 @@ namespace Barotrauma "watersplash", (Submarine == null ? pos : pos + Submarine.Position) - Vector2.UnitY * Rand.Range(0.0f, 10.0f), velocity, 0, flowTargetHull); - if (particle != null) { + if (particle.CurrentHull == null) { GameMain.ParticleManager.RemoveParticle(particle); } particle.Size *= Math.Min(Math.Abs(flowForce.X / 500.0f), 5.0f); } } @@ -238,9 +237,9 @@ namespace Barotrauma } else { - if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) return; + if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) { return; } - float particlesPerSec = open * rect.Width * 0.3f * particleAmountMultiplier; + float particlesPerSec = Math.Max(open * rect.Width * 0.3f * particleAmountMultiplier, 20.0f); float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { @@ -252,17 +251,21 @@ namespace Barotrauma if (flowTargetHull.WaterVolume < flowTargetHull.Volume * 0.95f) { var splash = GameMain.ParticleManager.CreateParticle( - "watersplash", - Submarine == null ? pos : pos + Submarine.Position, - velocity, 0, FlowTargetHull); - if (splash != null) splash.Size = splash.Size * MathHelper.Clamp(rect.Width / 50.0f, 0.8f, 4.0f); + "watersplash", + Submarine == null ? pos : pos + Submarine.Position, + velocity, 0, FlowTargetHull); + if (splash != null) + { + if (splash.CurrentHull == null) { GameMain.ParticleManager.RemoveParticle(splash); } + splash.Size *= MathHelper.Clamp(rect.Width / 50.0f, 1.5f, 4.0f); + } } if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { GameMain.ParticleManager.CreateParticle( - "bubbles", - Submarine == null ? pos : pos + Submarine.Position, - flowForce / 2.0f, 0, FlowTargetHull); + "bubbles", + Submarine == null ? pos : pos + Submarine.Position, + flowForce / 2.0f, 0, FlowTargetHull); } particleTimer -= emitInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index d1d15b1c9..6a01d168d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -37,6 +37,19 @@ namespace Barotrauma private double lastAmbientLightEditTime; + private float drawSurface; + + public float DrawSurface + { + get { return drawSurface; } + set + { + if (Math.Abs(drawSurface - value) < 0.00001f) { return; } + drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y); + update = true; + } + } + public override bool SelectableInEditor { get @@ -138,8 +151,15 @@ namespace Barotrauma } } - partial void UpdateProjSpecific(float deltaTime, Camera cam) + partial void UpdateProjSpecific(float deltaTime, Camera _) { + float waterDepth = WaterVolume / rect.Width; + //interpolate the position of the rendered surface towards the "target surface" + drawSurface = Math.Max(MathHelper.Lerp( + drawSurface, + rect.Y - rect.Height + waterDepth, + deltaTime * 10.0f), rect.Y - rect.Height); + if (GameMain.Client != null) { serverUpdateDelay -= deltaTime; @@ -171,55 +191,56 @@ namespace Barotrauma } } - if (!IdFreed) - { - if (EditWater) - { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) - { - if (PlayerInput.PrimaryMouseButtonHeld()) - { - WaterVolume += 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; - } - else if (PlayerInput.SecondaryMouseButtonHeld()) - { - WaterVolume -= 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; - } - } - } - else if (EditFire) - { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) - { - if (PlayerInput.PrimaryMouseButtonClicked()) - { - new FireSource(position, this, isNetworkMessage: true); - networkUpdatePending = true; - serverUpdateDelay = 0.5f; - } - } - } - } - - if (waterVolume < 1.0f) { return; } + /*if (waterVolume < 1.0f) { return; } for (int i = 1; i < waveY.Length - 1; i++) { float maxDelta = Math.Max(Math.Abs(rightDelta[i]), Math.Abs(leftDelta[i])); - if (maxDelta > 1.0f && maxDelta > Rand.Range(1.0f, 10.0f)) + if (maxDelta > 0.1f && maxDelta > Rand.Range(0.1f, 10.0f)) { var particlePos = new Vector2(rect.X + WaveWidth * i, surface + waveY[i]); - if (Submarine != null) particlePos += Submarine.Position; + if (Submarine != null) { particlePos += Submarine.Position; } GameMain.ParticleManager.CreateParticle("mist", particlePos, new Vector2(0.0f, -50.0f), 0.0f, this); } + }*/ + } + + public static void UpdateCheats(float deltaTime, Camera cam) + { + bool primaryMouseButtonHeld = PlayerInput.PrimaryMouseButtonHeld(); + bool secondaryMouseButtonHeld = PlayerInput.SecondaryMouseButtonHeld(); + if (!primaryMouseButtonHeld && !secondaryMouseButtonHeld) { return; } + + Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); + Hull hull = FindHull(position); + + if (hull == null || hull.IdFreed) { return; } + if (EditWater) + { + if (primaryMouseButtonHeld) + { + hull.WaterVolume += 100000.0f * deltaTime; + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } + else if (secondaryMouseButtonHeld) + { + hull.WaterVolume -= 100000.0f * deltaTime; + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } + + } + else if (EditFire) + { + if (primaryMouseButtonHeld) + { + new FireSource(position, hull, isNetworkMessage: true); + hull.networkUpdatePending = true; + hull.serverUpdateDelay = 0.5f; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 91eb1ce2a..0104d2e1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -194,7 +194,11 @@ namespace Barotrauma private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { if (location == null) { return; } - Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; + + var mapTile = generationParams.MapTiles.Values.FirstOrDefault()?.FirstOrDefault(); + if (mapTile == null) { return; } + + Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale; int startX = (int)Math.Max(Math.Floor(location.MapPosition.X / mapTileSize.X - 0.25f), 0); int startY = (int)Math.Max(Math.Floor(location.MapPosition.Y / mapTileSize.Y - 0.25f), 0); int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index c92f51088..0d93a3efe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -768,34 +768,40 @@ namespace Barotrauma switch (e) { case Item item: - { - if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - break; - } + { + if (item.FlippedX && item.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } + if (item.flippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + var wire = item.GetComponent(); + if (wire != null && !wire.Item.body.Enabled) + { + wire.Draw(spriteBatch, editing: false, new Vector2(moveAmount.X, -moveAmount.Y)); + continue; + } + break; + } case Structure structure: - { - if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - break; - } + { + if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } + if (structure.flippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + break; + } case WayPoint wayPoint: - { - Vector2 drawPos = e.WorldPosition; - drawPos.Y = -drawPos.Y; - drawPos += moveAmount; - wayPoint.Draw(spriteBatch, drawPos); - continue; - } + { + Vector2 drawPos = e.WorldPosition; + drawPos.Y = -drawPos.Y; + drawPos += moveAmount; + wayPoint.Draw(spriteBatch, drawPos); + continue; + } case LinkedSubmarine linkedSub: - { - var ma = moveAmount; - ma.Y = -ma.Y; - Vector2 lPos = linkedSub.Position; - lPos += ma; - linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); - break; - } + { + var ma = moveAmount; + ma.Y = -ma.Y; + Vector2 lPos = linkedSub.Position; + lPos += ma; + linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); + break; + } } e.prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index c50ae90a9..2977533a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma @@ -91,7 +91,7 @@ namespace Barotrauma public void CreateSpecsWindow(GUIListBox parent, ScalableFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) { float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f; + float rightPanelWidth = 0.4f / leftPanelWidth; string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; @@ -110,6 +110,17 @@ namespace Barotrauma submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); } + + if (Price > 0) + { + var priceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("subeditor.price"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), priceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", Price)), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + } + Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index f27c6daa0..bef065bce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2870,19 +2870,16 @@ namespace Barotrauma.Networking public void SendCampaignState() { - MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; - if (campaign == null) + if (!(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { DebugConsole.ThrowError("Failed send campaign state to the server (no campaign active).\n" + Environment.StackTrace.CleanupStackTrace()); return; } - IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageCampaign); campaign.ClientWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - clientPeer.Send(msg, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 441f71846..97099d85f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -196,6 +196,19 @@ namespace Barotrauma.Particles particles[particleCount] = swap; } + + public void RemoveParticle(Particle particle) + { + for (int i = 0; i < particleCount; i++) + { + if (particles[i] == particle) + { + RemoveParticle(i); + return; + } + } + } + public void Update(float deltaTime) { MaxParticles = GameMain.Config.ParticleLimit; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 445af25ff..2bc4bee2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -244,7 +244,11 @@ namespace Barotrauma foreach (string saveFile in saveFiles) { - if (string.IsNullOrEmpty(saveFile)) { continue; } + if (string.IsNullOrEmpty(saveFile)) + { + DebugConsole.AddWarning("Error when updating campaign load menu: path to a save file was empty.\n" + Environment.StackTrace); + continue; + } string fileName = saveFile; string subName = ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 32e05dfa4..5cc04acb3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -500,29 +500,34 @@ namespace Barotrauma.CharacterEditor int index = 0; bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow; bool isMovingFast = character.AnimController.ForceSelectAnimationType == AnimationType.Run || character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast; - if (isMovingFast) + if (character.AnimController.CanWalk) { - if (isSwimming || !character.AnimController.CanWalk) + if (isMovingFast) { - index = !character.AnimController.CanWalk ? (int)AnimationType.SwimFast : (int)AnimationType.SwimSlow; + if (isSwimming) + { + index = 2; + } + else + { + index = 0; + } } else { - index = (int)AnimationType.Walk; + if (isSwimming) + { + index = 3; + } + else + { + index = 1; + } } - index -= 1; } else { - if (isSwimming || !character.AnimController.CanWalk) - { - index = !character.AnimController.CanWalk ? (int)AnimationType.SwimSlow : (int)AnimationType.SwimFast; - } - else - { - index = (int)AnimationType.Run; - } - index -= 1; + index = isMovingFast ? 0 : 1; } if (animSelection.SelectedIndex != index) { @@ -536,17 +541,13 @@ namespace Barotrauma.CharacterEditor bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow; if (isSwimming) { - animSelection.Select((int)AnimationType.Walk - 1); + animSelection.Select(0); } else { - animSelection.Select((int)AnimationType.SwimSlow - 1); + animSelection.Select(2); } } - if (PlayerInput.KeyHit(Keys.F)) - { - SetToggle(freezeToggle, !freezeToggle.Selected); - } if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Keys.Escape)) { bool reset = false; @@ -853,6 +854,16 @@ namespace Barotrauma.CharacterEditor { DrawRagdoll(spriteBatch, (float)deltaTime); } + // Mouth + Limb head = character.AnimController.GetLimb(LimbType.Head); + if (head != null && character.CanEat && selectedLimbs.Contains(head)) + { + var mouthPos = character.AnimController.GetMouthPosition(); + if (mouthPos.HasValue) + { + ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUI.Style.Red, size: 8); + } + } if (showSpritesheet) { DrawSpritesheetEditor(spriteBatch, (float)deltaTime); @@ -2606,13 +2617,13 @@ namespace Barotrauma.CharacterEditor { animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk); animSelection.AddItem(AnimationType.Run.ToString(), AnimationType.Run); - if (character.IsHumanoid) - { - animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); - } } animSelection.AddItem(AnimationType.SwimSlow.ToString(), AnimationType.SwimSlow); animSelection.AddItem(AnimationType.SwimFast.ToString(), AnimationType.SwimFast); + if (character.AnimController.CanWalk && character.IsHumanoid) + { + animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); + } if (character.AnimController.ForceSelectAnimationType == AnimationType.NotDefined) { animSelection.SelectItem(character.AnimController.CanWalk ? AnimationType.Walk : AnimationType.SwimSlow); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index c5b3ff6b7..e013965ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -717,7 +717,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 40, LevelGenerationParams levelGenerationParams = null) + public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) { @@ -1396,7 +1396,7 @@ namespace Barotrauma return false; } - if (ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) + if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) { var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("forbiddenservernameverification", new string[] { "[forbiddenword]", "[servername]" }, new string[] { forbiddenWord, name }), diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index d22c31aa4..ca56a06bf 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 4261f3b2e..67d428f69 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 923513bc1..9fbc4f6b4 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index d71e5fa62..b3c348211 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index d4d5886f3..0c3d83771 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 7dad8ea2b..1ff909090 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1841,7 +1841,7 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Vector2 explosionPos = cursorWorldPos; - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; + float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 90d2abec4..1fe87b072 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -23,10 +24,10 @@ namespace Barotrauma { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction - Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); - foreach (SoldItem item in itemsToBuy) + var sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); + foreach (var item in itemsToBuy) { - var itemValue = sellValues[item.ItemPrefab]; + int itemValue = sellValues[item.ItemPrefab]; if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } Location.StoreCurrentBalance += itemValue; campaign.Money -= itemValue; @@ -36,17 +37,29 @@ namespace Barotrauma public void SellItems(List itemsToSell) { + bool canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; + IEnumerable sellableItemsInSub = Enumerable.Empty(); + if (canAddToRemoveQueue && itemsToSell.Any(i => i.Origin == SoldItem.SellOrigin.Submarine && i.ID == Entity.NullEntityID && !i.Removed)) + { + sellableItemsInSub = GetSellableItemsFromSub(); + } // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction - Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - var canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; - foreach (SoldItem item in itemsToSell) + var sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); + foreach (var item in itemsToSell) { - var itemValue = sellValues[item.ItemPrefab]; - + int itemValue = sellValues[item.ItemPrefab]; // check if the store can afford the item and if the item hasn't been removed already if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } - + // Server determines the items that are sold from the sub in multiplayer + if (item.Origin == SoldItem.SellOrigin.Submarine && item.ID == Entity.NullEntityID && !item.Removed) + { + var matchingItem = sellableItemsInSub.FirstOrDefault(i => !i.Removed && i.Prefab == item.ItemPrefab && + itemsToSell.None(itemToSell => itemToSell.ItemPrefab == i.Prefab && itemToSell.ID == i.ID)); + // This is a failsafe for scenarios where a client is trying to sell more items than there's available on the sub + if (matchingItem == null) { continue; } + item.SetItemId(matchingItem.ID); + } if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity) { item.Removed = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 1af2f9dc4..1bec8f2e2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -166,16 +166,15 @@ namespace Barotrauma /// /// There is a client-side implementation of the method in /// - public bool AllowedToManageCampaign(Client client) + public bool AllowedToManageCampaign(Client client, ClientPermissions permissions = ClientPermissions.ManageCampaign) { - //allow ending the round if the client has permissions, is the owner, or the only client in the server, + //allow managing the campaign if the client has permissions, is the owner, or the only client in the server, //or if no-one has management permissions return - client.HasPermission(ClientPermissions.ManageCampaign) || + client.HasPermission(permissions) || GameMain.Server.ConnectedClients.Count == 1 || IsOwner(client) || - GameMain.Server.ConnectedClients.None(c => - c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign))); + GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); } public void SaveExperiencePoints(Client client) @@ -562,14 +561,21 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); + } + + msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); + foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) + { + msg.Write(pi.ItemPrefab.Identifier); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.PurchasedItems.Count); foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, 100); + msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); } msg.Write((UInt16)CargoManager.SoldItems.Count); @@ -579,6 +585,7 @@ namespace Barotrauma msg.Write((UInt16)si.ID); msg.Write(si.Removed); msg.Write(si.SellerID); + msg.Write((byte)si.Origin); } msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); @@ -633,6 +640,15 @@ namespace Barotrauma buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } + UInt16 subSellCrateItemCount = msg.ReadUInt16(); + List subSellCrateItems = new List(); + for (int i = 0; i < subSellCrateItemCount; i++) + { + string itemPrefabIdentifier = msg.ReadString(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + } + UInt16 purchasedItemCount = msg.ReadUInt16(); List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) @@ -650,7 +666,8 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId)); + byte origin = msg.ReadByte(); + soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); } ushort purchasedUpgradeCount = msg.ReadUInt16(); @@ -674,125 +691,146 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; - string itemToInstallIdentifier = msg.ReadString(); ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - - if (itemToRemove == null) { continue; } - + if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } - if (!AllowedToManageCampaign(sender)) + bool allowedToManageCampaign = AllowedToManageCampaign(sender); + if (AllowedToManageCampaign(sender)) { - DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign"); - return; - } - - Location location = Map.CurrentLocation; - int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; - int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; - int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; - - if (purchasedHullRepairs != this.PurchasedHullRepairs) - { - if (purchasedHullRepairs && Money >= hullRepairCost) + Location location = Map.CurrentLocation; + int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; + int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; + int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; + if (purchasedHullRepairs != this.PurchasedHullRepairs) { - this.PurchasedHullRepairs = true; - Money -= hullRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + if (purchasedHullRepairs && Money >= hullRepairCost) + { + this.PurchasedHullRepairs = true; + Money -= hullRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + } + else if (!purchasedHullRepairs) + { + this.PurchasedHullRepairs = false; + Money += hullRepairCost; + } } - else if (!purchasedHullRepairs) + if (purchasedItemRepairs != this.PurchasedItemRepairs) { - this.PurchasedHullRepairs = false; - Money += hullRepairCost; + if (purchasedItemRepairs && Money >= itemRepairCost) + { + this.PurchasedItemRepairs = true; + Money -= itemRepairCost; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + } + else if (!purchasedItemRepairs) + { + this.PurchasedItemRepairs = false; + Money += itemRepairCost; + } } - } - if (purchasedItemRepairs != this.PurchasedItemRepairs) - { - if (purchasedItemRepairs && Money >= itemRepairCost) + if (purchasedLostShuttles != this.PurchasedLostShuttles) { - this.PurchasedItemRepairs = true; - Money -= itemRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) + { + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); + } + else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) + { + this.PurchasedLostShuttles = true; + Money -= shuttleRetrieveCost; + GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); + } + else if (!purchasedItemRepairs) + { + this.PurchasedLostShuttles = false; + Money += shuttleRetrieveCost; + } } - else if (!purchasedItemRepairs) + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) { - this.PurchasedItemRepairs = false; - Money += itemRepairCost; - } - } - if (purchasedLostShuttles != this.PurchasedLostShuttles) - { - if (GameMain.GameSession?.SubmarineInfo != null && - GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) - { - GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); - } - else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) - { - this.PurchasedLostShuttles = true; - Money -= shuttleRetrieveCost; - GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); - } - else if (!purchasedItemRepairs) - { - this.PurchasedLostShuttles = false; - Money += shuttleRetrieveCost; + Map.SetLocation(currentLocIndex); } + Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); + if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } + if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } + CheckTooManyMissions(Map.CurrentLocation, sender); } - if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) + bool allowedToUseStore = AllowedToManageCampaign(sender, ClientPermissions.CampaignStore); + if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { - Map.SetLocation(currentLocIndex); + var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); + currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); + buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); + CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); + CargoManager.PurchaseItems(purchasedItems, false); } - Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } - if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } - - CheckTooManyMissions(Map.CurrentLocation, sender); - - List currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); - - CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false); - - // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all - // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); - CargoManager.SellItems(soldItems); - - foreach (var (prefab, category, _) in purchasedUpgrades) + bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); + if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { - UpgradeManager.PurchaseUpgrade(prefab, category); - - // unstable logging - int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); - int level = UpgradeManager.GetUpgradeLevel(prefab, category); - GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); + var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); + currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity)); + subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity)); } - foreach (var purchasedItemSwap in purchasedItemSwaps) + bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); + if (allowedToManageCampaign || allowedToUseStore || (allowedToSellInventoryItems && allowedToSellSubItems)) { - if (purchasedItemSwap.ItemToInstall == null) + // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all + // sold items that are removed so they should be discarded on the next message + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); + CargoManager.SellItems(soldItems); + } + else if (allowedToSellInventoryItems || allowedToSellSubItems) + { + if (allowedToSellInventoryItems) { - UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); + soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); + soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } + CargoManager.SellItems(soldItems); } - foreach (Item item in Item.ItemList) + + if (allowedToManageCampaign) { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + foreach (var (prefab, category, _) in purchasedUpgrades) { - UpgradeManager.CancelItemSwap(item); - item.PendingItemSwap = null; + UpgradeManager.PurchaseUpgrade(prefab, category); + + // unstable logging + int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int level = UpgradeManager.GetUpgradeLevel(prefab, category); + GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); + } + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); + } + else + { + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + UpgradeManager.CancelItemSwap(item); + item.PendingItemSwap = null; + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index 16e3abf9a..e56702fc3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -7,6 +7,26 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(Snapped); + + if (!Snapped) + { + msg.Write(target?.ID ?? Entity.NullEntityID); + if (source is Entity entity && !entity.Removed) + { + msg.Write(entity?.ID ?? Entity.NullEntityID); + msg.Write((byte)0); + } + else if (source is Limb limb && limb.character != null && !limb.character.Removed) + { + msg.Write(limb.character?.ID ?? Entity.NullEntityID); + msg.Write((byte)limb.character.AnimController.Limbs.IndexOf(limb)); + } + else + { + msg.Write(Entity.NullEntityID); + msg.Write((byte)0); + } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 07117c718..68c431fbc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -98,22 +98,6 @@ namespace Barotrauma case NetEntityEvent.Type.AssignCampaignInteraction: msg.Write((byte)CampaignInteractionType); break; - case NetEntityEvent.Type.Treatment: - { - ItemComponent targetComponent = (ItemComponent)extraData[1]; - ActionType actionType = (ActionType)extraData[2]; - ushort targetID = (ushort)extraData[3]; - Limb targetLimb = (Limb)extraData[4]; - - Character targetCharacter = FindEntityByID(targetID) as Character; - byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; - - msg.Write((byte)components.IndexOf(targetComponent)); - msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); - msg.Write(targetID); - msg.Write(targetLimbIndex); - } - break; case NetEntityEvent.Type.ApplyStatusEffect: { ActionType actionType = (ActionType)extraData[1]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index dd99114fd..827171ade 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -27,8 +27,8 @@ namespace Barotrauma //don't create updates if all clients are very far from the hull float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; if (!GameMain.Server.ConnectedClients.Any(c => - c.Character != null && - Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) + c.Character != null && + Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index bc37d9df3..16829a04f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Networking string orderOption = orderMessageInfo.OrderOption ?? (orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]); - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) + orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character, isNewOrder: orderMessageInfo.IsNewOrder) { WallSectionIndex = wallSectionIndex }; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 38ae2e076..217d4e1d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -3154,11 +3154,11 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 18c2713cb..9d8d2aafb 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.1.0 + 0.16.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 8f16f561e..0dae570e8 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -77,8 +77,8 @@ + - @@ -113,6 +113,7 @@ + @@ -121,7 +122,9 @@ + + @@ -145,6 +148,8 @@ + + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index 7467a8269..5a78125cb 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -6,8 +6,8 @@ karmadecaythreshold="50" karmaincrease="0.05" karmaincreasethreshold="50" - structurerepairkarmaincrease="0.01" - structuredamagekarmadecrease="0.05" + structurerepairkarmaincrease="0.005" + structuredamagekarmadecrease="0.025" itemrepairkarmaincrease="0.03" reactoroverheatkarmadecrease="0.5" reactormeltdownkarmadecrease="30" @@ -35,8 +35,8 @@ karmadecaythreshold="50" karmaincrease="0.04" karmaincreasethreshold="45" - structurerepairkarmaincrease="0.01" - structuredamagekarmadecrease="0.2" + structurerepairkarmaincrease="0.005" + structuredamagekarmadecrease="0.1" itemrepairkarmaincrease="0.03" reactoroverheatkarmadecrease="1.0" reactormeltdownkarmadecrease="35" diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index eab4fbc2a..fe05e7a41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -531,8 +531,7 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } - if (SelectedAiTarget?.Entity != null && - (LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && + if ((LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { UpdateWallTarget(requiredHoleCount); @@ -646,7 +645,7 @@ namespace Barotrauma } else { - run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); + run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2); State = AIState.Escape; avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f); } @@ -674,7 +673,8 @@ namespace Barotrauma Character c = a.Character; if (c.IsDead || c.Removed) { return false; } if (!Character.IsFriendly(c)) { return true; } - // Only apply the threshold to friendly characters + if (!c.IsPlayer) { return false; } + // Only apply the threshold to players return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; @@ -686,6 +686,8 @@ namespace Barotrauma // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); + State = AIState.Attack; + UpdateWallTarget(requiredHoleCount); return; } } @@ -2270,6 +2272,10 @@ namespace Barotrauma if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; + if (Character.SelectedCharacter != null) + { + Character.DeselectCharacter(); + } return; } if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item) @@ -2285,7 +2291,16 @@ namespace Barotrauma Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); - if (limbDiff.LengthSquared() < extent * extent) + bool tooFar = Character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; + if (tooFar) + { + steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); + if (Character.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } + } + else { if (SelectedAiTarget.Entity is Character targetCharacter) { @@ -2301,11 +2316,9 @@ namespace Barotrauma { item.body.LinearVelocity *= 0.9f; item.body.LinearVelocity -= limbDiff * 0.25f; - bool wasBroken = item.Condition <= 0.0f; - - item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime); - + item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); + Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) { if (!wasBroken) { PetBehavior?.OnEat(item); } @@ -2317,14 +2330,6 @@ namespace Barotrauma steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); } - else - { - steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); - if (Character.AnimController.InWater) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - } - } } else { @@ -2392,7 +2397,7 @@ namespace Barotrauma selectedTargetMemory = null; targetingParams = null; bool isAnyTargetClose = false; - + bool isBeingChased = IsBeingChased; foreach (AITarget aiTarget in AITarget.List) { if (aiTarget.InDetectable) { continue; } @@ -2515,11 +2520,12 @@ namespace Barotrauma // Ignore inner walls when outside (walltargets still work) continue; } - valueModifier = 1; if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) { continue; } + // Prefer weaker walls (200 is the default for normal hull walls) + valueModifier = 200f / s.MaxHealth; for (int i = 0; i < s.Sections.Length; i++) { var section = s.Sections[i]; @@ -2674,6 +2680,10 @@ namespace Barotrauma } } } + if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0) + { + valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f); + } valueModifier *= targetParams.Priority; if (valueModifier == 0.0f) { continue; } if (targetingTag != "decoy") @@ -2720,9 +2730,21 @@ namespace Barotrauma // Stick to the current target valueModifier *= 1.1f; } + if (!isBeingChased) + { + if (targetParams.State == AIState.Avoid || targetParams.State == AIState.PassiveAggressive || targetParams.State == AIState.Aggressive) + { + float reactDistance = targetParams.ReactDistance; + if (reactDistance > 0 && reactDistance < dist) + { + // The target is too far and should be ignored. + continue; + } + } + } //if the target is very close, the distance doesn't make much difference - // -> just ignore the distance and attack whatever has the highest priority + // -> just ignore the distance and target whatever has the highest priority dist = Math.Max(dist, 100.0f); AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) @@ -2801,9 +2823,9 @@ namespace Barotrauma { if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull) { - if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe) + if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { - // Ignore targets that cannot see + // Ignore targets that cannot be seen if (!VisibleHulls.Contains(targetCharacter.CurrentHull)) { continue; @@ -3295,7 +3317,7 @@ namespace Barotrauma { if (priority.HasValue) { - targetParams.Priority = priority.Value; + targetParams.Priority = Math.Max(targetParams.Priority, priority.Value); } targetParams.State = state; if (!modifiedParams.ContainsKey(tag)) @@ -3314,6 +3336,7 @@ namespace Barotrauma /// /// Temporarily changes the predefined state for a target. Eg. Idle -> Attack. + /// Note: does not change the current AIState! /// private void ChangeTargetState(Character target, AIState state, float? priority = null) { @@ -3335,14 +3358,14 @@ namespace Barotrauma // --> Target the submarine too. if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls)) { - ChangeParams("room", state, priority * 0.1f); + ChangeParams("room", state, priority / 2); if (canAttackWalls) { - ChangeParams("wall", state, priority * 0.1f); + ChangeParams("wall", state, priority / 2); } if (canAttackDoors) { - ChangeParams("door", state, priority * 0.1f); + ChangeParams("door", state, priority / 2); } } ChangeParams("provocative", state, priority, onlyExisting: true); @@ -3394,9 +3417,15 @@ namespace Barotrauma private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false) { + if (target?.Entity == null) { return false; } bool insideSightRange; bool insideSoundRange; - checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine; + if (checkVisibility) + { + // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible. + // When the monsters attack the player sub, they wall hack so that they can be more aggressive. + checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; + } if (dist > 0) { insideSightRange = IsInRange(dist, target.SightRange, Sight); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 498f633aa..39c698d32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -565,7 +565,7 @@ namespace Barotrauma Character.AnimController.HeadInWater || Character.Submarine == null || (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || - !ObjectiveManager.IsCurrentObjective() && ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) || + ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; @@ -878,7 +878,7 @@ namespace Barotrauma foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull || !target.Enabled) { continue; } - if (AIObjectiveFightIntruders.IsValidTarget(target, Character)) + if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false)) { if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) { @@ -1772,7 +1772,7 @@ namespace Barotrauma foreach (var enemy in Character.CharacterList) { if (enemy.CurrentHull != hull) { continue; } - if (AIObjectiveFightIntruders.IsValidTarget(enemy, character)) + if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false)) { AddTargets(character, enemy); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index e5a71a3ed..24efa6f19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -9,10 +9,10 @@ namespace Barotrauma { class IndoorsSteeringManager : SteeringManager { - private PathFinder pathFinder; + private readonly PathFinder pathFinder; private SteeringPath currentPath; - private bool canOpenDoors; + private readonly bool canOpenDoors; public bool CanBreakDoors { get; set; } private bool ShouldBreakDoor(Door door) => @@ -20,7 +20,7 @@ namespace Barotrauma !door.Item.Indestructible && !door.Item.InvulnerableToDamage && (door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID); - private Character character; + private readonly Character character; private Vector2 currentTarget; @@ -77,8 +77,10 @@ namespace Barotrauma public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host) { - pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); - pathFinder.GetNodePenalty = GetNodePenalty; + pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true) + { + GetNodePenalty = GetNodePenalty + }; this.canOpenDoors = canOpenDoors; this.CanBreakDoors = canBreakDoors; @@ -508,6 +510,16 @@ namespace Barotrauma canAccessButtons = true; } } + foreach (var linked in door.Item.linkedTo) + { + if (!(linked is Item linkedItem)) { continue; } + var button = linkedItem.GetComponent(); + if (button == null) { continue; } + if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) + { + canAccessButtons = true; + } + } return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 3d07e452f..2a76c67aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -30,6 +30,8 @@ namespace Barotrauma public virtual bool ConcurrentObjectives => false; public virtual bool KeepDivingGearOn => false; + public virtual bool KeepDivingGearOnAlsoWhenInactive => false; + /// /// There's a separate property for diving suit and mask: KeepDivingGearOn. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index f4625b5bd..6cb902dbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -12,10 +12,12 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 0.2f; + public bool TargetCharactersInOtherSubs { get; set; } + public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) => IsValidTarget(target, character); + protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs); protected override IEnumerable GetList() => Character.CharacterList; @@ -54,7 +56,7 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Character target) => HumanAIController.RemoveTargets(character, target); - public static bool IsValidTarget(Character target, Character character) + public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs) { if (target == null || target.Removed) { return false; } if (target.IsDead) { return false; } @@ -65,7 +67,7 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - if (character.Submarine.TeamID != target.Submarine.TeamID) { return false; } + if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index d06835684..5f76c9663 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -629,18 +629,20 @@ namespace Barotrauma { get { - if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder) + if (character.IsClimbing) { - // Climbing a ladder - if (Target.WorldPosition.Y > character.WorldPosition.Y) + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder) { - // The target is still above us - return false; - } - if (!character.AnimController.IsAboveFloor) - { - // Going through a hatch - return false; + if (Target.WorldPosition.Y > character.WorldPosition.Y) + { + // The target is still above us + return false; + } + if (!character.AnimController.IsAboveFloor) + { + // Going through a hatch + return false; + } } } if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 95b96f372..44cc68d5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -422,7 +422,7 @@ namespace Barotrauma case "wait": newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { - AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine) + AllowGoingOutside = true }; break; case "return": @@ -468,6 +468,12 @@ namespace Barotrauma case "fightintruders": newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier); break; + case "assaultenemy": + newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier) + { + TargetCharactersInOtherSubs = true + }; + break; case "steer": var steering = (order?.TargetEntity as Item)?.GetComponent(); if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index c4b2e1d1c..9bea9ed11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override string Identifier { get; set; } = "prepare"; public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; + public override bool KeepDivingGearOnAlsoWhenInactive => true; public override bool PrioritizeIfSubObjectivesActive => true; private AIObjectiveGetItem getSingleItemObjective; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 1db23dee1..4bc797d54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -155,6 +155,9 @@ namespace Barotrauma public OrderCategory? Category { get; private set; } //legacy support + /// + /// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs. + /// public readonly string[] AppropriateJobs; public readonly string[] Options; public readonly string[] HiddenOptions; @@ -177,6 +180,10 @@ namespace Barotrauma public bool IsPrefab { get; private set; } public readonly bool MustManuallyAssign; public readonly bool AutoDismiss; + /// + /// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs. + /// + public string[] PreferredJobs { get; } public readonly OrderTarget TargetPosition; @@ -327,6 +334,7 @@ namespace Barotrauma ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); + PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); AllOptions = Options.Concat(HiddenOptions).ToArray(); @@ -407,7 +415,7 @@ namespace Barotrauma MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); - AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement); + AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement); AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false); DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false); @@ -435,6 +443,7 @@ namespace Barotrauma ControllerTags = prefab.ControllerTags; TargetAllCharacters = prefab.TargetAllCharacters; AppropriateJobs = prefab.AppropriateJobs; + PreferredJobs = prefab.PreferredJobs; FadeOutTime = prefab.FadeOutTime; MustSetTarget = prefab.MustSetTarget; CanBeGeneralized = prefab.CanBeGeneralized; @@ -446,8 +455,9 @@ namespace Barotrauma Hidden = prefab.Hidden; IgnoreAtOutpost = prefab.IgnoreAtOutpost; AssignmentPriority = prefab.AssignmentPriority; - ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; + AutoDismiss = prefab.AutoDismiss; DisplayGiverInTooltip = prefab.DisplayGiverInTooltip; + ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -488,30 +498,37 @@ namespace Barotrauma WallSectionIndex = sectionIndex; TargetType = OrderTargetType.WallSection; } - - public bool HasAppropriateJob(Character character) - { - if (character.Info == null || character.Info.Job == null) { return false; } - if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; } - if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) && - (AppropriateJobs == null || AppropriateJobs.Length == 0)) + private bool HasSpecifiedJob(Character character, string[] jobs) + { + if (jobs == null || jobs.Length == 0) { return false; } + string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier; + if (string.IsNullOrEmpty(jobIdentifier)) { return false; } + for (int i = 0; i < jobs.Length; i++) { - return true; - } - for (int i = 0; i < AppropriateJobs.Length; i++) - { - if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } + if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } + public bool HasAppropriateJob(Character character) => HasSpecifiedJob(character, AppropriateJobs); + + public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true) { if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { // Use special dialogue when we're rearranging character orders - return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + if (!givingOrderToSelf) + { + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + } + else + { + // Say nothing when rearranging the orders of the character you're controlling + return string.Empty; + } } string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}"; if (!string.IsNullOrEmpty(orderOption)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 28a9b6bde..074817047 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -346,7 +346,12 @@ namespace Barotrauma Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1); - if (limbDiff.LengthSquared() < extent * extent) + bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; + if (tooFar) + { + character.SelectedCharacter = null; + } + else { //pull the target character to the position of the mouth //(+ make the force fluctuate to waggle the character a bit) @@ -383,7 +388,7 @@ namespace Barotrauma mouthLimb.body.ApplyTorque(-force * 50); } - if (Character.CanEat) + if (Character.CanEat && target.IsDead) { var jaw = GetLimb(LimbType.Jaw); if (jaw != null) @@ -432,10 +437,6 @@ namespace Barotrauma } } } - else - { - character.SelectedCharacter = null; - } } public bool reverse; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index eae78c6fd..a02fefc3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -533,7 +533,7 @@ namespace Barotrauma bool onSlope = Math.Abs(movement.X) > 0.01f && Math.Abs(floorNormal.X) > 0.1f && Math.Sign(floorNormal.X) != Math.Sign(movement.X); - bool movingHorizontally = !MathUtils.NearlyEqual(targetMovement.X, 0.0f); + bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); if (Stairs != null || onSlope) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 31d8938b8..9d2017b81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -304,7 +304,27 @@ namespace Barotrauma public abstract float? TorsoPosition { get; } public abstract float? TorsoAngle { get; } - public float ImpactTolerance => RagdollParams.ImpactTolerance; + float? impactTolerance; + public float ImpactTolerance + { + get + { + if (impactTolerance == null) + { + impactTolerance = RagdollParams.ImpactTolerance; + if (character.Params.VariantFile != null) + { + float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); + if (tolerance.HasValue) + { + impactTolerance = tolerance; + } + } + } + return impactTolerance.Value; + } + } + public bool Draggable => RagdollParams.Draggable; public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; @@ -1833,7 +1853,7 @@ namespace Barotrauma float sin = (float)Math.Sin(mouthLimb.Rotation); Vector2 bodySize = mouthLimb.body.GetSize(); Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2); - return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * mouthLimb.Scale * RagdollParams.LimbScale; + return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos); } public Vector2 GetColliderBottom() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index ced071a7b..2e0ad881b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -101,11 +101,21 @@ namespace Barotrauma [Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] public bool Retreat { get; private set; } + private float _range; [Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] - public float Range { get; set; } + public float Range + { + get => _range * RangeMultiplier; + set => _range = value; + } + private float _damageRange; [Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] - public float DamageRange { get; set; } + public float DamageRange + { + get => _damageRange * RangeMultiplier; + set => _damageRange = value; + } [Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } @@ -145,10 +155,20 @@ namespace Barotrauma public float Penetration { get; private set; } /// - /// Currently only used with variants. Used for multiplying all the damage. + /// Used for multiplying all the damage. /// public float DamageMultiplier { get; set; } = 1; + /// + /// Used for multiplying all the ranges. + /// + public float RangeMultiplier { get; set; } = 1; + + /// + /// Used for multiplying the physics forces. + /// + public float ImpactMultiplier { get; set; } = 1; + [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 8078d4931..49ccb10ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1249,6 +1249,10 @@ namespace Barotrauma Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); #if CLIENT + if (info.Head?.HairWithHatElement != null) + { + head.HairWithHatSprite = new WearableSprite(info.Head?.HairWithHatElement.Element("sprite"), WearableType.Hair); + } head.EnableHuskSprite = Params.Husk; head.LoadHerpesSprite(); head.UpdateWearableTypesToHide(); @@ -3499,7 +3503,7 @@ namespace Barotrauma Limb limbHit = targetLimb; - float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; + float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime; AbilityAttackData attackData = new AbilityAttackData(attack, this); if (attacker != null) @@ -3537,7 +3541,7 @@ namespace Barotrauma } if (limbHit == null) { return new AttackResult(); } - Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; + Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier; if (attacker != null) { forceWorld.X *= attacker.AnimController.Dir; @@ -3845,40 +3849,49 @@ namespace Barotrauma targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); statusEffect.Apply(actionType, deltaTime, this, targets); } - else + else if (statusEffect.targetLimbs != null) { - statusEffect.Apply(actionType, deltaTime, this, this); - if (statusEffect.targetLimbs != null) + foreach (var limbType in statusEffect.targetLimbs) { - foreach (var limbType in statusEffect.targetLimbs) + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + // Target all matching limbs + foreach (var limb in AnimController.Limbs) { - // Target all matching limbs - foreach (var limb in AnimController.Limbs) + if (limb.IsSevered) { continue; } + if (limb.type == limbType) { - if (limb.IsSevered) { continue; } - if (limb.type == limbType) - { - statusEffect.Apply(actionType, deltaTime, this, limb); - } + statusEffect.sourceBody = limb.body; + statusEffect.Apply(actionType, deltaTime, this, limb); } } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = AnimController.GetLimb(limbType); + if (limb != null) { - // Target just the first matching limb - Limb limb = AnimController.GetLimb(limbType); + statusEffect.sourceBody = limb.body; statusEffect.Apply(actionType, deltaTime, this, limb); } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + if (limb != null) { - // Target just the last matching limb - Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.sourceBody = limb.body; statusEffect.Apply(actionType, deltaTime, this, limb); } } } } + if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, this, this); + } } if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index fa99a10d6..d32e68cab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -43,6 +43,7 @@ namespace Barotrauma public int FaceAttachmentIndex { get; set; } = -1; public XElement HairElement { get; set; } + public XElement HairWithHatElement { get; set; } public XElement BeardElement { get; set; } public XElement MoustacheElement { get; set; } public XElement FaceAttachment { get; set; } @@ -1125,6 +1126,16 @@ namespace Barotrauma Head.HairElement = GetRandomElement(hairs); Head.HairIndex = hairs.IndexOf(Head.HairElement); } + if (Head.HairElement != null) + { + int thisHairIndex = hairs.IndexOf(head.HairElement); + int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex); + if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count) + { + head.HairWithHatElement = hairs[hairWithHatIndex]; + } + } + if (IsValidIndex(Head.BeardIndex, beards)) { Head.BeardElement = beards[Head.BeardIndex]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index d6f55b65d..46577e017 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -649,6 +649,8 @@ namespace Barotrauma if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); + attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f); + attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f); } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index ff46272dc..7b004f09d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -14,9 +14,9 @@ namespace Barotrauma NotDefined = 0, Walk = 1, Run = 2, - Crouch = 3, - SwimSlow = 4, - SwimFast = 5 + SwimSlow = 3, + SwimFast = 4, + Crouch = 5 } abstract class GroundedMovementParams : AnimationParams diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index 43085c608..c23cf4ad1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -42,7 +42,7 @@ namespace Barotrauma } private float skillIncreasePerRepairedStructureDamage; - [Serialize(0.005f, true)] + [Serialize(0.0025f, true)] public float SkillIncreasePerRepairedStructureDamage { get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 1da7cf86c..0832d8e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -521,6 +521,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } targetCharacter.GodMode = !targetCharacter.GodMode; + NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on " + targetCharacter.Name), Color.White); }, () => { @@ -1042,6 +1043,20 @@ namespace Barotrauma throw new Exception("crash command issued"); })); + commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) => + { + float seconds = 0; + if (args.Length > 0) { float.TryParse(args[0], out seconds); } + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + for (int i = 0; i < seconds * Timing.FixedUpdateRate; i++) + { + Screen.Selected?.Update(Timing.Step); + } + sw.Stop(); + NewMessage($"Fast-forwarded by {seconds} seconds (took {sw.ElapsedMilliseconds / 1000.0f} s)."); + })); + commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) => { if (args.Length == 0) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 60455b983..227528cee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -351,7 +351,7 @@ namespace Barotrauma } } - public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null) + public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); @@ -365,7 +365,7 @@ namespace Barotrauma stats.Add(newStats); } debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); - LogEventStats(stats, debugLines); + LogEventStats(stats, debugLines, fullLog); } return debugLines; @@ -415,14 +415,19 @@ namespace Barotrauma if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } - float spawnProbability = monsterEvent.Prefab.Probability; if (Rand.Value() > spawnProbability) { return; } - - string character = monsterEvent.speciesName; int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } - if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; } + string character = monsterEvent.speciesName; + if (stats.MonsterCounts.TryGetValue(character, out int currentCount)) + { + if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; } + } + else + { + stats.MonsterCounts[character] = 0; + } stats.MonsterCounts[character] += count; var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai"); @@ -433,7 +438,7 @@ namespace Barotrauma } } - static void LogEventStats(List stats, List debugLines) + static void LogEventStats(List stats, List debugLines, bool fullLog) { if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0)) { @@ -442,28 +447,42 @@ namespace Barotrauma } else { + var allMonsters = new Dictionary(); + foreach (var stat in stats) + { + foreach (var monster in stat.MonsterCounts) + { + if (!allMonsters.TryAdd(monster.Key, monster.Value)) + { + allMonsters[monster.Key] += monster.Value; + } + } + } + allMonsters = allMonsters.OrderBy(m => m.Key).ToDictionary(m => m.Key, m => m.Value); stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum())); - debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats.First())}"); - debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}"); - debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}"); - debugLines.Add($" {LogMonsterCounts(stats.Last())}"); - debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}"); - debugLines.Add($" "); - + debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))} (Min: {stats.First().MonsterCounts.Values.Sum()}, Max: {stats.Last().MonsterCounts.Values.Sum()})"); + debugLines.Add($" {LogMonsterCounts(allMonsters, divider: stats.Count)}"); + if (fullLog) + { + debugLines.Add($" All samples:"); + stats.ForEach(s => debugLines.Add($" {LogMonsterCounts(s.MonsterCounts)}")); + } stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength)); - debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}"); - debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}"); - debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}"); - debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}"); + debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))} (Min: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}, Max: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)})"); debugLines.Add($" "); } } - static string LogMonsterCounts(EventDebugStats stats) + static string LogMonsterCounts(Dictionary stats, float divider = 0) { - return string.Join(", ", stats.MonsterCounts.Select(mc => mc.Key + " x " + mc.Value)); + if (divider > 0) + { + return string.Join("\n ", stats.Select(mc => mc.Key + " x " + (mc.Value / divider).FormatSingleDecimal())); + } + else + { + return string.Join(", ", stats.Select(mc => mc.Key + " x " + mc.Value)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 3fc58b81a..d5ee46335 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -1,3 +1,4 @@ +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -58,6 +59,18 @@ namespace Barotrauma if (IsClient) { return; } if (!swarmSpawned && level.CheckBeaconActive()) { + List connectedSubs = level.BeaconStation.GetConnectedSubs(); + foreach (Item item in Item.ItemList) + { + if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (item.GetComponent() != null || + item.GetComponent() != null || + item.GetComponent() != null) + { + item.Indestructible = true; + } + } + State = 1; Vector2 spawnPos = level.BeaconStation.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 5f628660c..0bbb70d87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -60,8 +60,16 @@ namespace Barotrauma } else { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); + if (itemIdentifier != null) + { + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + } + if (itemPrefab == null) + { + string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", ""); + itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; + } if (itemPrefab == null) { DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier); @@ -150,8 +158,8 @@ namespace Barotrauma if (item == null) { item = new Item(itemPrefab, position, null); + item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation); item.body.FarseerBody.BodyType = BodyType.Kinematic; - item.FindHull(); } for (int i = 0; i < statusEffects.Count; i++) @@ -192,7 +200,7 @@ namespace Barotrauma } if (validContainers.Any()) { - var selectedContainer = validContainers.GetRandom(); + var selectedContainer = validContainers.GetRandom(Rand.RandSync.Unsynced); if (selectedContainer.Combine(item, user: null)) { #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e1c8e0a14..bae60c6e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -26,7 +26,7 @@ namespace Barotrauma private bool spawnPending; - private readonly int maxAmountPerLevel = int.MaxValue; + public readonly int MaxAmountPerLevel = int.MaxValue; public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; @@ -74,7 +74,7 @@ namespace Barotrauma minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); - maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); + MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || @@ -367,9 +367,9 @@ namespace Barotrauma if (spawnPos == null) { - if (maxAmountPerLevel < int.MaxValue) + if (MaxAmountPerLevel < int.MaxValue) { - if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel) + if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= MaxAmountPerLevel) { disallowed = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 7081b424e..d4f099142 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -507,20 +507,25 @@ namespace Barotrauma return; } - var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); - if (allPackages?.Count > 0) + if (GameMain.Config != null) { - List packageNames = new List(); - foreach (ContentPackage cp in allPackages) + var allPackages = GameMain.Config.AllEnabledPackages.ToList(); + if (allPackages?.Count > 0) { - string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); - sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); - packageNames.Add(sanitizedName); - loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); + List packageNames = new List(); + foreach (ContentPackage cp in allPackages) + { + string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); + sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); + packageNames.Add(sanitizedName); + loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); + } + packageNames.Sort(); + loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(", ", packageNames)); } - packageNames.Sort(); - loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(", ", packageNames)); + loadedImplementation?.AddDesignEvent("Language:" + GameMain.Config.Language); } + } static partial void InitKeys(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 9742c0287..15fde73a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -27,21 +27,85 @@ namespace Barotrauma class SoldItem { public ItemPrefab ItemPrefab { get; } - public ushort ID { get; } + public ushort ID { get; private set; } public bool Removed { get; set; } public byte SellerID { get; } + public SellOrigin Origin { get; } - public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId) + public enum SellOrigin + { + Character, + Submarine + } + + public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin) { ItemPrefab = itemPrefab; ID = id; Removed = removed; SellerID = sellerId; + Origin = origin; + } + + public void SetItemId(ushort id) + { + if (ID != Entity.NullEntityID) + { + DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed."); + return; + } + ID = id; } } partial class CargoManager { + private class SoldEntity + { + public enum SellStatus + { + /// + /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. + /// + Confirmed, + /// + /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. + /// + Unconfirmed, + /// + /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. + /// + Local + } + + public Item Item { get; private set; } + public ItemPrefab ItemPrefab { get; } + public SellStatus Status { get; set; } + + public SoldEntity(Item item, SellStatus status) + { + Item = item; + ItemPrefab = item?.Prefab; + Status = status; + } + + public SoldEntity(ItemPrefab itemPrefab, SellStatus status) + { + ItemPrefab = itemPrefab; + Status = status; + } + + public void SetItem(Item item) + { + if (Item != null) + { + DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + Item = item; + } + } + public const int MaxQuantity = 100; public List ItemsInBuyCrate { get; } = new List(); @@ -92,7 +156,7 @@ namespace Barotrauma public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity) { - PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); + var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) { itemInCrate.Quantity += changeInQuantity; @@ -109,6 +173,25 @@ namespace Barotrauma OnItemsInBuyCrateChanged?.Invoke(); } + public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + { + var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); + if (itemInCrate != null) + { + itemInCrate.Quantity += changeInQuantity; + if (itemInCrate.Quantity < 1) + { + ItemsInSellFromSubCrate.Remove(itemInCrate); + } + } + else if (changeInQuantity > 0) + { + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + ItemsInSellFromSubCrate.Add(itemInCrate); + } + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + public void PurchaseItems(List itemsToPurchase, bool removeFromCrate) { // Check all the prices before starting the transaction @@ -185,6 +268,82 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } + private Dictionary UndeterminedSoldEntities { get; } = new Dictionary(); + + public IEnumerable GetSellableItemsFromSub() + { + if (Submarine.MainSub == null) { return new List(); } + var confirmedSoldEntities = Enumerable.Empty(); + UndeterminedSoldEntities.Clear(); +#if CLIENT + confirmedSoldEntities = GetConfirmedSoldEntities(); + foreach (var soldEntity in SoldEntities) + { + if (soldEntity.Item != null) { continue; } + if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count)) + { + UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1; + } + else + { + UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1); + } + } +#endif + return Submarine.MainSub.GetItems(true).FindAll(item => + { + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } + if (item.GetRootInventoryOwner() is Character) { return false; } + if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } + if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!ItemAndAllContainersInteractable(item)) { return false; } + if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; } + return true; + }).Distinct(); + + static bool ItemAndAllContainersInteractable(Item item) + { + do + { + if (!item.IsPlayerTeamInteractable) { return false; } + item = item.Container; + } while (item != null); + return true; + } + } + + private bool IsItemSellable(Item item, IEnumerable confirmedItems) + { + if (item.Removed) { return false; } + if (!item.Prefab.CanBeSold) { return false; } + if (item.SpawnedInCurrentOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (confirmedItems.Any(ci => ci.Item == item)) { return false; } + if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count)) + { + int newCount = count - 1; + if (newCount > 0) + { + UndeterminedSoldEntities[item.Prefab] = newCount; + } + else + { + UndeterminedSoldEntities.Remove(item.Prefab); + } + return false; + } + if (item.OwnInventory?.Container is ItemContainer itemContainer) + { + var containedItems = item.ContainedItems; + if (containedItems.None()) { return true; } + // Allow selling the item if contained items are unsellable and set to be removed on deconstruct + if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; } + } + return true; + } + public static void CreateItems(List itemsToSpawn, Submarine sub) { if (itemsToSpawn.Count == 0) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index bbd0ca3f2..4856ff843 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -448,19 +448,21 @@ namespace Barotrauma filteredCharacters = filteredCharacters.Union(extraCharacters); } return filteredCharacters - // 1. Prioritize those who are on the same submarine than the controlled character + // Prioritize those who are on the same submarine as the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) - // 2. Prioritize those who are already ordered to operate the device + // Prioritize those who are already ordered to operate the device .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity)) - // 3. Prioritize those with the appropriate job for the order + // Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) - // 4. Prioritize those who don't yet have another Operate order of the same kind (which allows quick-assigning multiple Operate orders to different characters) - .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) - // 5. Prioritize bots over player controlled characters + // Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters) + .ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) + // Prioritize those with the preferred job for the order + .ThenByDescending(c => order.HasPreferredJob(c)) + // Prioritize bots over player-controlled characters .ThenByDescending(c => c.IsBot) - // 6. Use the priority value of the current objective + // Prioritize those with a lower current objective priority .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) - // 7. Prioritize those with the best skill for the order + // Prioritize those with a higher order skill level .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 0258a3eee..9ec10aff0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + public bool ApplyEffectsOnDocking + { + get; + set; + } + [Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } @@ -261,7 +268,7 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; - if (applyEffects) + if (applyEffects && ApplyEffectsOnDocking) { ApplyStatusEffects(ActionType.OnUse, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 76eca095e..319c60ce4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components { if (holdable.Attached) { - GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + item.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.Prefab.Identifier); holdable.DeattachFromWall(); } trigger.Enabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index cebf5f018..e172945b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -841,9 +841,9 @@ namespace Barotrauma.Items.Components if (statusEffectLists == null) { return; } if (!statusEffectLists.TryGetValue(actionType, out List statusEffects)) { return; } - currentTargets.Clear(); foreach (StatusEffect effect in statusEffects) { + currentTargets.Clear(); effect.SetUser(user); if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 310aa6405..944fd6055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components } } - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + targetItem.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + targetItem.prefab.Identifier); if (targetItem.AllowDeconstruct && allowRemove) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 266c28fff..177571933 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components prevVoltage = Voltage; hasPower = Voltage > MinVoltage; - Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f); + Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); @@ -137,18 +137,19 @@ namespace Barotrauma.Items.Components currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } Vector2 forceVector = new Vector2(currForce, 0); - item.Submarine.ApplyForce(forceVector); + item.Submarine.ApplyForce(forceVector * deltaTime * Timing.FixedUpdateRate); UpdatePropellerDamage(deltaTime); #if CLIENT - particleTimer -= deltaTime; - if (particleTimer <= 0.0f) + float particleInterval = 1.0f / particlesPerSec; + particleTimer += deltaTime; + while (particleTimer > particleInterval) { Vector2 particleVel = -forceVector.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale, - particleVel * Rand.Range(0.9f, 1.1f), + particleVel * Rand.Range(0.8f, 1.1f), 0.0f, item.CurrentHull); - particleTimer = 1.0f / particlesPerSec; - } + particleTimer -= particleInterval; + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 291778bdd..43dfd81d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -397,7 +397,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Name ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); if (i < amountFittingContainer) { Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index e0a701b14..b7cb7b076 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -103,7 +103,20 @@ namespace Barotrauma.Items.Components if (TargetLevel != null) { float hullPercentage = 0.0f; - if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } + if (item.CurrentHull != null) + { + float hullWaterVolume = item.CurrentHull.WaterVolume; + float totalHullVolume = item.CurrentHull.Volume; + foreach (var linked in item.CurrentHull.linkedTo) + { + if ((linked is Hull linkedHull)) + { + hullWaterVolume += linkedHull.WaterVolume; + totalHullVolume += linkedHull.Volume; + } + } + hullPercentage = hullWaterVolume / totalHullVolume * 100.0f; + } FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } @@ -131,8 +144,8 @@ namespace Barotrauma.Items.Components //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); - item.CurrentHull.WaterVolume += currFlow; - if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } + item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate; + if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; } Voltage -= deltaTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 1283caeec..ea96276e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -738,8 +738,9 @@ namespace Barotrauma.Items.Components } lastTarget = target; - float projectileNewSpeed = 0.5f; - float projectileDeflectedNewSpeed = 0.1f; + int remainingHits = Math.Max(MaxTargetsToHit - hits.Count, 0); + float speedMultiplier = Math.Min(0.4f + remainingHits * 0.1f, 1.0f); + float deflectedSpeedMultiplier = 0.1f; AttackResult attackResult = new AttackResult(); Character character = null; @@ -755,8 +756,8 @@ namespace Barotrauma.Items.Components // when hitting limbs with piercing ammo, don't lose as much speed if (MaxTargetsToHit > 1) { - projectileNewSpeed = 1f; - projectileDeflectedNewSpeed = 0.8f; + speedMultiplier = 1f; + deflectedSpeedMultiplier = 0.8f; } if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; } @@ -869,7 +870,7 @@ namespace Barotrauma.Items.Components if (attackResult.AppliedDamageModifiers != null && (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) { - item.body.LinearVelocity *= projectileDeflectedNewSpeed; + item.body.LinearVelocity *= deflectedSpeedMultiplier; } else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && @@ -901,13 +902,13 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif - item.body.LinearVelocity *= projectileNewSpeed; + item.body.LinearVelocity *= speedMultiplier; return Hitscan; } else { - item.body.LinearVelocity *= projectileNewSpeed; + item.body.LinearVelocity *= speedMultiplier; } var containedItems = item.OwnInventory?.AllItems; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 5119cb9a7..185d8a158 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components ic.ReceiveSignal(signal, connection); } - if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) + if (recipient.Effects != null && signal.value != "0") { foreach (StatusEffect effect in recipient.Effects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index e83fe10f4..f8177bdcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -241,7 +241,7 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { - if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && + if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && IsOn && (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 604aed27d..48504c6d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -235,6 +235,13 @@ namespace Barotrauma /// public bool IsInteractable(Character character) { +#if CLIENT + if (Screen.Selected is EditorScreen) + { + return true; + } +#endif + if (character != null && character.IsOnPlayerTeam) { return IsPlayerTeamInteractable; @@ -638,6 +645,10 @@ namespace Barotrauma } } + private float buoyancySineMagnitude; + private float buoyancySineFrequency; + private float buoyancyRandomForce; + public bool FireProof { get { return Prefab.FireProof; } @@ -866,9 +877,11 @@ namespace Barotrauma } } } - - body.FarseerBody.AngularDamping = element.GetAttributeFloat("angulardamping", 0.2f); - body.FarseerBody.LinearDamping = element.GetAttributeFloat("lineardamping", 0.1f); + body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f); + body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f); + buoyancySineMagnitude = subElement.GetAttributeFloat("buoyancysinemagnitude", 0f); + buoyancySineFrequency = subElement.GetAttributeFloat("buoyancysinefrequency", 0f); + buoyancyRandomForce = subElement.GetAttributeFloat("buoyancyrandom", 0f); body.UserData = this; break; case "trigger": @@ -1716,7 +1729,7 @@ namespace Barotrauma UpdateNetPosition(deltaTime); if (inWater) { - ApplyWaterForces(); + ApplyWaterForces(deltaTime); CurrentHull?.ApplyFlowForces(deltaTime, this); } } @@ -1805,16 +1818,24 @@ namespace Barotrauma transformDirty = false; } + private float sineTime; /// /// Applies buoyancy, drag and angular drag caused by water /// - private void ApplyWaterForces() + private void ApplyWaterForces(float deltaTime) { if (body.Mass <= 0.0f || body.Density <= 0.0f) { return; } - + if (buoyancySineFrequency > 0) + { + if (sineTime >= float.MaxValue) + { + sineTime = float.MinValue; + } + sineTime += deltaTime * buoyancySineFrequency; + } float forceFactor = 1.0f; if (CurrentHull != null) { @@ -1833,7 +1854,10 @@ namespace Barotrauma Vector2 drag = body.LinearVelocity * volume; - body.ApplyForce((uplift - drag) * 10.0f); + float sine = (float)Math.Sin(sineTime) * buoyancySineMagnitude; + Vector2 sineForce = Vector2.UnitY * sine * volume; + Vector2 randomForce = Vector2.UnitY * Rand.Range(-buoyancyRandomForce, buoyancyRandomForce, Rand.RandSync.Unsynced) * volume; + body.ApplyForce((uplift - drag) * 10.0f + sineForce + randomForce); //apply simple angular drag body.ApplyTorque(body.AngularVelocity * volume * -0.05f); @@ -1971,7 +1995,7 @@ namespace Barotrauma return connectedComponents; } - private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents) where T : ItemComponent + private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false) where T : ItemComponent { ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) { return; } @@ -1980,18 +2004,18 @@ namespace Barotrauma { if (alreadySearched.Contains(c)) { continue; } alreadySearched.Add(c); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); } } /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponentsRecursive(Connection c) where T : ItemComponent + public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false) where T : ItemComponent { List connectedComponents = new List(); HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); return connectedComponents; } @@ -2008,7 +2032,7 @@ namespace Barotrauma ("signal_in2", "signal_out") }; - private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents) where T : ItemComponent + private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent { alreadySearched.Add(c); @@ -2033,12 +2057,18 @@ namespace Barotrauma foreach (Connection wifiOutput in receiverConnections) { if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays); } } } - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays); + } + + if (ignoreInactiveRelays) + { + var relay = GetComponent(); + if (relay != null && !relay.IsOn) { return; } } foreach ((string input, string output) in connectionPairs) @@ -2049,7 +2079,7 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); } } else if (output == c.Name) @@ -2058,7 +2088,7 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 9d2873a8e..50b0358e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -301,7 +301,7 @@ namespace Barotrauma Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1]; if (hull1 == hull2) { return; } - UpdateOxygen(hull1, hull2); + UpdateOxygen(hull1, hull2, deltaTime); if (linkedTo.Count == 1) { @@ -316,7 +316,7 @@ namespace Barotrauma flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce); - if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length()) + if (openedTimer > 0.0f && flowForce.LengthSquared() > lerpedFlowForce.LengthSquared()) { //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing lerpedFlowForce = flowForce; @@ -344,7 +344,7 @@ namespace Barotrauma subOffset = hull2.Submarine.Position - Submarine.Position; } - if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) return; + if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) { return; } float size = IsHorizontal ? rect.Height : rect.Width; @@ -366,7 +366,7 @@ namespace Barotrauma //water flowing from the righthand room to the lefthand room if (dir == -1) { - if (!(hull2.WaterVolume > 0.0f)) return; + if (!(hull2.WaterVolume > 0.0f)) { return; } lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1]; //delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume)); //delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress); @@ -374,10 +374,10 @@ namespace Barotrauma flowTargetHull = hull1; //make sure not to move more than what the room contains - delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume)); + delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 300.0f * sizeModifier * deltaTime, Math.Min(hull2.WaterVolume, hull2.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume)); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; if (hull1.WaterVolume > hull1.Volume) @@ -389,16 +389,16 @@ namespace Barotrauma } else if (dir == 1) { - if (!(hull1.WaterVolume > 0.0f)) return; + if (!(hull1.WaterVolume > 0.0f)) { return; } lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1]; flowTargetHull = hull2; //make sure not to move more than what the room contains - delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume)); + delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 300.0f * sizeModifier * deltaTime, Math.Min(hull1.WaterVolume, hull1.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume)); + delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - hull2.WaterVolume); hull1.WaterVolume -= delta; hull2.WaterVolume += delta; if (hull2.WaterVolume > hull2.Volume) @@ -409,7 +409,7 @@ namespace Barotrauma flowForce = new Vector2(delta, 0.0f); } - if (delta > 100.0f && subOffset == Vector2.Zero) + if (delta > 1.5f && subOffset == Vector2.Zero) { float avg = (hull1.Surface + hull2.Surface) / 2.0f; @@ -516,7 +516,7 @@ namespace Barotrauma delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; - if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f; + if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 30.0f * deltaTime; } flowTargetHull = hull1; @@ -541,24 +541,24 @@ namespace Barotrauma { if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f; + float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f; vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - hull1.WaveVel[hull1.WaveY.Length - 1] += vel; - hull1.WaveVel[hull1.WaveY.Length - 2] += vel; + hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime; + hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime; } else { - float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 0.1f; + float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f; vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); - hull1.WaveVel[0] += vel; - hull1.WaveVel[1] += vel; + hull1.WaveVel[0] += vel * deltaTime; + hull1.WaveVel[1] += vel * deltaTime; } } else { - hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; } } else @@ -573,7 +573,7 @@ namespace Barotrauma } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : 10.0f) * deltaTime; } } } @@ -639,7 +639,7 @@ namespace Barotrauma } } - private void UpdateOxygen(Hull hull1, Hull hull2) + private void UpdateOxygen(Hull hull1, Hull hull2, float deltaTime) { if (hull1 == null || hull2 == null) { return; } @@ -650,10 +650,10 @@ namespace Barotrauma } float totalOxygen = hull1.Oxygen + hull2.Oxygen; - float totalVolume = (hull1.Volume + hull2.Volume); + float totalVolume = hull1.Volume + hull2.Volume; float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen; - deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed); + deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed * deltaTime, Hull.OxygenDistributionSpeed * deltaTime); hull1.Oxygen += deltaOxygen; hull2.Oxygen -= deltaOxygen; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 7007094d8..869d1fe35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -107,7 +107,7 @@ namespace Barotrauma public static bool ShowHulls = true; public static bool EditWater, EditFire; - public const float OxygenDistributionSpeed = 500.0f; + public const float OxygenDistributionSpeed = 30000.0f; public const float OxygenDeteriorationSpeed = 0.3f; public const float OxygenConsumptionSpeed = 700.0f; @@ -132,7 +132,7 @@ namespace Barotrauma private float lethalPressure; - private float surface, drawSurface; + private float surface; private float waterVolume; private float pressure; @@ -241,7 +241,10 @@ namespace Barotrauma } OxygenPercentage = prevOxygenPercentage; - surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width; + surface = rect.Y - rect.Height + WaterVolume / rect.Width; +#if CLIENT + drawSurface = surface; +#endif Pressure = surface; CreateBackgroundSections(); @@ -275,17 +278,6 @@ namespace Barotrauma get { return surface; } } - public float DrawSurface - { - get { return drawSurface; } - set - { - if (Math.Abs(drawSurface - value) < 0.00001f) return; - drawSurface = MathHelper.Clamp(value, rect.Y - rect.Height, rect.Y); - update = true; - } - } - public float WorldSurface { get { return Submarine == null ? surface : surface + Submarine.Position.Y; } @@ -628,7 +620,10 @@ namespace Barotrauma Gap.UpdateHulls(); } - surface = drawSurface = rect.Y - rect.Height + WaterVolume / rect.Width; + surface = rect.Y - rect.Height + WaterVolume / rect.Width; +#if CLIENT + drawSurface = surface; +#endif Pressure = surface; } @@ -753,8 +748,6 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - base.Update(deltaTime, cam); - BallastFlora?.Update(deltaTime); UpdateProjSpecific(deltaTime, cam); @@ -808,11 +801,6 @@ namespace Barotrauma surface, rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); - //interpolate the position of the rendered surface towards the "target surface" - drawSurface = Math.Max(MathHelper.Lerp( - drawSurface, - rect.Y - rect.Height + waterDepth, - deltaTime * 10.0f), rect.Y - rect.Height); for (int i = 0; i < waveY.Length; i++) { @@ -900,10 +888,10 @@ namespace Barotrauma } } - //0.01 increase every ~1000 frames = reaches full dirtiness in ~27 minutes - if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(1000) == 1) + //0.016 increase every ~2000 frames = reaches full dirtiness in ~35 minutes + if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(2000) == 1) { - DirtySections(submergedSections, 0.01f); + DirtySections(submergedSections, deltaTime); } if (waterVolume < Volume) @@ -911,11 +899,13 @@ namespace Barotrauma LethalPressure -= 10.0f * deltaTime; if (WaterVolume <= 0.0f) { +#if CLIENT //wait for the surface to be lerped back to bottom and the waves to settle until disabling update - if (drawSurface > rect.Y - rect.Height + 1) return; + if (drawSurface > rect.Y - rect.Height + 1) { return; } +#endif for (int i = 1; i < waveY.Length - 1; i++) { - if (waveY[i] > 0.1f) return; + if (waveY[i] > 0.1f) { return; } } update = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 082ad2521..7c26aea92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -147,8 +147,6 @@ namespace Barotrauma { List points = new List(); - var wallPrefabs = StructurePrefab.Prefabs.Where(mp => mp.Body); - foreach (XElement element in rootElement.Elements()) { if (element.Name != "Structure") { continue; } @@ -159,8 +157,12 @@ namespace Barotrauma StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null) { continue; } + float scale = element.GetAttributeFloat("scale", prefab.Scale); + var rect = element.GetAttributeVector4("rect", Vector4.Zero); - + rect.Z *= scale / prefab.Scale; + rect.W *= scale / prefab.Scale; + points.Add(new Vector2(rect.X, rect.Y)); points.Add(new Vector2(rect.X + rect.Z, rect.Y)); points.Add(new Vector2(rect.X, rect.Y - rect.W)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index dabc6e22c..a5929ded1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -486,7 +486,8 @@ namespace Barotrauma { location.LevelData = new LevelData(location) { - Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) + Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) + //Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) }; location.UnlockInitialMissions(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 1683e5269..b37f1701e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -3,12 +3,10 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -21,6 +19,9 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; + private const int GapUpdateInterval = 4; + private static int gapUpdateTimer; + /// /// List of upgrades this item has /// @@ -410,6 +411,7 @@ namespace Barotrauma } //connect clone wires to the clone items and refresh links between doors and gaps + List orphanedWires = new List(); for (int i = 0; i < clones.Count; i++) { if (!(clones[i] is Item cloneItem)) { continue; } @@ -442,7 +444,7 @@ namespace Barotrauma } var connectedItem = originalWire.Connections[n].Item; - if (connectedItem == null) { continue; } + if (connectedItem == null || !entitiesToClone.Contains(connectedItem)) { continue; } //index of the item the wire is connected to int itemIndex = entitiesToClone.IndexOf(connectedItem); @@ -469,6 +471,20 @@ namespace Barotrauma (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false); } + + if (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) + { + if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) + { + orphanedWires.Add(cloneWire); + } + } + } + + foreach (var orphanedWire in orphanedWires) + { + orphanedWire.Item.Remove(); + clones.Remove(orphanedWire.Item); } return clones; @@ -548,20 +564,27 @@ namespace Barotrauma { hull.Update(deltaTime, cam); } +#if CLIENT + Hull.UpdateCheats(deltaTime, cam); +#endif foreach (Structure structure in Structure.WallList) { structure.Update(deltaTime, cam); } - //update gaps in random order, because otherwise in rooms with multiple gaps //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through //one gap in a room even if there are several - foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) + gapUpdateTimer++; + if (gapUpdateTimer >= GapUpdateInterval) { - gap.Update(deltaTime, cam); + foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) + { + gap.Update(deltaTime * GapUpdateInterval, cam); + } + gapUpdateTimer = 0; } Powered.UpdatePower(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index dd0dcc2b0..6596c4067 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Barotrauma.Extensions; namespace Barotrauma { @@ -317,6 +318,11 @@ namespace Barotrauma return null; } + public static MapEntityPrefab GetRandom(Predicate predicate, Rand.RandSync sync) + { + return List.GetRandom(p => predicate(p), sync); + } + /// /// Find a matching map entity prefab /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 622795eb4..12ecc872f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -85,7 +85,9 @@ namespace Barotrauma.Networking get { if (customTextColor != null) { return customTextColor.Value; } - return MessageColor[(int)Type]; + int intType = (int)Type; + if (intType < 0 || intType >= MessageColor.Length) { return Color.White; } + return MessageColor[intType]; } set @@ -230,13 +232,13 @@ namespace Barotrauma.Networking break; case ChatMessageType.Radio: case ChatMessageType.Order: - if (receiver != null && !receiver.IsDead) + if (receiver?.Inventory != null && !receiver.IsDead) { - foreach (Item receiverItem in receiver.Inventory?.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) + foreach (Item receiverItem in receiver.Inventory.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) { - if (!receiver.HasEquippedItem(receiverItem)) { continue; } + if (sender.Inventory == null || !receiver.HasEquippedItem(receiverItem)) { continue; } - foreach (Item senderItem in sender.Inventory?.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) + foreach (Item senderItem in sender.Inventory.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) { if (!sender.HasEquippedItem(senderItem)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index d77dc356a..01ef58ee2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -22,7 +22,11 @@ namespace Barotrauma.Networking ManageSettings = 0x200, ManagePermissions = 0x400, KarmaImmunity = 0x800, - All = 0xFFF + BuyItems = 0x1000, + SellInventoryItems = 0x2000, + SellSubItems = 0x4000, + CampaignStore = 0x8000, + All = 0xFFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index e0d34e7ef..3d0a7c719 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1591,8 +1591,10 @@ namespace Barotrauma if (rope != null && sourceBody.UserData is Limb sourceLimb) { rope.Attach(sourceLimb, newItem); +#if SERVER + newItem.CreateServerEvent(rope); +#endif } - float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread)); var worldPos = sourceBody.Position; float rotation = chosenItemSpawnInfo.Rotation; @@ -1625,8 +1627,27 @@ namespace Barotrauma } else { - newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); - newItem.Rotation = chosenItemSpawnInfo.Rotation; + var body = newItem.body; + if (body != null) + { + float rotation = MathHelper.ToRadians(chosenItemSpawnInfo.Rotation); + if (chosenItemSpawnInfo.RotationType == ItemSpawnInfo.SpawnRotationType.Limb) + { + if (sourceBody != null) + { + rotation += sourceBody.Rotation; + } + } + else if (chosenItemSpawnInfo.RotationType == ItemSpawnInfo.SpawnRotationType.Collider) + { + if (entity is Character character) + { + rotation += character.AnimController.Collider.Rotation; + } + } + body.SetTransform(newItem.SimPosition, rotation); + body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + } } }); break; diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index e6b23855e..05289e918 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub index efc7bddea..9f09b8a2c 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub and b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index e7e878fc7..7d5ef8e3d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 3ef87f69f..081ca15a3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index c595276b0..13e491cf7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Herja.sub b/Barotrauma/BarotraumaShared/Submarines/Herja.sub index 4110c936d..422059a09 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Herja.sub and b/Barotrauma/BarotraumaShared/Submarines/Herja.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 1884c10ea..6fde41765 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 2de8f93e6..b95f6b326 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index c88e6472d..55eb5b23d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 97f6fa3cd..116a2f410 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub index 47dbe7d35..04e8414ce 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index aa9ad8e1e..eda5cadc8 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 33047bde9..f7431caff 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 26ec00c8f..3147d053a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 4075081a5..cec49a349 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 8a614c4c1..f125fb933 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 956eafdbd..57f80a052 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index a5602a06c..f11f731da 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub index 13a1215b9..ba3210be5 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub and b/Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 57d31717c..c436200fd 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,55 @@ +--------------------------------------------------------------------------------------------------------- +v0.16.2.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added 3 new monster variants: Giant Spineling, Crawler Broodmother and Veteran Mudraptor. +- Added the Submarine tab to the multiplayer campaign store to allow selling items on the submarine. +- Added new campaign store related client permissions: use campaign store, buy items, sell inventory items, and sell submarine items. +- Fixed wall healths being half of what they should be on vanilla subs, increased structure damages to compensate. +- Prevented selling items from submarine containers tagged with "donttakeitems", e.g. constructors, deconstructors. +- Added a new "Assault Enemy" order: bots with the order will seek out and attack any hostile characters in any connected submarines or outposts. +- Made the order quick-assigning logic prefer characters who don't have the order yet (among the characters with the appropriate job). +- Made it possible to use the order quick-assignment to give the Fix Leaks order to all character (although mechanics, engineers, and assistants are still preferred). +- Misc improvements and fixes to Winterhalter, Herja and Barsuk (unstable only). +- Balanced mission rewards, commonness and difficulties. +- Optimizations to gap logic. +- Chaingun projectiles and canister shells cause lacerations instead of gunshot wounds. +- Added recycle recipe to SMG magazines. +- Significantly reduced the speed at which welding tools fix walls. +- Bots can find buttons connected to a door using links made in the sub editor. Allows working around complex circuits that prevent the bots from figuring out which button controls a door. +- Allow using cheats in editors. +- Don't show "hidden in-game" docking ports on the sonar, option to disable the docking port's particle and sound effects. +- The prompt about forbidden words in the server's name is only shown when trying to start a public server. +- Show prices in the submarine specs window (previously there was no way to see a sub's price in the server lobby). +- Allow wiring non-interactable items and accessing non-interactable containers in the sub editor. + +Fixes: +- Fixed static lights being on even if turned off in the sub editor (unstable only). +- Fixed bots sometimes getting stuck on ladders while swimming. +- Fixed some hairs clipping through hats (unstable only). +- Fixed bots returning to the sub even when they have an active wait order. Happened when the order was given inside and then when e.g. the character is controlled by the player, and then when the player changes the character, the bot falls to the "find safety objective", because it's not allowed to stay outside. +- Fixed aggressive boarders not being aggressive enough inside the player sub, because they couldn't target things that were blocked by a wall. +- Adjusted threshers' targeting priorities. Tigerthreshers can now damage doors, but should do so only when they get inside. +- Fixed crouching animation not appearing in MP when moving backwards while crouching (unstable only). +- Fixed crashing when trying to send a messagebox to clients using console commands (unstable only). +- Fixed some terminals not working in alien ruins (unstable only). +- Fixed pumps not taking the volumes/shapes of the linked hulls into account when using "set_targetlevel", causing the neural level to be off in irregularly shaped multi-hull ballasts. +- Fixed artifacts sometimes spawning outside the level when there's no artifact holder to place them in (e.g. when having 2 artifact missions active at the same time). +- Fixed plasma cutter doing damage to all limbs, not just the one it hits (unstable only). +- The electrical grid in beacon stations is turned indestructible after activating it. Should fix beacon missions sometimes failing for no apparent reason (if something happened to damage the beacon's walls during the round and flood it). +- Fixed crashing when receiving a chat message when controlling a character with no inventory (unstable only). +- Fixed monsters continuing to eat a characters who've been revived with console commands (leading to weird results, such as the character being able to run around after being dismembered).. +- Attempt to fix a nullref exception in Map.RemoveFogOfWar (suspecting it was caused by a mod that didn't configure the campaign map's sprite for some biome). +- Fixed console error when trying to make a legacy husk crouch. +- Fixed the dialogue reserved for rearranging character orders not being used in multiplayer. +- Fixed Operate orders not being dismissed automatically when another character is ordered to operate the same device. +- Fixed WiFi components receiving non-radio chat messages in single player. +- Fixed nav terminal's docking button staying visible if the terminal is disconnected from the docking port by deactivating a relay between them. + +Modding: +- Fixed Rope component not attaching to the limb it's fired from in multiplayer (doesn't affect any vanilla content). + --------------------------------------------------------------------------------------------------------- v0.16.1.0 ---------------------------------------------------------------------------------------------------------