diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs rename to Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 9b31bf9bf..bfb8b7202 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -39,7 +39,7 @@ namespace Barotrauma targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); - if (wallTarget != null) + if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 2c7bc106e..890be7cb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -15,7 +15,7 @@ namespace Barotrauma partial void InitProjSpecific(ContentXElement element) { - if (element.Attribute("sound") != null) + if (element.GetAttribute("sound") != null) { DebugConsole.ThrowError("Error in attack ("+element+") - sounds should be defined as child elements, not as attributes."); return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 22d741e27..72fba4aaf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -152,7 +152,7 @@ namespace Barotrauma case TreatmentEventData _: msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; - case StatusEventData _: + case CharacterStatusEventData _: //do nothing break; case UpdateTalentsEventData _: @@ -343,8 +343,12 @@ namespace Barotrauma if (controlled == this) { Controlled = null; - IsRemotePlayer = ownerID > 0; } + if (GameMain.Client?.Character == this) + { + GameMain.Client.Character = null; + } + IsRemotePlayer = ownerID > 0; } break; case EventType.Status: @@ -371,7 +375,9 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -380,13 +386,16 @@ namespace Barotrauma if (targetEntity == null && eventType == EventType.SetAttackTarget) { DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetNotFound", GameAnalyticsManager.ErrorSeverity.Error, "Received invalid SetAttackTarget message. Target entity not found."); break; } - if (targetEntity is Character targetCharacter) + if (targetEntity is Character targetCharacter && targetLimbIndex != 255) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + string errorMsgWithoutName = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.SpeciesName}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"; + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsgWithoutName); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; @@ -560,6 +569,10 @@ namespace Barotrauma } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); + if (character.CampaignInteractionType == CampaignMode.InteractionType.Store) + { + character.MerchantIdentifier = inc.ReadIdentifier(); + } character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index b61c76669..436e47141 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.CharacterStatusEventData()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 3ae094982..86a971bf5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -12,22 +12,30 @@ namespace Barotrauma { public sealed partial class PackageSource : ICollection { - public ContentPackage SaveAndEnableRegularMod(ModProject modProject) + public string SaveRegularMod(ModProject modProject) { if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } - - //save the content package + string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) .CleanUpPathCrossPlatform(correctFilenameCase: false); - Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); modProject.Save(fileListPath); Refresh(); EnabledPackages.DisableRemovedMods(); - var newPackage = Regular.First(p => p.Path == fileListPath); - //enable it - EnabledPackages.EnableRegular(newPackage); + return fileListPath; + } - return newPackage; + public RegularPackage GetRegularModByPath(string fileListPath) + { + return Regular.First(p => p.Path == fileListPath); + } + + public RegularPackage SaveAndEnableRegularMod(ModProject modProject) + { + string fileListPath = SaveRegularMod(modProject); + var package = GetRegularModByPath(fileListPath); + EnabledPackages.EnableRegular(package); + + return package; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 4f4d110e3..f653a1a83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1454,9 +1454,9 @@ namespace Barotrauma // omega nesting incoming if (fabricationRecipe != null) { - foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + foreach (var priceInfo in itemPrefab.GetSellPricesOver(0)) { - NewMessage(" If bought at " + itemLocationPrice.Key + " it costs " + itemLocationPrice.Value.Price); + NewMessage($" If bought at {GetSeller(priceInfo.Value)} it costs {priceInfo.Value.Price}"); int totalPrice = 0; int? totalBestPrice = 0; foreach (var ingredient in fabricationRecipe.RequiredItems) @@ -1464,31 +1464,33 @@ namespace Barotrauma foreach (ItemPrefab ingredientItemPrefab in ingredient.ItemPrefabs) { int defaultPrice = ingredientItemPrefab.DefaultPrice?.Price ?? 0; - NewMessage(" Its ingredient " + ingredientItemPrefab.Name + " has base cost " + defaultPrice); + NewMessage($" Its ingredient {ingredientItemPrefab.Name} has base cost {defaultPrice}"); totalPrice += defaultPrice; totalBestPrice += ingredientItemPrefab.GetMinPrice(); int basePrice = defaultPrice; - foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + foreach (var ingredientItemPriceInfo in ingredientItemPrefab.GetBuyPricesUnder()) { - if (basePrice > ingredientItemLocationPrice.Value.Price) + if (basePrice > ingredientItemPriceInfo.Value.Price) { - NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for cheaper, " + ingredientItemLocationPrice.Value.Price, Color.Yellow); + NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for cheaper, {ingredientItemPriceInfo.Value.Price}", Color.Yellow); } else { - NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for more, " + ingredientItemLocationPrice.Value.Price, Color.Teal); + NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for more, {ingredientItemPriceInfo.Value.Price}", Color.Teal); } } } } int costDifference = itemPrefab.DefaultPrice.Price - totalPrice; - NewMessage(" Constructing the item from store-bought items provides " + costDifference + " profit with default values."); + NewMessage($" Constructing the item from store-bought items provides {costDifference} profit with default values."); if (totalBestPrice.HasValue) { - int? bestDifference = itemLocationPrice.Value.Price - totalBestPrice; - NewMessage(" Constructing the item from store-bought items provides " + bestDifference + " profit with best-case scenario values."); + int? bestDifference = priceInfo.Value.Price - totalBestPrice; + NewMessage($" Constructing the item from store-bought items provides {bestDifference} profit with best-case scenario values."); } + + static string GetSeller(PriceInfo priceInfo) => $"store with identifier \"{priceInfo.StoreIdentifier}\""; } } }, @@ -1763,7 +1765,7 @@ namespace Barotrauma //check missing mission texts foreach (var missionPrefab in MissionPrefab.Prefabs) { - Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); + Identifier missionId = (missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); addIfMissing($"missionname.{missionId}".ToIdentifier(), language); addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 7d3b239de..e99e15745 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -130,7 +130,7 @@ namespace Barotrauma UISprite newSprite = new UISprite(subElement); GUIComponent.ComponentState spriteState = GUIComponent.ComponentState.None; - if (subElement.Attribute("state") != null) + if (subElement.GetAttribute("state") != null) { string stateStr = subElement.GetAttributeString("state", "None"); Enum.TryParse(stateStr, out spriteState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 04eabc733..17f493e12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -172,7 +172,7 @@ namespace Barotrauma { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => FormatCurrency(campaign.Wallet.Balance) + TextGetter = () => TextManager.FormatCurrency(campaign.Wallet.Balance) }; var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -400,7 +400,7 @@ namespace Barotrauma if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - FormatCurrency(characterInfo.Salary), + TextManager.FormatCurrency(characterInfo.Salary), textAlignment: Alignment.Center) { CanBeFocused = false @@ -629,7 +629,7 @@ namespace Barotrauma { total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); - totalBlock.Text = FormatCurrency(total); + totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); @@ -925,7 +925,5 @@ namespace Barotrauma GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } } - - private LocalizedString FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e78e4e160..deb5dab1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -40,7 +40,7 @@ namespace Barotrauma public IEnumerable GetAllChildren() where T : GUIComponent { - return GetAllChildren().Where(c => c is T).Select(c => c as T); + return GetAllChildren().OfType(); } /// @@ -67,7 +67,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.UserData != null && child.UserData.Equals(obj))) { return child; } + if (Equals(child.UserData, obj)) { return child; } } return null; } @@ -108,7 +108,7 @@ namespace Barotrauma } public GUIComponent FindChild(object userData, bool recursive = false) { - var matchingChild = Children.FirstOrDefault(c => c.UserData == userData); + var matchingChild = Children.FirstOrDefault(c => Equals(c.UserData, userData)); if (recursive && matchingChild == null) { foreach (GUIComponent child in Children) @@ -123,7 +123,7 @@ namespace Barotrauma public IEnumerable FindChildren(object userData) { - return Children.Where(c => c.UserData == userData); + return Children.Where(c => Equals(c.UserData, userData)); } public IEnumerable FindChildren(Func predicate) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 5b4ae436a..2de088d11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -16,8 +16,8 @@ namespace Barotrauma public LocalizedString Tooltip; - public ContextMenuOption(string labelTag, bool isEnabled, Action onSelected) - : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + public ContextMenuOption(string label, bool isEnabled, Action onSelected) + : this(TextManager.Get(label).Fallback(label), isEnabled, onSelected) { } public ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected) : this(TextManager.Get(labelTag), isEnabled, onSelected) { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 5ec73c90f..d34f6bf91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -314,13 +314,12 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (obj == child.UserData) { tickBox.Selected = true; } + if (Equals(obj, child.UserData)) { tickBox.Selected = true; } } } else { - GUITextBlock textBlock = component as GUITextBlock; - if (textBlock == null) + if (!(component is GUITextBlock textBlock)) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index 33837ece7..d21951cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -96,15 +96,11 @@ namespace Barotrauma switch (child.ScaleBasis) { case ScaleBasis.BothHeight: - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - break; case ScaleBasis.Smallest when Rect.Height <= Rect.Width: case ScaleBasis.Largest when Rect.Height > Rect.Width: child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); break; case ScaleBasis.BothWidth: - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - break; case ScaleBasis.Smallest when Rect.Width <= Rect.Height: case ScaleBasis.Largest when Rect.Width > Rect.Height: child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index a40d29880..efaaebbc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -402,8 +402,7 @@ namespace Barotrauma int i = 0; foreach (GUIComponent child in children) { - if ((child.UserData != null && child.UserData.Equals(userData)) || - (child.UserData == null && userData == null)) + if (Equals(child.UserData, userData)) { Select(i, force, autoScroll); if (!SelectMultiple) { return; } @@ -1219,6 +1218,20 @@ namespace Barotrauma i++; } + if (isDraggingElement && CurrentDragMode == DragMode.DragOutsideBox && HideDraggedElement) + { + Rectangle drawRect = DraggedElement.Rect; + int draggedElementIndex = Content.GetChildIndex(DraggedElement); + CalculateChildrenOffsets((index, point) => + { + if (draggedElementIndex == index) + { + drawRect.Location = Content.Rect.Location + point; + } + }); + GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f); + } + if (HideChildrenOutsideFrame) { spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index ccef64092..1487d6943 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -65,7 +65,7 @@ namespace Barotrauma LoadFont(); } - private void LoadFont() + public void LoadFont() { string fontPath = GetFontFilePath(element); uint size = GetFontSize(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index ed32a00b9..93f8803cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -268,7 +268,7 @@ namespace Barotrauma } int totalCost = medicalClinic.GetTotalCost(); - healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); + healList.PriceBlock.Text = TextManager.FormatCurrency(totalCost); healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; if (medicalClinic.GetWallet().CanAfford(totalCost)) @@ -288,7 +288,7 @@ namespace Barotrauma { if (element.FindAfflictionElement(affliction) is { } existingAffliction) { - existingAffliction.Price.Text = UpgradeStore.FormatCurrency(affliction.Strength); + existingAffliction.Price.Text = TextManager.FormatCurrency(affliction.Strength); continue; } @@ -467,7 +467,7 @@ namespace Barotrauma GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { - TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetWallet().Balance), + TextGetter = () => TextManager.FormatCurrency(medicalClinic.GetWallet().Balance), AutoScaleVertical = true, TextScale = 1.1f }; @@ -571,7 +571,7 @@ namespace Barotrauma GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); @@ -684,7 +684,7 @@ namespace Barotrauma GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUIStyle.SubHeadingFont); - GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; @@ -876,7 +876,7 @@ namespace Barotrauma ToolTip = prefab.Description }; - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); @@ -931,7 +931,7 @@ namespace Barotrauma }); } - private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + public static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) { if (string.IsNullOrWhiteSpace(text)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 87c7ebe15..7d408efd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -44,6 +44,7 @@ namespace Barotrauma private bool suppressBuySell; private int buyTotal, sellTotal, sellFromSubTotal; + private GUITextBlock storeNameBlock; private GUITextBlock merchantBalanceBlock; private GUITextBlock currentSellValueBlock, newSellValueBlock; private GUIImage sellValueChangeArrow; @@ -65,6 +66,7 @@ namespace Barotrauma private Point resolutionWhenCreated; private Dictionary OwnedItems { get; } = new Dictionary(); + private Location.StoreInfo ActiveStore { get; set; } private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; @@ -238,6 +240,39 @@ namespace Barotrauma campaignUI.Campaign.CargoManager.OnItemsInSellFromSubCrateChanged += () => { needsSellingFromSubRefresh = true; }; } + public void SelectStore(Identifier identifier) + { + if (CurrentLocation?.Stores != null) + { + if (CurrentLocation.GetStore(identifier) is { } store) + { + ActiveStore = store; + if (storeNameBlock != null) + { + var storeName = TextManager.Get($"storename.{store.Identifier}"); + if (storeName.IsNullOrEmpty()) + { + storeName = TextManager.Get("store"); + } + storeNameBlock.SetRichText(storeName); + } + } + else + { + ActiveStore = null; + string msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + DebugConsole.ShowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Store.SelectStore:StoreDoesntExist", GameAnalyticsManager.ErrorSeverity.Error, msg); + } + } + else + { + ActiveStore = null; + } + RefreshItemsToSell(); + Refresh(); + } + public void Refresh(bool updateOwned = true) { UpdatePermissions(); @@ -321,7 +356,7 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreTradingIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) + storeNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) { CanBeFocused = false, ForceUpperCase = ForceUpperCase.Yes @@ -350,7 +385,7 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - merchantBalanceBlock.TextColor = CurrentLocation?.BalanceColor ?? Color.Red; + merchantBalanceBlock.TextColor = ActiveStore?.BalanceColor ?? Color.Red; return GetMerchantBalanceText(); } }; @@ -388,17 +423,17 @@ namespace Barotrauma { int balanceAfterTransaction = activeTab switch { - StoreTab.Buy => CurrentLocation.StoreCurrentBalance + buyTotal, - StoreTab.Sell => CurrentLocation.StoreCurrentBalance - sellTotal, - StoreTab.SellSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, + StoreTab.Buy => ActiveStore.Balance + buyTotal, + StoreTab.Sell => ActiveStore.Balance - sellTotal, + StoreTab.SellSub => ActiveStore.Balance - sellFromSubTotal, _ => throw new NotImplementedException(), }; - if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) + if (balanceAfterTransaction != ActiveStore.Balance) { var newStatus = CurrentLocation.GetStoreBalanceStatus(balanceAfterTransaction); - if (CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) + if (ActiveStore.ActiveBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) { - string tooltipTag = newStatus.SellPriceModifier > CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier ? + string tooltipTag = newStatus.SellPriceModifier > ActiveStore.ActiveBalanceStatus.SellPriceModifier ? "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; sellValueContainer.ToolTip = TextManager.Get(tooltipTag); currentSellValueBlock.TextColor = newStatus.Color; @@ -406,14 +441,14 @@ namespace Barotrauma sellValueChangeArrow.Visible = true; newSellValueBlock.TextColor = newStatus.Color; newSellValueBlock.Text = $"{(newStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; } } sellValueContainer.ToolTip = TextManager.Get("campaignstore.sellvaluetooltip"); - currentSellValueBlock.TextColor = CurrentLocation.BalanceColor; + currentSellValueBlock.TextColor = ActiveStore.BalanceColor; sellValueChangeArrow.Visible = false; newSellValueBlock.Text = null; - return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; } else { @@ -698,9 +733,9 @@ namespace Barotrauma if (!HasActiveTabPermissions()) { return false; } var itemsToRemove = activeTab switch { - StoreTab.Buy => new List(CargoManager.ItemsInBuyCrate), - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.Buy => new List(CargoManager.GetBuyCrateItems(ActiveStore)), + StoreTab.Sell => new List(CargoManager.GetSellCrateItems(ActiveStore)), + StoreTab.SellSub => new List(CargoManager.GetSubCrateItems(ActiveStore)), _ => throw new NotImplementedException(), }; itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); @@ -708,14 +743,13 @@ namespace Barotrauma } }; - Refresh(); ChangeStoreTab(activeTab); resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + private LocalizedString GetMerchantBalanceText() => TextManager.FormatCurrency(ActiveStore?.Balance ?? 0); - private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerWallet.Balance); + private LocalizedString GetPlayerBalanceText() => TextManager.FormatCurrency(PlayerWallet.Balance); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { @@ -746,21 +780,25 @@ namespace Barotrauma private void UpdateLocation(Location prevLocation, Location newLocation) { if (prevLocation == newLocation) { return; } - if (prevLocation?.Reputation != null) { - prevLocation.Reputation.OnReputationValueChanged = null; + prevLocation.Reputation.OnReputationValueChanged -= SetNeedsRefresh; } - if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _))) + if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtFrom(newLocation))) { selectedItemCategory = null; searchBox.Text = ""; ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; + newLocation.Reputation.OnReputationValueChanged += SetNeedsRefresh; } } + + void SetNeedsRefresh() + { + needsRefresh = true; + } } private void ChangeStoreTab(StoreTab tab) @@ -862,9 +900,9 @@ namespace Barotrauma bool hasPermissions = HasBuyPermissions; HashSet existingItemFrames = new HashSet(); - int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; + int dailySpecialCount = ActiveStore.DailySpecials.Count; - if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) + if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) { @@ -881,32 +919,32 @@ namespace Barotrauma prevDailySpecialCount = dailySpecialCount; } - foreach (PurchasedItem item in CurrentLocation.StoreStock) + foreach (PurchasedItem item in ActiveStore.Stock) { CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (ItemPrefab itemPrefab in CurrentLocation.DailySpecials) + foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) { - if (CurrentLocation.StoreStock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } + if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } CreateOrUpdateItemFrame(itemPrefab, 0); } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) { - if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo)) { - var isDailySpecial = CurrentLocation.DailySpecials.Contains(itemPrefab); + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.PurchasedItems.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem purchasedItem) + if (CargoManager.GetPurchasedItem(ActiveStore, itemPrefab) is { } purchasedItem) { quantity = Math.Max(quantity - purchasedItem.Quantity, 0); } - if (CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInBuyCrate) + if (CargoManager.GetBuyCrateItem(ActiveStore, itemPrefab) is { } buyCrateItem) { - quantity = Math.Max(quantity - itemInBuyCrate.Quantity, 0); + quantity = Math.Max(quantity - buyCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -945,7 +983,7 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Sell); HashSet existingItemFrames = new HashSet(); - if ((storeRequestedGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) + if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any()) { if (storeRequestedGoodGroup == null) { @@ -965,7 +1003,7 @@ namespace Barotrauma CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in CurrentLocation.RequestedGoods) + foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } CreateOrUpdateItemFrame(requestedGood, 0); @@ -973,15 +1011,15 @@ namespace Barotrauma void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) { - PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore); if (priceInfo == null) { return; } - var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + var isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab); var itemFrame = isRequestedGood ? storeRequestedGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeSellList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellCrate) + if (CargoManager.GetSellCrateItem(ActiveStore, itemPrefab) is { } sellCrateItem) { - itemQuantity = Math.Max(itemQuantity - itemInSellCrate.Quantity, 0); + itemQuantity = Math.Max(itemQuantity - sellCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -1023,7 +1061,7 @@ namespace Barotrauma bool hasPermissions = HasSellSubPermissions; HashSet existingItemFrames = new HashSet(); - if ((storeRequestedSubGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) + if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any()) { if (storeRequestedSubGoodGroup == null) { @@ -1043,7 +1081,7 @@ namespace Barotrauma CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in CurrentLocation.RequestedGoods) + foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } CreateOrUpdateItemFrame(requestedGood, 0); @@ -1051,15 +1089,15 @@ namespace Barotrauma void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) { - PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore); if (priceInfo == null) { return; } - var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + bool isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab); var itemFrame = isRequestedGood ? storeRequestedSubGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeSellFromSubList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellFromSubCrate) + if (CargoManager.GetSubCrateItem(ActiveStore, itemPrefab) is { } subCrateItem) { - itemQuantity = Math.Max(itemQuantity - itemInSellFromSubCrate.Quantity, 0); + itemQuantity = Math.Max(itemQuantity - subCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -1102,13 +1140,13 @@ namespace Barotrauma { if (buying) { - undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); + undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency( + ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); } else { - undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); + undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency( + ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); } } @@ -1116,11 +1154,11 @@ namespace Barotrauma { if (buying) { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); + priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); } else { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); + priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); } } } @@ -1135,21 +1173,21 @@ namespace Barotrauma { item.Quantity += 1; } - else if (playerItem.Prefab.GetPriceInfo(CurrentLocation) != null) + else if (playerItem.Prefab.GetPriceInfo(ActiveStore) != null) { itemsToSell.Add(new PurchasedItem(playerItem.Prefab, 1)); } } // Remove items from sell crate if they aren't in player inventory anymore - var itemsInCrate = new List(CargoManager.ItemsInSellCrate); + var itemsInCrate = new List(CargoManager.GetSellCrateItems(ActiveStore)); foreach (PurchasedItem crateItem in itemsInCrate) { var playerItem = itemsToSell.Find(i => i.ItemPrefab == crateItem.ItemPrefab); var playerItemQuantity = playerItem != null ? playerItem.Quantity : 0; if (crateItem.Quantity > playerItemQuantity) { - CargoManager.ModifyItemQuantityInSellCrate(crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity); + CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity); } } needsItemsToSellRefresh = false; @@ -1165,35 +1203,35 @@ namespace Barotrauma { item.Quantity += 1; } - else if (subItem.Prefab.GetPriceInfo(CurrentLocation) != null) + else if (subItem.Prefab.GetPriceInfo(ActiveStore) != null) { itemsToSellFromSub.Add(new PurchasedItem(subItem.Prefab, 1)); } } // Remove items from sell crate if they aren't on the sub anymore - var itemsInCrate = new List(CargoManager.ItemsInSellFromSubCrate); + var itemsInCrate = new List(CargoManager.GetSubCrateItems(ActiveStore)); foreach (PurchasedItem crateItem in itemsInCrate) { var subItem = itemsToSellFromSub.Find(i => i.ItemPrefab == crateItem.ItemPrefab); var subItemQuantity = subItem != null ? subItem.Quantity : 0; if (crateItem.Quantity > subItemQuantity) { - CargoManager.ModifyItemQuantityInSellFromSubCrate(crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity); + CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity); } } sellableItemsFromSubUpdateTimer = 0.0f; needsItemsToSellFromSubRefresh = false; } - private void RefreshShoppingCrateList(List items, GUIListBox listBox, StoreTab tab) + private void RefreshShoppingCrateList(IEnumerable items, GUIListBox listBox, StoreTab tab) { bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; foreach (PurchasedItem item in items) { - if (!(item.ItemPrefab.GetPriceInfo(CurrentLocation) is { } priceInfo)) { continue; } + if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } GUINumberInput numInput = null; if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { @@ -1227,9 +1265,9 @@ namespace Barotrauma { 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), + StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), _ => throw new NotImplementedException() }; totalPrice += item.Quantity * price; @@ -1265,11 +1303,11 @@ namespace Barotrauma SetConfirmButtonStatus(); } - private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.ItemsInBuyCrate, shoppingCrateBuyList, StoreTab.Buy); + private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.GetBuyCrateItems(ActiveStore), shoppingCrateBuyList, StoreTab.Buy); - private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList, StoreTab.Sell); + private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.GetSellCrateItems(ActiveStore), shoppingCrateSellList, StoreTab.Sell); - private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellSub); + private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.GetSubCrateItems(ActiveStore), shoppingCrateSellFromSubList, StoreTab.SellSub); private void SortItems(GUIListBox list, SortingMethod sortingMethod) { @@ -1316,8 +1354,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { - var sortResult = CurrentLocation.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemSellPrice(itemY.ItemPrefab)); + int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( + ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } return sortResult; } @@ -1340,8 +1378,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { - var sortResult = CurrentLocation.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); + int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( + ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } return sortResult; } @@ -1485,7 +1523,7 @@ namespace Barotrauma }; bool isSellingRelatedList = containingTab != StoreTab.Buy; bool locationHasDealOnItem = isSellingRelatedList ? - CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); + ActiveStore.RequestedGoods.Contains(pi.ItemPrefab) : ActiveStore.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) { @@ -1673,7 +1711,7 @@ namespace Barotrauma } // Add items already purchased - CargoManager?.PurchasedItems?.ForEach(pi => AddNonEmptyOwnedItems(pi)); + CargoManager?.GetPurchasedItems(ActiveStore).ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1689,7 +1727,7 @@ namespace Barotrauma void AddOwnedItem(Item item) { - if (!(item?.Prefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo)) { return; } + if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; } bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { @@ -1862,7 +1900,7 @@ namespace Barotrauma { list = mode switch { - StoreTab.Buy => CurrentLocation?.StoreStock, + StoreTab.Buy => ActiveStore?.Stock, StoreTab.Sell => itemsToSell, StoreTab.SellSub => itemsToSellFromSub, _ => throw new NotImplementedException() @@ -1876,7 +1914,7 @@ namespace Barotrauma { if (mode == StoreTab.Buy) { - var purchasedItem = CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab); + var purchasedItem = CargoManager.GetPurchasedItem(ActiveStore, item.ItemPrefab); if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } } return item.Quantity; @@ -1887,22 +1925,19 @@ namespace Barotrauma } } - private LocalizedString GetCurrencyFormatted(int amount) => - TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); - private bool ModifyBuyQuantity(PurchasedItem item, int quantity) { if (item?.ItemPrefab == null) { return false; } if (!HasBuyPermissions) { return false; } if (quantity > 0) { - var itemInCrate = CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab); - if (itemInCrate != null && itemInCrate.Quantity >= CargoManager.MaxQuantity) { return false; } + var crateItem = CargoManager.GetBuyCrateItem(ActiveStore, item.ItemPrefab); + if (crateItem != null && crateItem.Quantity >= CargoManager.MaxQuantity) { return false; } // Make sure there's enough available in the store - var totalQuantityToBuy = itemInCrate != null ? itemInCrate.Quantity + quantity : quantity; + var totalQuantityToBuy = crateItem != null ? crateItem.Quantity + quantity : quantity; if (totalQuantityToBuy > GetMaxAvailable(item.ItemPrefab, StoreTab.Buy)) { return false; } } - CargoManager.ModifyItemQuantityInBuyCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInBuyCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); return true; } @@ -1914,11 +1949,11 @@ namespace Barotrauma if (quantity > 0) { // Make sure there's enough available to sell - var itemToSell = CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var itemToSell = CargoManager.GetSellCrateItem(ActiveStore, item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; } } - CargoManager.ModifyItemQuantityInSellCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); return true; } @@ -1929,11 +1964,11 @@ namespace Barotrauma if (quantity > 0) { // Make sure there's enough available to sell - var itemToSell = CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var itemToSell = CargoManager.GetSubCrateItem(ActiveStore, item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellSub)) { return false; } } - CargoManager.ModifyItemQuantityInSellFromSubCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); return true; } @@ -1981,32 +2016,27 @@ namespace Barotrauma private bool BuyItems() { if (!HasBuyPermissions) { return false; } - - var itemsToPurchase = new List(CargoManager.ItemsInBuyCrate); + var itemsToPurchase = new List(CargoManager.GetBuyCrateItems(ActiveStore)); var itemsToRemove = new List(); - var totalPrice = 0; - foreach (PurchasedItem item in itemsToPurchase) + int totalPrice = 0; + foreach (var item in itemsToPurchase) { - if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) { itemsToRemove.Add(item); continue; } - totalPrice += item.Quantity * CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); + totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); - if (itemsToPurchase.None() || !PlayerWallet.CanAfford(totalPrice)) { return false; } - - CargoManager.PurchaseItems(itemsToPurchase, true); + CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, true); GameMain.Client?.SendCampaignState(); - var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; - return false; } @@ -2018,8 +2048,8 @@ namespace Barotrauma { itemsToSell = activeTab switch { - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.Sell => new List(CargoManager.GetSellCrateItems(ActiveStore)), + StoreTab.SellSub => new List(CargoManager.GetSubCrateItems(ActiveStore)), _ => throw new NotImplementedException() }; } @@ -2032,9 +2062,9 @@ namespace Barotrauma int totalValue = 0; foreach (PurchasedItem item in itemsToSell) { - if (item?.ItemPrefab?.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) + if (item?.ItemPrefab?.GetPriceInfo(ActiveStore) is PriceInfo priceInfo) { - totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); + totalValue += item.Quantity * ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); } else { @@ -2042,8 +2072,8 @@ namespace Barotrauma } } itemsToRemove.ForEach(i => itemsToSell.Remove(i)); - if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } - CargoManager.SellItems(itemsToSell, activeTab); + if (itemsToSell.None() || totalValue > ActiveStore.Balance) { return false; } + CargoManager.SellItems(ActiveStore.Identifier, itemsToSell, activeTab); GameMain.Client?.SendCampaignState(); return false; } @@ -2052,7 +2082,7 @@ namespace Barotrauma { if (IsBuying) { - shoppingCrateTotal.Text = GetCurrencyFormatted(buyTotal); + shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal); shoppingCrateTotal.TextColor = !PlayerWallet.CanAfford(buyTotal) ? Color.Red : Color.White; } else @@ -2063,8 +2093,8 @@ namespace Barotrauma StoreTab.SellSub => sellFromSubTotal, _ => throw new NotImplementedException(), }; - shoppingCrateTotal.Text = GetCurrencyFormatted(total); - shoppingCrateTotal.TextColor = CurrentLocation != null && total > CurrentLocation.StoreCurrentBalance ? Color.Red : Color.White; + shoppingCrateTotal.Text = TextManager.FormatCurrency(total); + shoppingCrateTotal.TextColor = CurrentLocation != null && total > ActiveStore.Balance ? Color.Red : Color.White; } } @@ -2100,8 +2130,8 @@ namespace Barotrauma activeTab switch { StoreTab.Buy => PlayerWallet.CanAfford(buyTotal), - StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, - StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, + StoreTab.Sell => CurrentLocation != null && sellTotal <= ActiveStore.Balance, + StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, _ => false }; } @@ -2124,6 +2154,7 @@ namespace Barotrauma if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); + needsRefresh = true; } else { @@ -2131,38 +2162,62 @@ namespace Barotrauma ownedItemsUpdateTimer += deltaTime; if (ownedItemsUpdateTimer >= timerUpdateInterval) { - var prevOwnedItems = new Dictionary(OwnedItems); + bool checkForRefresh = !needsItemsToSellRefresh || !needsRefresh; + var prevOwnedItems = checkForRefresh ? new Dictionary(OwnedItems) : null; UpdateOwnedItems(); - bool refresh = OwnedItems.Count != prevOwnedItems.Count || - OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || - OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || - prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); - if (refresh) + if (checkForRefresh) { - needsItemsToSellRefresh = true; - needsRefresh = true; + bool refresh = OwnedItems.Count != prevOwnedItems.Count || + OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || + OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || + prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); + if (refresh) + { + needsItemsToSellRefresh = true; + needsRefresh = true; + } } } // Update the sellable sub items at short intervals and check if the interface should be refreshed sellableItemsFromSubUpdateTimer += deltaTime; if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval) { - var prevSubItems = new List(itemsToSellFromSub); + bool checkForRefresh = !needsRefresh; + var prevSubItems = checkForRefresh ? new List(itemsToSellFromSub) : null; RefreshItemsToSellFromSub(); - needsRefresh = needsRefresh || - itemsToSellFromSub.Count != prevSubItems.Count || - itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || - itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || - prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); + if (checkForRefresh) + { + needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count || + itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || + itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); + } } } - - if (needsItemsToSellRefresh) { RefreshItemsToSell(); } - if (needsItemsToSellFromSubRefresh) { RefreshItemsToSellFromSub(); } - 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(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + if (needsItemsToSellRefresh) + { + RefreshItemsToSell(); + } + if (needsItemsToSellFromSubRefresh) + { + RefreshItemsToSellFromSub(); + } + 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(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + } updateStopwatch.Stop(); GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 4272b8dcc..152424061 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -31,7 +31,7 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; @@ -99,8 +99,7 @@ namespace Barotrauma priceText = TextManager.Get("price"); } - currencyShorthandText = TextManager.Get("currencyformat"); - currencyLongText = TextManager.Get("credit").Value.ToLowerInvariant(); + currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); UpdateSubmarines(); missingPreviewText = TextManager.Get("SubPreviewImageNotFound"); @@ -335,7 +334,7 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - LocalizedString amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); + LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -344,7 +343,7 @@ namespace Barotrauma { if (deliveryFee > 0) { - LocalizedString amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); + LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -584,7 +583,7 @@ namespace Barotrauma if (!GameMain.GameSession.Campaign.Wallet.CanAfford(deliveryFee) && deliveryFee > 0) { new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename]", selectedSubmarine.DisplayName), ("[location1]", deliveryLocationName), ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); @@ -601,7 +600,7 @@ namespace Barotrauma ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyLongText)), messageBoxOptions); + ("[currencyname]", currencyName)), messageBoxOptions); } else { @@ -632,7 +631,7 @@ namespace Barotrauma if (!GameMain.GameSession.Campaign.Wallet.CanAfford(selectedSubmarine.Price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename]", selectedSubmarine.DisplayName))); return; } @@ -644,7 +643,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => @@ -667,7 +666,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), - ("[currencyname]", currencyLongText)), messageBoxOptions); + ("[currencyname]", currencyName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 201d8d242..abd866462 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -218,7 +218,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { - infoFrame?.AddToGUIUpdateList(); + infoFrame?.AddToGUIUpdateList(order: 1); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); } @@ -379,8 +379,7 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - // FIXME remove TestScreen stuff - crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? Array.Empty(); teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -817,8 +816,11 @@ namespace Barotrauma else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } - CreateWalletFrame(background, client.Character); + GameMain.Client?.SelectCrewClient(client, preview); + if (client.Character != null) + { + CreateWalletFrame(background, client.Character); + } } return true; @@ -845,22 +847,23 @@ namespace Barotrauma float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X; GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont); - GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); + GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform)); GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true); GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()), textAlignment: Alignment.BottomRight); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight); GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f) { - Range = Vector2.UnitY, + ToolTip = TextManager.Get("crewwallet.salary.tooltip"), + Range = new Vector2(0, 1), BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, BarSize = 0.1f, OnMoved = (bar, scroll) => { - rewardBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()); + SetRewardText((int)(scroll * 100), rewardBlock); return true; }, OnReleased = (bar, scroll) => @@ -871,6 +874,9 @@ namespace Barotrauma return true; } }; + + SetRewardText(targetWallet.RewardDistribution, rewardBlock); + // @formatter:off GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) { @@ -883,7 +889,7 @@ namespace Barotrauma GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform)); GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; + GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight); GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red }; @@ -902,8 +908,11 @@ namespace Barotrauma GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; // @formatter:on + ImmutableArray layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout); + MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups); transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") { + ToolTip = TextManager.Get("crewwallet.transfer.tooltip"), OnClicked = (button, o) => { isTransferMenuOpen = !isTransferMenuOpen; @@ -951,13 +960,15 @@ namespace Barotrauma break; } + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + if (!hasPermissions) { centerButton.Enabled = centerButton.CanBeFocused = false; salarySlider.Enabled = salarySlider.CanBeFocused = false; } - leftBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); UpdateAllInputs(); @@ -983,7 +994,7 @@ namespace Barotrauma { if (e.Wallet == targetWallet) { - moneyBlock.Text = UpgradeStore.FormatCurrency(e.Info.Balance); + moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; } UpdateAllInputs(); @@ -1022,23 +1033,23 @@ namespace Barotrauma confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; if (transferAmountInput.IntValue == 0) { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); rightBalance.TextColor = GUIStyle.TextColorNormal; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); leftBalance.TextColor = GUIStyle.TextColorNormal; } else if (isSending) { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); rightBalance.TextColor = GUIStyle.Blue; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); leftBalance.TextColor = GUIStyle.Red; } else { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); rightBalance.TextColor = GUIStyle.Red; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); leftBalance.TextColor = GUIStyle.Blue; } } @@ -1073,14 +1084,14 @@ namespace Barotrauma Receiver = to.Select(option => option.ID), Amount = amount }; - IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY); transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } static void SetRewardDistribution(Character character, int newValue) { - INetSerializableStruct transfer = new NetWalletSalaryUpdate + INetSerializableStruct transfer = new NetWalletSetSalaryUpdate { Target = character.ID, NewRewardDistribution = newValue @@ -1090,7 +1101,23 @@ namespace Barotrauma GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - string GetSharePercentage() => Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage.ToString(); + void SetRewardText(int value, GUITextBlock block) + { + var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option.None()); + LocalizedString tooltip = string.Empty; + block.TextColor = GUIStyle.TextColorNormal; + + if (sum > 100) + { + tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}")); + block.TextColor = GUIStyle.Orange; + } + + LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}"); + + block.Text = text; + block.ToolTip = RichString.Rich(tooltip); + } } private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f4ec7f408..a6a0f3a54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -287,7 +287,7 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(PlayerWallet.Balance, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(PlayerWallet.Balance, format: true) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.FormatCurrency(PlayerWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => TextManager.FormatCurrency(PlayerWallet.Balance) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => @@ -571,7 +571,7 @@ namespace Barotrauma var repairIcon = new GUIFrame(rectT(new Point(contentLayout.Rect.Height, contentLayout.Rect.Height), contentLayout), style: imageStyle); GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; - new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); + new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerWallet.Balance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); @@ -1094,7 +1094,7 @@ namespace Barotrauma if (addBuyButton) { - var formattedPrice = FormatCurrency(Math.Abs(price)); + var formattedPrice = TextManager.FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; @@ -1577,7 +1577,7 @@ namespace Barotrauma if (priceLabel != null && !WaitForServerUpdate) { - priceLabel.Text = FormatCurrency(price); + priceLabel.Text = TextManager.FormatCurrency(price); if (currentLevel >= prefab.MaxLevel) { priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); @@ -1695,11 +1695,6 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - public static LocalizedString FormatCurrency(int money, bool format = true) - { - return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); - } - // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me) private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index c1ed2807f..e4c8ab417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1038,9 +1038,9 @@ namespace Barotrauma // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedOutpost && spCampaign.Map?.CurrentLocation != null && spCampaign.CargoManager != null) { - spCampaign.Map.CurrentLocation.AddToStock(spCampaign.CargoManager.SoldItems); + spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); spCampaign.CargoManager.ClearSoldItemsProjSpecific(); - spCampaign.Map.CurrentLocation.RemoveFromStock(spCampaign.CargoManager.PurchasedItems); + spCampaign.Map.CurrentLocation.RemoveStock(spCampaign.CargoManager.PurchasedItems); } SaveUtil.SaveGame(GameSession.SavePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 3c9ac933b..29cea3e9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -45,28 +45,37 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } - public void SetItemsInBuyCrate(List items) + public void SetItemsInBuyCrate(Dictionary> items) { ItemsInBuyCrate.Clear(); - ItemsInBuyCrate.AddRange(items); + foreach (var entry in items) + { + ItemsInBuyCrate.Add(entry.Key, entry.Value); + } OnItemsInBuyCrateChanged?.Invoke(); } - public void SetItemsInSubSellCrate(List items) + public void SetItemsInSubSellCrate(Dictionary> items) { ItemsInSellFromSubCrate.Clear(); - ItemsInSellFromSubCrate.AddRange(items); + foreach (var entry in items) + { + ItemsInSellFromSubCrate.Add(entry.Key, entry.Value); + } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void SetSoldItems(List items) + public void SetSoldItems(Dictionary> items) { SoldItems.Clear(); - SoldItems.AddRange(items); + foreach (var entry in items) + { + SoldItems.Add(entry.Key, entry.Value); + } foreach (var se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } - if (SoldItems.Any(si => Match(si, se, true))) + if (SoldItems.Any(si => si.Value.Any(si => Match(si, se, true)))) { se.Status = SoldEntity.SellStatus.Confirmed; } @@ -75,13 +84,16 @@ namespace Barotrauma se.Status = SoldEntity.SellStatus.Unconfirmed; } } - foreach (var si in SoldItems) + foreach (var soldItems in SoldItems.Values) { - 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; + 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(); @@ -94,45 +106,24 @@ namespace Barotrauma } } - public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity) { - var itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemToSell != null) + if (GetSellCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemToSell.Quantity += changeInQuantity; - if (itemToSell.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInSellCrate.Remove(itemToSell); + GetSellCrateItems(storeIdentifier)?.Remove(item); } } else if (changeInQuantity > 0) { - itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); - ItemsInSellCrate.Add(itemToSell); + GetSellCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity)); } OnItemsInSellCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSellFromSubCrate(ItemPrefab itemPrefab, int changeInQuantity) - { - var itemToSell = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemToSell != null) - { - itemToSell.Quantity += changeInQuantity; - if (itemToSell.Quantity < 1) - { - ItemsInSellFromSubCrate.Remove(itemToSell); - } - } - else if (changeInQuantity > 0) - { - itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); - ItemsInSellFromSubCrate.Add(itemToSell); - } - OnItemsInSellFromSubCrateChanged?.Invoke(); - } - - public void SellItems(List itemsToSell, Store.StoreTab sellingMode) + public void SellItems(Identifier storeIdentifier, List itemsToSell, Store.StoreTab sellingMode) { IEnumerable sellableItems; try @@ -146,19 +137,24 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error selling items: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.ShowError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; 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) + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); + if (!(Location.GetStore(storeIdentifier) is { } store)) + { + DebugConsole.ShowError($"Error selling items at {Location}: no store with identifier \"{storeIdentifier}\" exists.\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + var storeSpecificSoldItems = GetSoldItems(storeIdentifier, create: true); + foreach (var item in itemsToSell) { int itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item - if (Location.StoreCurrentBalance < itemValue) { continue; } + if (store.Balance < 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); int count = Math.Min(item.Quantity, matchingItems.Count()); @@ -168,7 +164,7 @@ namespace Barotrauma for (int i = 0; i < count; i++) { var matchingItem = matchingItems.ElementAt(i); - SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); + storeSpecificSoldItems.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.AddItemToRemoveQueue(matchingItem); } } @@ -178,22 +174,23 @@ namespace Barotrauma // When selling from the sub in multiplayer, the server will determine the items that are sold for (int i = 0; i < count; i++) { - SoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); + storeSpecificSoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local)); } } // Exchange money - Location.StoreCurrentBalance -= itemValue; + store.Balance -= itemValue; campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate - if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) + var sellCrate = (sellingMode == Store.StoreTab.Sell ? GetSellCrateItems(storeIdentifier) : GetSubCrateItems(storeIdentifier)); + if (sellCrate?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { itemToSell.Quantity -= item.Quantity; if (itemToSell.Quantity < 1) { - (sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Remove(itemToSell); + sellCrate.Remove(itemToSell); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 7186c3bd9..6d334c7a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,6 +63,10 @@ namespace Barotrauma } } + /// + /// Gets the current personal wallet + /// In singleplayer this is the campaign bank and in multiplayer this is the personal wallet + /// public virtual Wallet Wallet => GetWallet(); public override void ShowStartMessage() @@ -301,7 +305,7 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType); + CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); CampaignUI.UpgradeStore?.RefreshAll(); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 597939e79..2847da846 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -553,36 +553,10 @@ namespace Barotrauma msg.Write(PurchasedItemRepairs); msg.Write(PurchasedLostShuttles); - msg.Write((UInt16)CargoManager.ItemsInBuyCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - 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, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.SoldItems.Count); - foreach (SoldItem si in CargoManager.SoldItems) - { - msg.Write(si.ItemPrefab.Identifier); - msg.Write((UInt16)si.ID); - msg.Write(si.Removed); - msg.Write(si.SellerID); - msg.Write((byte)si.Origin); - } + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteItems(msg, CargoManager.PurchasedItems); + WriteItems(msg, CargoManager.SoldItems); msg.Write((ushort)UpgradeManager.PurchasedUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades) @@ -644,50 +618,22 @@ namespace Barotrauma availableMissions.Add((missionIdentifier, connectionIndex)); } - UInt16? storeBalance = null; + var storeBalances = new Dictionary(); if (msg.ReadBoolean()) { - storeBalance = msg.ReadUInt16(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier identifier = msg.ReadIdentifier(); + UInt16 storeBalance = msg.ReadUInt16(); + storeBalances.Add(identifier, storeBalance); + } } - UInt16 buyCrateItemCount = msg.ReadUInt16(); - List buyCrateItems = new List(); - for (int i = 0; i < buyCrateItemCount; i++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - 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++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); - } - - UInt16 soldItemCount = msg.ReadUInt16(); - List soldItems = new List(); - for (int i = 0; i < soldItemCount; i++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - UInt16 id = msg.ReadUInt16(); - bool removed = msg.ReadBoolean(); - byte sellerId = msg.ReadByte(); - byte origin = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); - } + var buyCrateItems = ReadPurchasedItems(msg, sender: null); + var subSellCrateItems = ReadPurchasedItems(msg, sender: null); + var purchasedItems = ReadPurchasedItems(msg, sender: null); + var soldItems = ReadSoldItems(msg); ushort pendingUpgradeCount = msg.ReadUInt16(); List pendingUpgrades = new List(); @@ -756,7 +702,13 @@ namespace Barotrauma campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.CargoManager.SetSoldItems(soldItems); - if (storeBalance.HasValue) { campaign.Map.CurrentLocation.StoreCurrentBalance = storeBalance.Value; } + foreach (var balance in storeBalances) + { + if (campaign.Map.CurrentLocation.GetStore(balance.Key) is { } store) + { + store.Balance = balance.Value; + } + } campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); campaign.UpgradeManager.PurchasedUpgrades.Clear(); foreach (var purchasedItemSwap in purchasedItemSwaps) @@ -914,7 +866,7 @@ namespace Barotrauma WalletInfo info = transaction.Info; switch (transaction.CharacterID) { - case Some { Value: var charID}: + case Some { Value: var charID }: { Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); if (targetCharacter is null) { break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index da8a40fad..891fc70b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["engineer"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 0), new Skill("weapons".ToIdentifier(), 0), new Skill("mechanical".ToIdentifier(), 20), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index b26b2d32e..a9ea9047a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -147,7 +147,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["mechanic"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 0), new Skill("weapons".ToIdentifier(), 0), new Skill("mechanical".ToIdentifier(), 50), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 68d91df57..751cc4cd8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -128,7 +128,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["securityofficer"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 20), new Skill("weapons".ToIdentifier(), 70), new Skill("mechanical".ToIdentifier(), 20), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 2770fc3c0..abe401df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -318,7 +318,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew().Where(c => c != controlled), Option.Some(reward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index e19ace1a9..4746f0e9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -966,7 +966,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index ad2a8cdf2..cc05194e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } - string spritePath = spriteElement.Attribute("texture").Value; + string spritePath = spriteElement.GetAttribute("texture").Value; spritePath = characterInfo.ReplaceVars(spritePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index b153c5b80..02ad190c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -462,7 +462,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "guiframe": - if (subElement.Attribute("rect") != null) + if (subElement.GetAttribute("rect") != null) { DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 597c90daa..5fe3adde1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,8 +1,7 @@ -using System; -using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 7c7821844..39e714c89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -2,12 +2,14 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ItemLabel : ItemComponent, IDrawableComponent + partial class ItemLabel : ItemComponent, IDrawableComponent, IHasExtraTextPickerEntries { private GUITextBlock textBlock; @@ -94,7 +96,15 @@ namespace Barotrauma.Items.Components get { return textBlock == null ? 1.0f : textBlock.TextScale; } set { - if (textBlock != null) { textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); } + if (textBlock != null) + { + float prevScale = TextBlock.TextScale; + textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); + if (!MathUtils.NearlyEqual(prevScale, TextBlock.TextScale)) + { + SetScrollingText(); + } + } } } @@ -106,7 +116,7 @@ namespace Barotrauma.Items.Components set { scrollable = value; - IsActive = value; + IsActive = value || parseSpecialTextTagOnStart; TextBlock.Wrap = !scrollable; TextBlock.TextAlignment = scrollable ? Alignment.CenterLeft : Alignment.Center; } @@ -136,18 +146,23 @@ namespace Barotrauma.Items.Components { } + public IEnumerable GetExtraTextPickerEntries() + { + return SpecialTextTags; + } + private void SetScrollingText() { if (!scrollable) { return; } - float totalWidth = textBlock.Font.MeasureString(DisplayText).X; + float totalWidth = textBlock.Font.MeasureString(DisplayText).X * TextBlock.TextScale; float textAreaWidth = Math.Max(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z, 0); if (totalWidth >= textAreaWidth) { //add enough spaces to fill the rect //(so the text can scroll entirely out of view before we reset it back to start) needsScrolling = true; - float spaceWidth = textBlock.Font.MeasureChar(' ').X; + float spaceWidth = textBlock.Font.MeasureChar(' ').X * TextBlock.TextScale; scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText.Value; } else @@ -166,7 +181,7 @@ namespace Barotrauma.Items.Components charWidths = new float[scrollingText.Length]; for (int i = 0; i < scrollingText.Length; i++) { - float charWidth = TextBlock.Font.MeasureChar(scrollingText[i]).X; + float charWidth = TextBlock.Font.MeasureChar(scrollingText[i]).X * TextBlock.TextScale; scrollPadding = Math.Max(charWidth, scrollPadding); charWidths[i] = charWidth; } @@ -174,9 +189,18 @@ namespace Barotrauma.Items.Components scrollIndex = MathHelper.Clamp(scrollIndex, 0, DisplayText.Length); } + private static readonly string[] SpecialTextTags = new string[] { "[CurrentLocationName]", "[CurrentBiomeName]", "[CurrentSubName]" }; + private bool parseSpecialTextTagOnStart; private void SetDisplayText(string value) { + if (SpecialTextTags.Contains(value)) + { + parseSpecialTextTagOnStart = true; + IsActive = true; + } + DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); + TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -198,9 +222,37 @@ namespace Barotrauma.Items.Components }; } + private void ParseSpecialTextTag() + { + switch (text) + { + case "[CurrentLocationName]": + SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); + break; + case "[CurrentBiomeName]": + SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); + break; + case "[CurrentSubName]": + SetDisplayText(item.Submarine?.Info?.DisplayName.Value ?? string.Empty); + break; + default: + break; + } + } + public override void Update(float deltaTime, Camera cam) { - if (!scrollable) { return; } + if (parseSpecialTextTagOnStart) + { + ParseSpecialTextTag(); + parseSpecialTextTagOnStart = false; + } + + if (!scrollable) + { + IsActive = false; + return; + } if (scrollingText == null) { @@ -286,5 +338,6 @@ namespace Barotrauma.Items.Components { Text = msg.ReadString(); } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7d2a5101f..433243ee3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection.Metadata; @@ -46,9 +47,12 @@ namespace Barotrauma.Items.Components private GUITextBlock requiredTimeBlock; + [Serialize("FabricatorCreate", IsPropertySaveable.Yes)] + public string CreateButtonText { get; set; } + partial void InitProjSpecific() { - CreateGUI(); + //CreateGUI(); } protected override void OnResolutionChanged() @@ -68,9 +72,11 @@ namespace Barotrauma.Items.Components AutoScaleVertical = true }; - var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.02f + RelativeSpacing = 0.02f, + Stretch = true, + CanBeFocused = true }; // === TOP AREA === @@ -131,41 +137,55 @@ namespace Barotrauma.Items.Components // === BOTTOM AREA === // var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.3f), mainFrame.RectTransform), style: null); + if (inputContainer.Capacity > 0) + { // === SEPARATOR === // var separatorArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), bottomFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true) { - Stretch = true, + Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; - inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); - new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + inputLabel.RectTransform.Resize(new Point((int)inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); + new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); // === INPUT AREA === // var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1f), bottomFrame.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft); - - // === INPUT SLOTS === // - inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); - new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; - // === ACTIVATE BUTTON === // - var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); - activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), - TextManager.Get("FabricatorCreate"), style: "DeviceButton") - { - OnClicked = StartButtonClicked, - UserData = selectedItem, - Enabled = false - }; - // === POWER WARNING === // - inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) - { - HoverColor = Color.Black, - IgnoreLayoutGroups = true, - Visible = false, - CanBeFocused = false - }; + // === INPUT SLOTS === // + inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); + new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; + + // === ACTIVATE BUTTON === // + var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); + activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), + TextManager.Get(CreateButtonText), style: "DeviceButton") + { + OnClicked = StartButtonClicked, + UserData = selectedItem, + Enabled = false + }; + } + else + { + bottomFrame.RectTransform.RelativeSize = new Vector2(1.0f, 0.1f); + activateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomFrame.RectTransform, Anchor.CenterRight), + TextManager.Get(CreateButtonText), style: "DeviceButton") + { + OnClicked = StartButtonClicked, + UserData = selectedItem, + Enabled = false + }; + } + // === POWER WARNING === // + inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), + TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) + { + HoverColor = Color.Black, + IgnoreLayoutGroups = true, + Visible = false, + CanBeFocused = false + }; CreateRecipes(); } @@ -222,8 +242,12 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific() { - inputContainer.AllowUIOverlap = true; - inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; + CreateGUI(); + if (inputInventoryHolder != null) + { + inputContainer.AllowUIOverlap = true; + inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; + } outputContainer.AllowUIOverlap = true; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } @@ -262,7 +286,7 @@ namespace Barotrauma.Items.Components var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUIStyle.SubHeadingFont) - { + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -271,10 +295,14 @@ namespace Barotrauma.Items.Components { insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform)); } + else + { + sufficientSkillsText.Visible = false; + } var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont) - { + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -593,14 +621,28 @@ namespace Barotrauma.Items.Components float requiredTime = overrideRequiredTime ?? (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + if (requiredTime > 0.0f) { - AutoScaleHorizontal = true, - }; - - requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), - font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), + TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + { + AutoScaleHorizontal = true, + }; + requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), + font: GUIStyle.SmallFont); + } + + if (SelectedItem.RequiredMoney > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), + TextManager.Get("subeditor.price"), textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + { + AutoScaleHorizontal = true, + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney), + font: GUIStyle.SmallFont); + + } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 2dc8ec191..36713eab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -34,6 +34,13 @@ namespace Barotrauma.Items.Components [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); + [Serialize(true, IsPropertySaveable.No, description: "")] + public bool BreakFromMiddle + { + get; + set; + } + public Vector2 DrawSize { get @@ -124,9 +131,14 @@ namespace Barotrauma.Items.Components int width = (int)(SpriteWidth * snapState); if (width > 0.0f) - { - DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); - DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); + { + float positionMultiplier = snapState; + if (BreakFromMiddle) + { + positionMultiplier /= 2; + DrawRope(spriteBatch, endPos - diff * positionMultiplier, endPos, width); + } + DrawRope(spriteBatch, startPos, startPos + diff * positionMultiplier, width); } } else @@ -143,7 +155,7 @@ namespace Barotrauma.Items.Components float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); } - if (endSprite != null) + if (endSprite != null && (!Snapped || BreakFromMiddle)) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 1c27aaf3c..6224c628c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1846,7 +1846,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private void ApplyReceivedState() + public void ApplyReceivedState() { if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 8cb513864..3cc3610e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1198,9 +1198,9 @@ namespace Barotrauma Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable) + if (ic is Repairable r) { - if (!IsFullCondition) { color = Color.Cyan; } + if (r.IsBelowRepairThreshold) { color = Color.Cyan; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 860e59d1a..864384257 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -173,7 +173,7 @@ namespace Barotrauma int groupID = 0; DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { groupID = subElement.GetAttributeInt("randomgroupid", 0); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 80d814e5f..264b5b2e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -370,6 +370,7 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); + bool isRootGrowth = msg.ReadBoolean(); byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); @@ -385,7 +386,8 @@ namespace Barotrauma.MapCreatures.Behavior { ID = id, MaxHealth = maxHealth, - Sides = (TileSide) sides + Sides = (TileSide) sides, + IsRootGrowth = isRootGrowth }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 92112a32a..0b68a66ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -217,8 +217,8 @@ namespace Barotrauma 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)); - int endY = (int)Math.Min(Math.Floor(location.MapPosition.Y / mapTileSize.Y + 0.25f), mapTiles.GetLength(1)); + int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0) - 1); + int endY = (int)Math.Min(Math.Floor(location.MapPosition.Y / mapTileSize.Y + 0.25f), mapTiles.GetLength(1) - 1); for (int x = startX; x <= endX; x++) { for (int y = startY; y <= endY; y++) @@ -451,7 +451,7 @@ namespace Barotrauma SelectLocation(-1); if (GameMain.Client == null) { - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); ProgressWorld(); Radiation?.OnStep(1); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 8cd2acaaf..e69d0bbb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -272,11 +272,7 @@ namespace Barotrauma private GUIComponent CreateEditingHUD() { - int width = 500; - int height = spawnType == SpawnType.Path ? 80 : 200; - int x = GameMain.GraphicsWidth / 2 - width / 2, y = 30; - - editingHUD = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas) { ScreenSpaceOffset = new Point(x, y) }) + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; @@ -284,7 +280,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) { Stretch = true, - RelativeSpacing = 0.05f + AbsoluteSpacing = (int)(GUI.Scale * 5) }; if (spawnType == SpawnType.Path) @@ -418,6 +414,10 @@ namespace Barotrauma }; } + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y))); + PositionEditingHUD(); return editingHUD; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 4c2721291..8f046b69d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -155,8 +155,13 @@ namespace Barotrauma.Networking Dispose(true); } } - - const int MaxFileSize = 50000000; //50 MB + + private static int GetMaxFileSizeInBytes(FileTransferType fileTransferType) => + fileTransferType switch + { + FileTransferType.Mod => 500 * 1024 * 1024, //500 MiB should be good enough, right? + _ => 50 * 1024 * 1024 //50 MiB for everything other than mods + }; public delegate void TransferInDelegate(FileTransferIn fileStreamReceiver); public TransferInDelegate OnFinished; @@ -410,18 +415,18 @@ namespace Barotrauma.Networking { errorMessage = ""; - if (fileSize > MaxFileSize) - { - errorMessage = "File too large (" + MathUtils.GetBytesReadable(fileSize) + ")"; - return false; - } - if (!Enum.IsDefined(typeof(FileTransferType), (int)type)) { errorMessage = "Unknown file type"; return false; } + if (fileSize > GetMaxFileSizeInBytes((FileTransferType)type)) + { + errorMessage = $"File too large ({MathUtils.GetBytesReadable(fileSize)} > {MathUtils.GetBytesReadable(GetMaxFileSizeInBytes((FileTransferType)type))})"; + return false; + } + if (string.IsNullOrEmpty(fileName) || fileName.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 98073f188..cf2caabed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -163,16 +163,11 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - var config = GameSettings.CurrentConfig; - config.Audio.VoiceCaptureDevice = deviceName; - GameSettings.SetCurrentConfig(config); + if (Instance == null) { return; } - if (Instance != null) - { - UInt16 storedBufferID = Instance.LatestBufferID; - Instance.Dispose(); - Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); - } + UInt16 storedBufferID = Instance.LatestBufferID; + Instance.Dispose(); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } IntPtr nativeBuffer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 85570fcbd..8ce374562 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -54,7 +54,17 @@ namespace Barotrauma.Networking } else { - if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + try + { + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"VoipCature.Create failed: {e.Message} {e.StackTrace.CleanupStackTrace()}"); + var config = GameSettings.CurrentConfig; + config.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(config); + } if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } @@ -146,7 +156,7 @@ namespace Barotrauma.Networking { var soundIconStyle = GUIStyle.GetComponentStyle("GUISoundIcon"); Rectangle sourceRect = soundIconStyle.Sprites.First().Value.First().Sprite.SourceRect; - var indexPieces = soundIconStyle.Element.Attribute("sheetindices").Value.Split(';'); + var indexPieces = soundIconStyle.Element.GetAttribute("sheetindices").Value.Split(';'); voiceIconSheetRects = new Rectangle[indexPieces.Length]; for (int i = 0; i < indexPieces.Length; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index de2a858cc..519af766f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -242,30 +242,30 @@ namespace Barotrauma.Particles } //if velocity change in water is not given, it defaults to the normal velocity change - if (element.Attribute("velocitychangewater") == null) + if (element.GetAttribute("velocitychangewater") == null) { VelocityChangeWater = VelocityChange; } - if (element.Attribute("angularvelocity") != null) + if (element.GetAttribute("angularvelocity") != null) { AngularVelocityMin = element.GetAttributeFloat("angularvelocity", 0.0f); AngularVelocityMax = AngularVelocityMin; } - if (element.Attribute("startsize") != null) + if (element.GetAttribute("startsize") != null) { StartSizeMin = element.GetAttributeVector2("startsize", Vector2.One); StartSizeMax = StartSizeMin; } - if (element.Attribute("sizechange") != null) + if (element.GetAttribute("sizechange") != null) { SizeChangeMin = element.GetAttributeVector2("sizechange", Vector2.Zero); SizeChangeMax = SizeChangeMin; } - if (element.Attribute("startrotation") != null) + if (element.GetAttribute("startrotation") != null) { StartRotationMin = element.GetAttributeFloat("startrotation", 0.0f); StartRotationMax = StartRotationMin; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e8b404bb2..efbb7b0cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -680,7 +680,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab) + public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -724,8 +724,7 @@ namespace Barotrauma } break; case CampaignMode.InteractionType.Store: - Store.RefreshItemsToSell(); - Store.Refresh(); + Store.SelectStore(storeIdentifier); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 56032d102..e9de25b54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2805,7 +2805,8 @@ namespace Barotrauma.CharacterEditor return false; } #endif - if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) + ContentPath texturePath = ContentPath.FromRaw(character.Prefab.ContentPackage, RagdollParams.Texture); + if (!character.IsHuman && (texturePath.IsNullOrWhiteSpace() || !File.Exists(texturePath.Value))) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index e58af763e..4efe53c5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -248,18 +248,24 @@ namespace Barotrauma } spriteBatch.End(); - //draw characters with deformable limbs last, because they can't be batched into SpriteBatch - //pretty hacky way of preventing draw order issues between normal and deformable sprites spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) - for (int i = Character.CharacterList.Count - 1; i >= 0; i--) - { - Character c = Character.CharacterList[i]; - if (!c.IsVisible || c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } - c.Draw(spriteBatch, Cam); - } + DrawDeformed(firstPass: true); + DrawDeformed(firstPass: false); spriteBatch.End(); + void DrawDeformed(bool firstPass) + { + //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) + for (int i = Character.CharacterList.Count - 1; i >= 0; i--) + { + Character c = Character.CharacterList[i]; + if (!c.IsVisible) { continue; } + if (c.Params.DrawLast == firstPass) { continue; } + if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + c.Draw(spriteBatch, Cam); + } + } + Level.Loaded?.DrawFront(spriteBatch, cam); //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 470977caf..a1bbea8ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -32,7 +32,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private Sprite editingSprite; @@ -126,6 +126,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { SerializeAll(); + GUI.AddMessage(TextManager.Get("leveleditor.allsaved"), GUIStyle.Green); return true; } }; @@ -169,6 +170,12 @@ namespace Barotrauma mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); + allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), + TextManager.Get("leveleditor.allowinvalidoutpost")) + { + ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") + }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -179,6 +186,7 @@ namespace Barotrauma GameMain.LightManager.ClearLights(); LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + levelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; Level.Generate(levelData, mirror: mirrorLevel.Selected); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0ff737f3c..7fb77b452 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,6 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; + private GUIDropDown serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -418,7 +419,14 @@ namespace Barotrauma //PLACEHOLDER var tutorialList = new GUIListBox( new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); - var tutorialTypes = ReflectionUtils.GetDerivedNonAbstract(); + var tutorialTypes = new List() + { + typeof(MechanicTutorial), + typeof(EngineerTutorial), + typeof(DoctorTutorial), + typeof(OfficerTutorial), + typeof(CaptainTutorial), + }; foreach (Type tutorialType in tutorialTypes) { Tutorial tutorial = (Tutorial)Activator.CreateInstance(tutorialType); @@ -557,6 +565,35 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); return true; } + + serverExecutableDropdown.ListBox.Content.Children.ToArray() + .Where(c => c.UserData is ServerExecutableFile f && !ContentPackageManager.EnabledPackages.All.Contains(f.ContentPackage)) + .ForEach(serverExecutableDropdown.ListBox.RemoveChild); + var newServerExes + = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()) + .Where(f => serverExecutableDropdown.ListBox.Content.Children.None(c => c.UserData == f)) + .ToArray(); + foreach (var newServerExe in newServerExes) + { + serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + } + serverExecutableDropdown.ListBox.Content.Children.ForEach(c => + { + c.RectTransform.RelativeSize = (1.0f, c.RectTransform.RelativeSize.Y); + c.ForceLayoutRecalculation(); + }); + bool serverExePickable = serverExecutableDropdown.ListBox.Content.CountChildren > 1; + serverExecutableDropdown.Parent.Visible + = serverExePickable; + serverExecutableDropdown.Parent.RectTransform.RelativeSize + = (1.0f, serverExePickable ? 0.1f : 0.0f); + serverExecutableDropdown.Parent.ForceLayoutRecalculation(); + (serverExecutableDropdown.Parent.Parent as GUILayoutGroup)?.Recalculate(); + if (serverExecutableDropdown.SelectedComponent is null) + { + serverExecutableDropdown.Select(0); + } + break; case Tab.Tutorials: if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) @@ -784,7 +821,7 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = "DedicatedServer.exe"; + string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -814,15 +851,20 @@ namespace Barotrauma arguments += " -ownerkey " + ownerKey; } - string filename = exeName; -#if LINUX || OSX - filename = "./" + Path.GetFileNameWithoutExtension(exeName); - //arguments = ToolBox.EscapeCharacters(arguments); + string filename = Path.Combine( + Path.GetDirectoryName(exeName), + Path.GetFileNameWithoutExtension(exeName)); +#if WINDOWS + filename += ".exe"; +#else + filename = "./" + exeName; #endif + var processInfo = new ProcessStartInfo { FileName = filename, Arguments = arguments, + WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG CreateNoWindow = true, UseShellExecute = false, @@ -1184,12 +1226,12 @@ namespace Barotrauma label.RectTransform.MaxSize = serverNameBox.RectTransform.MaxSize; var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); - var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) + var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.1f }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, OnClicked = ChangeMaxPlayers @@ -1209,7 +1251,7 @@ namespace Barotrauma currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); maxPlayersBox.Text = currMaxPlayers.ToString(); }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, OnClicked = ChangeMaxPlayers @@ -1223,6 +1265,41 @@ namespace Barotrauma }; label.RectTransform.MaxSize = passwordBox.RectTransform.MaxSize; + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("ServerExecutable"), textAlignment: textAlignment); + const string vanillaServerOption = "Vanilla"; + serverExecutableDropdown + = new GUIDropDown(new RectTransform(textFieldSize, serverExecutableLabel.RectTransform, Anchor.CenterRight), + vanillaServerOption); + var listBoxSize = serverExecutableDropdown.ListBox.RectTransform.RelativeSize; + serverExecutableDropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.5f, listBoxSize.Y); + serverExecutableDropdown.AddItem(vanillaServerOption, userData: null); + serverExecutableDropdown.OnSelected = (selected, userData) => + { + if (userData != null) + { + var warningBox = new GUIMessageBox(headerText: TextManager.Get("Warning"), + text: TextManager.GetWithVariable("ModServerExesAtYourOwnRisk", "[exename]", serverExecutableDropdown.Text), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + warningBox.Buttons[0].OnClicked = (_, __) => + { + warningBox.Close(); + return false; + }; + warningBox.Buttons[1].OnClicked = (_, __) => + { + serverExecutableDropdown.Select(0); + warningBox.Close(); + return false; + }; + } + + serverExecutableDropdown.Text = ToolBox.LimitString(serverExecutableDropdown.Text, + serverExecutableDropdown.Font, serverExecutableDropdown.Rect.Width * 8 / 10); + + return true; + }; + // tickbox upper --------------- var tickboxAreaUpper = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, tickBoxSize.Y), parent.RectTransform), isHorizontal: true); @@ -1312,8 +1389,8 @@ namespace Barotrauma { var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); - client.ExecuteAsync(request, RemoteContentReceived); - CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); + TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), + RemoteContentReceived); } catch (Exception e) @@ -1327,58 +1404,31 @@ namespace Barotrauma } } - private IEnumerable WairForRemoteContentReceived() + private void RemoteContentReceived(Task t) { - while (true) + try { - lock (remoteContentLock) + if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + string xml = remoteContentResponse.Content; + int index = xml.IndexOf('<'); + if (index > 0) { xml = xml.Substring(index, xml.Length - index); } + if (!string.IsNullOrWhiteSpace(xml)) { - if (remoteContentResponse != null) { break; } - } - yield return new WaitForSeconds(0.1f); - } - lock (remoteContentLock) - { - if (remoteContentResponse.ResponseStatus != ResponseStatus.Completed || remoteContentResponse.StatusCode != HttpStatusCode.OK) - { - yield return CoroutineStatus.Success; - } - - try - { - string xml = remoteContentResponse.Content; - int index = xml.IndexOf('<'); - if (index > 0) { xml = xml.Substring(index, xml.Length - index); } - if (!string.IsNullOrWhiteSpace(xml)) + remoteContentDoc = XDocument.Parse(xml); + foreach (var subElement in remoteContentDoc?.Root.Elements()) { - remoteContentDoc = XDocument.Parse(xml); - foreach (var subElement in remoteContentDoc?.Root.Elements()) - { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); - } + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } - - catch (Exception e) - { -#if DEBUG - DebugConsole.ThrowError("Reading received remote main menu content failed.", e); -#endif - GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.WairForRemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, - "Reading received remote main menu content failed. " + e.Message); - } } - yield return CoroutineStatus.Success; - } - private readonly object remoteContentLock = new object(); - private IRestResponse remoteContentResponse; - - private void RemoteContentReceived(IRestResponse response, RestRequestAsyncHandle handle) - { - lock (remoteContentLock) + catch (Exception e) { - remoteContentResponse = response; +#if DEBUG + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received remote main menu content failed. " + e.Message); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 478571b01..681edc43a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -29,10 +30,19 @@ namespace Barotrauma currentDownload = null; confirmDownload = false; } + + private void DeletePrevDownloads() + { + if (Directory.Exists(ModReceiver.DownloadFolder)) + { + Directory.Delete(ModReceiver.DownloadFolder, recursive: true); + } + } public override void Select() { base.Select(); + DeletePrevDownloads(); Reset(); Frame.ClearChildren(); @@ -67,6 +77,18 @@ namespace Barotrauma .Where(sp => sp.ContentPackage is null).ToArray(); if (!missingPackages.Any()) { + if (!GameMain.Client.IsServerOwner) + { + ContentPackageManager.EnabledPackages.BackUp(); + ContentPackageManager.EnabledPackages.SetCore( + GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.CorePackage) + .First(p => p != null)); + ContentPackageManager.EnabledPackages.SetRegular( + GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.RegularPackage) + .Where(p => p != null).ToArray()); + } GameMain.NetLobbyScreen.Select(); return; } @@ -201,7 +223,7 @@ namespace Barotrauma if (!pendingDownloads.Contains(p)) { - downloadProgress.ClearChildren(); + downloadProgress.GetAllChildren().ToArray().ForEach(c => downloadProgress.RemoveChild(c)); return 1.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 8abc2da45..af54e3654 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -3786,7 +3786,7 @@ namespace Barotrauma RelativeSpacing = 0.02f }; - var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIDragIndicator") { CanBeFocused = false diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 7e5b5e6ae..aad8f3493 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -380,7 +380,7 @@ namespace Barotrauma string spriteFolder = ""; ContentPath texturePath = null; - if (element.Attribute("texture") != null) + if (element.GetAttribute("texture") != null) { texturePath = element.GetAttributeContentPath("texture"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 2e55fe64b..7e6abbd01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -9,11 +9,7 @@ using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; -#if DEBUG -using System.IO; -#else using Barotrauma.IO; -#endif namespace Barotrauma { @@ -1262,7 +1258,9 @@ namespace Barotrauma } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - if (ep.Category == MapEntityCategory.ItemAssembly) + if (ep.Category == MapEntityCategory.ItemAssembly + && ep.ContentPackage?.Files.Length == 1 + && ContentPackageManager.LocalPackages.Contains(ep.ContentPackage)) { var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, TextManager.Get("Delete"), style: "GUIButtonSmall") @@ -1978,6 +1976,7 @@ namespace Barotrauma if (newPackage is RegularPackage regular) { ContentPackageManager.EnabledPackages.EnableRegular(regular); + GameSettings.SaveCurrentConfig(); } } SubmarineInfo.RefreshSavedSub(savePath); @@ -2164,12 +2163,12 @@ namespace Barotrauma var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"Any").ToEnumerable()), selectMultiple: true); - allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); + allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier()); if (MainSub.Info.OutpostModuleInfo == null || !MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")) { - allowAttachDropDown.SelectItem("any"); + allowAttachDropDown.SelectItem("any".ToIdentifier()); } foreach (Identifier flag in availableFlags) { @@ -2211,7 +2210,7 @@ namespace Barotrauma locationTypeDropDown.SelectItem(locationType); } } - if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } + if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any".ToIdentifier()); } locationTypeDropDown.OnSelected += (_, __) => { @@ -2225,9 +2224,7 @@ namespace Barotrauma // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft); - var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); @@ -2238,11 +2235,11 @@ namespace Barotrauma { outpostModuleInfo.DetermineGapPositions(MainSub); } - foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) { - if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; } gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); - if (outpostModuleInfo.GapPositions.HasFlag((OutpostModuleInfo.GapPosition)gapPos)) + if (outpostModuleInfo.GapPositions.HasFlag(gapPos)) { gapPositionDropDown.SelectItem(gapPos); } @@ -2271,6 +2268,49 @@ namespace Barotrauma }; gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft) + { + ToolTip = TextManager.Get("canattachtoprevious.tooltip") + }; + var canAttachToPrevDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), + text: "", selectMultiple: true); + if (outpostModuleInfo != null) + { + foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + { + if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + canAttachToPrevDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); + if (outpostModuleInfo.CanAttachToPrevious.HasFlag(gapPos)) + { + canAttachToPrevDropDown.SelectItem(gapPos); + } + } + } + + canAttachToPrevDropDown.OnSelected += (_, __) => + { + if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None; + if (canAttachToPrevDropDown.SelectedDataMultiple.Any()) + { + List gapPosTexts = new List(); + foreach (OutpostModuleInfo.GapPosition gapPos in canAttachToPrevDropDown.SelectedDataMultiple) + { + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious |= gapPos; + gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()).Value); + } + canAttachToPrevDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + else + { + canAttachToPrevDropDown.Text = ToolBox.LimitString("None", canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + return true; + }; + canAttachToPrevGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + // ------------------- var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) @@ -2583,7 +2623,18 @@ namespace Barotrauma //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } - if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } + + if (!contentPacks.Contains(contentPack.Name)) + { + string altName = contentPack.AltNames.FirstOrDefault(n => contentPacks.Contains(n)); + if (!string.IsNullOrEmpty(altName)) + { + MainSub.Info.RequiredContentPackages.Remove(altName); + MainSub.Info.RequiredContentPackages.Add(contentPack.Name); + contentPacks.Remove(altName); + } + contentPacks.Add(contentPack.Name); + } } foreach (string contentPackageName in contentPacks) @@ -2749,11 +2800,7 @@ namespace Barotrauma } bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; -#if DEBUG - string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; -#else string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); -#endif string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { @@ -2782,26 +2829,27 @@ namespace Barotrauma void Save() { - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); -#if DEBUG - doc.Save(filePath); -#else - doc.SaveSafe(filePath); -#endif - ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.Regular.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); if (existingContentPackage == null) { //content package doesn't exist, create one ModProject modProject = new ModProject() { Name = nameBox.Text }; - var newFile = ModProject.File.FromPath(filePath); + var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); - ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); - } - else - { - EnqueueForReload(existingContentPackage); + string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); + existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); + doc.SaveSafe(filePath); + + var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; + if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + { + ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + GameSettings.SaveCurrentConfig(); + } + UpdateEntityList(); } @@ -2884,11 +2932,8 @@ namespace Barotrauma { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { -#if DEBUG - deleteBtn.Enabled = true; -#else - deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); -#endif + deleteBtn.Enabled = userData is SubmarineInfo subInfo + && (GetContentPackageIntrinsicallyTiedToSub(subInfo) != null || Path.GetDirectoryName(subInfo.FilePath) == SubmarineInfo.SavePath); } return true; } @@ -3141,18 +3186,10 @@ namespace Barotrauma ReconstructLayers(); } - private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) - { - foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) - { - if (regularPackage.Files.Length == 1 && regularPackage.Files[0].Path == sub.FilePath) - { - return regularPackage; - } - } - - return null; - } + private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + => ContentPackageManager.LocalPackages.Regular + .Where(p => p.Files.Length == 1) + .FirstOrDefault(regularPackage => regularPackage.Files[0].Path == sub.FilePath); private void TryDeleteSub(SubmarineInfo sub) { @@ -3160,8 +3197,10 @@ namespace Barotrauma //If the sub is included in a content package that only defines that one sub, //check that it's a local content package and only allow deletion if it is. + //(deleting from the Submarines folder is also currently allowed, but this is temporary) var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); - if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } + bool isInOldSavePath = Path.GetDirectoryName(sub.FilePath) == SubmarineInfo.SavePath; + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage) && !isInOldSavePath) { return; } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), @@ -3171,10 +3210,18 @@ namespace Barotrauma { try { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); + if (subPackage != null) + { + Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } + else if (isInOldSavePath && File.Exists(sub.FilePath)) + { + File.Delete(sub.FilePath); + } sub.Dispose(); - File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index f8186ff57..af4eba036 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,13 +390,13 @@ namespace Barotrauma } GUIComponent propertyField = null; - if (value is bool) + if (value is bool boolVal) { - propertyField = CreateBoolField(entity, property, (bool)value, displayName, toolTip); + propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); } - else if (value is string) + else if (value is string stringVal) { - propertyField = CreateStringField(entity, property, (string)value, displayName, toolTip); + propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); } else if (value.GetType().IsEnum) { @@ -1277,7 +1277,7 @@ namespace Barotrauma public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1307,6 +1307,18 @@ namespace Barotrauma UserData = tagTextPair.Key.ToString() }; } + + if (entity is IHasExtraTextPickerEntries hasExtraTextPickerEntries) + { + foreach (string extraEntry in hasExtraTextPickerEntries.GetExtraTextPickerEntries()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, + ToolBox.LimitString(extraEntry, GUIStyle.Font, textList.Content.Rect.Width), GUIStyle.Green) + { + UserData = extraEntry + }; + } + } } private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) @@ -1445,4 +1457,12 @@ namespace Barotrauma } } } + + /// + /// Implement this interface to insert extra entires to the text pickers created for the SerializableEntityEditors of the entity + /// + interface IHasExtraTextPickerEntries + { + public IEnumerable GetExtraTextPickerEntries(); + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index ca6ea6874..cd908d386 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -40,7 +40,7 @@ namespace Barotrauma if (sound?.Sound != null) { loopSound = subElement.GetAttributeBool("loop", false); - if (subElement.Attribute("selectionmode") != null) + if (subElement.GetAttribute("selectionmode") != null) { if (Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs index c94e33092..5ab0a9d8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -533,6 +533,9 @@ namespace Barotrauma.Steam private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) { taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + + var contentPackage + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); @@ -567,34 +570,74 @@ namespace Barotrauma.Steam RectTransform rightSideButtonRectT() => new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight); + + bool reinstallAction(GUIButton button, object o) + { + TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.Reinstall(workshopItem), t => + { + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + }); + return false; + } + + var (updateButton, updateSprite) = CreatePaddedButton( + rightSideButtonRectT(), + "GUIUpdateButton", + spriteScale: 0.8f); + updateButton.ToolTip = TextManager.Get("WorkshopItemUpdate"); + updateButton.Visible = false; + updateButton.OnClicked = reinstallAction; + + if (contentPackage != null) + { + TaskPool.Add( + $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", + contentPackage.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + updateButton.Visible = !isUpToDate; + }); + } var (reinstallButton, reinstallSprite) = CreatePaddedButton( rightSideButtonRectT(), "GUIReloadButton", spriteScale: 0.8f); reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall"); - reinstallButton.OnClicked += (button, o) => - { - SteamManager.Workshop.Uninstall(workshopItem); - TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), t => { }); - return false; - }; + reinstallButton.OnClicked = reinstallAction; var reinstallButtonUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, reinstallButton.RectTransform), onUpdate: (f, component) => { - reinstallButton.Visible = workshopItem.IsSubscribed; + reinstallButton.Visible = workshopItem.IsSubscribed || workshopItem.Owner.Id == SteamManager.GetSteamID(); reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending && !SteamManager.Workshop.IsInstalling(workshopItem); + reinstallSprite.Color = reinstallButton.Enabled ? reinstallSprite.Style.Color : Color.DimGray; + updateButton.Enabled = reinstallButton.Enabled && contentPackage != null && ContentPackageManager.WorkshopPackages.Contains(contentPackage); + updateSprite.Color = reinstallSprite.Color; + + if (contentPackage != null + && !ContentPackageManager.WorkshopPackages.Contains(contentPackage) + && ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + { + updateButton.Visible = false; + } }); CreateSubscribeButton(workshopItem, rightSideButtonRectT(), spriteScale: 0.8f); + + var padding = new GUIFrame( + new RectTransform((0.15f, 1.0f), headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); - var padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); + padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform), isHorizontal: true) @@ -638,7 +681,7 @@ namespace Barotrauma.Steam isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var starColor = Color.Lerp( - Color.Lerp(Color.Red, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), + Color.Lerp(Color.White, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f)); for (int i = 0; i < 5; i++) { @@ -652,12 +695,23 @@ namespace Barotrauma.Steam star.SelectedColor = starColor; } } - var scoreVoteCountPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + var scoreTextPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null); + + var scoreTextContainer = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + style: null); + var scoreVoteCount = new GUITextBlock( - new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center), TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]", - (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.CenterLeft) + (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.BottomLeft) + { + Padding = Vector4.Zero + }; + var subscriptionCount = new GUITextBlock( + new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center), + TextManager.GetWithVariable("WorkshopItemSubscriptions", "[SubscriptionCount]", + workshopItem.NumUniqueSubscriptions.ToString()), textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index 45fa95f50..720121b09 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -89,23 +89,22 @@ namespace Barotrauma.Steam return (fileCount, byteCount); } + + private void DeselectPublishedItem() + { + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.WorkshopPackages.Refresh(); - var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); - Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } - ? action - : null; - - void deselectItem() - { - deselectAction?.Invoke(); - SelectTab(Tab.Publish); - } - parentFrame.ClearChildren(); GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), childAnchor: Anchor.TopCenter); @@ -146,7 +145,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - deselectItem(); + DeselectPublishedItem(); return false; } }; @@ -285,7 +284,7 @@ namespace Barotrauma.Steam RectTransform newButtonRectT() => new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform); - var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemPublish")) + var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get(workshopItem.Id != 0 ? "WorkshopItemUpdate" : "WorkshopItemPublish")) { OnClicked = (button, o) => { @@ -333,8 +332,9 @@ namespace Barotrauma.Steam TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), t => { + SteamManager.Workshop.Uninstall(workshopItem); confirmDeletion.Close(); - deselectItem(); + DeselectPublishedItem(); }); return false; }; @@ -364,24 +364,10 @@ namespace Barotrauma.Steam return false; }; - var coroutineEval = subcoroutine(messageBox.Text, messageBox); + var coroutineEval = subcoroutine(messageBox.Text, messageBox).GetEnumerator(); while (true) { - bool moveNext = true; - try - { - moveNext = coroutineEval.GetEnumerator().MoveNext(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); - messageBox.Close(); - } - if (!moveNext) - { - messageBox.Close(); - } - var status = coroutineEval.GetEnumerator().Current; + var status = coroutineEval.Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -397,6 +383,20 @@ namespace Barotrauma.Steam { yield return status; } + bool moveNext = true; + try + { + moveNext = coroutineEval.MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } } } @@ -408,26 +408,9 @@ namespace Barotrauma.Steam { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) { - //Must download! - while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) - { - bool shouldForceInstall = workshopItem.IsInstalled - && Directory.Exists(workshopItem.Directory) - && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); - shouldForceInstall |= workshopItem is - { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; - if (shouldForceInstall) - { - SteamManager.Workshop.ForceRedownload(workshopItem); - } - currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); - yield return new WaitForSeconds(0.5f); - } - } - else - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); + SteamManager.Workshop.NukeDownload(workshopItem); } + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); TaskPool.Add($"Install {workshopItem.Title}", SteamManager.Workshop.WaitForInstall(workshopItem), (t) => @@ -436,7 +419,9 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = TextManager.Get("PublishPopupInstall"); + currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) + ? TextManager.Get("PublishPopupInstall") + : TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } @@ -457,7 +442,6 @@ namespace Barotrauma.Steam currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } - PopulatePublishTab(workshopItem, parentFrame); yield return CoroutineStatus.Success; @@ -500,7 +484,10 @@ namespace Barotrauma.Steam editor.SubmitAsync(), t => { - t.TryGetResult(out result); + if (t.TryGetResult(out Steamworks.Ugc.PublishResult publishResult)) + { + result = publishResult; + } resultException = t.Exception?.GetInnermost(); }); currentStepText.Text = TextManager.Get("PublishPopupSubmit"); @@ -523,6 +510,14 @@ namespace Barotrauma.Steam $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); } + ContentPackage? pkgToNuke + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); + if (pkgToNuke != null) + { + Directory.Delete(pkgToNuke.Dir, recursive: true); + ContentPackageManager.WorkshopPackages.Refresh(); + } + bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -537,13 +532,17 @@ namespace Barotrauma.Steam yield return new WaitForSeconds(0.5f); } + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + var localModProject = new ModProject(localPackage) { SteamWorkshopId = resultId }; + localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); ContentPackageManager.ReloadContentPackage(localPackage); - ContentPackageManager.WorkshopPackages.Refresh(); + DeselectPublishedItem(); if (result.Value.NeedsWorkshopAgreement) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index fe499a4d9..3a90d6ea1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -8,6 +8,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Barotrauma.Extensions; namespace Barotrauma.Steam { @@ -117,7 +118,7 @@ namespace Barotrauma.Steam if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } var client = new RestClient(thumbnailUrl); var request = new RestRequest(".", Method.GET); - IRestResponse response = await client.ExecuteTaskAsync(request, cancellationToken); + IRestResponse response = await client.ExecuteAsync(request, cancellationToken); if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) { using var dataStream = new System.IO.MemoryStream(); @@ -207,6 +208,11 @@ namespace Barotrauma.Steam } string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; + if (File.Exists(newPath) || Directory.Exists(newPath)) + { + newPath += $"_{contentPackage.SteamWorkshopId}"; + } + if (File.Exists(newPath) || Directory.Exists(newPath)) { throw new Exception($"{newPath} already exists"); @@ -256,6 +262,18 @@ namespace Barotrauma.Steam } } + public static async Task Reinstall(Steamworks.Ugc.Item workshopItem) + { + NukeDownload(workshopItem); + var toUninstall + = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + .ToHashSet(); + toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); + CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); + DownloadModThenEnqueueInstall(workshopItem); + await WaitForInstall(workshopItem); + } + public static async Task WaitForInstall(Steamworks.Ugc.Item item) => await WaitForInstall(item.Id); @@ -263,6 +281,7 @@ namespace Barotrauma.Steam { var installWaiter = new InstallWaiter(item); while (installWaiter.Waiting) { await Task.Delay(500); } + await Task.Delay(500); } public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) @@ -276,7 +295,8 @@ namespace Barotrauma.Steam return; } else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id)) + && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id) + && !InstallTaskCounter.IsInstalling(id)) { TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs index feab2338c..59b42ef78 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -31,6 +31,7 @@ namespace Barotrauma.Steam private readonly GUIListBox enabledRegularModsList; private readonly GUIListBox disabledRegularModsList; private readonly Action onInstalledInfoButtonHit; + private readonly GUITextBox modsListFilter; private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); @@ -47,7 +48,12 @@ namespace Barotrauma.Steam contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); - CreateInstalledModsTab(out enabledCoreDropdown, out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit); + CreateInstalledModsTab( + out enabledCoreDropdown, + out enabledRegularModsList, + out disabledRegularModsList, + out onInstalledInfoButtonHit, + out modsListFilter); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -176,7 +182,8 @@ namespace Barotrauma.Steam out GUIDropDown enabledCoreDropdown, out GUIListBox enabledRegularModsList, out GUIListBox disabledRegularModsList, - out Action onInstalledInfoButtonHit) + out Action onInstalledInfoButtonHit, + out GUITextBox modsListFilter) { GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); @@ -287,7 +294,7 @@ namespace Barotrauma.Steam var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); searchRectT.RelativeSize = (0.5f, searchRectT.RelativeSize.Y); var searchHolder = new GUIFrame(searchRectT, style: null); - var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), ""); + var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), "", createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, textColor: Color.DarkGray * 0.6f, text: TextManager.Get("Search") + "...", @@ -300,12 +307,10 @@ namespace Barotrauma.Steam searchBox.OnTextChanged += (sender, str) => { - enabledModsList.Content.Children.Concat(disabledModsList.Content.Children) - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + UpdateModListItemVisibility(); return true; }; + modsListFilter = searchBox; new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), onUpdate: (f, component) => @@ -320,6 +325,15 @@ namespace Barotrauma.Steam }); } + private void UpdateModListItemVisibility() + { + string str = modsListFilter.Text; + enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) + .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() + || (c.UserData is ContentPackage p + && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + } + private void PopulateInstalledModLists() { ContentPackageManager.UpdateContentPackageList(); @@ -344,15 +358,21 @@ namespace Barotrauma.Steam RelativeSpacing = 0.02f }; - var dragIndicator = new GUIButton(new RectTransform((0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + var dragIndicator = new GUIButton(new RectTransform((0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIDragIndicator") { CanBeFocused = false }; - var modNameScissor - = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)); - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), text: mod.Name); + var modNameScissor = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)) + { + CanBeFocused = false + }; + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), + text: mod.Name) + { + CanBeFocused = false + }; if (ContentPackageManager.LocalPackages.Contains(mod)) { var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", @@ -418,6 +438,8 @@ namespace Barotrauma.Steam if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; } addRegularModToList(mod, disabledRegularModsList); } + + UpdateModListItemVisibility(); } private void CreatePopularModsTab(out GUIListBox popularModsList) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 80a620bcc..65d8b3408 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -133,7 +133,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 20a82c153..d6beedd53 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -128,7 +128,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 35a96ffc0..539cc7653 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -135,7 +135,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c599af014..dbda5fa53 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 036a8d02c..883d5184f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 49405004e..2e5acdab7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class Character { - public static Character Controlled = null; + public static Character Controlled => null; partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7d8e7791e..26a1fa3a3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -424,7 +424,7 @@ namespace Barotrauma msg.Write(owner == c && owner.Character == this); msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; - case StatusEventData _: + case CharacterStatusEventData _: WriteStatus(msg); break; case UpdateSkillsEventData _: @@ -657,6 +657,10 @@ namespace Barotrauma int infoLength = msg.LengthBytes - msgLengthBeforeInfo; msg.Write((byte)CampaignInteractionType); + if (CampaignInteractionType == CampaignMode.InteractionType.Store) + { + msg.Write(MerchantIdentifier); + } int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 00190b2fd..484ebf03d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -7,51 +7,57 @@ namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(List itemsToSell, Client client = null) + public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client = null) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in itemsToSell) + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var storeSpecificItems = GetPurchasedItems(storeIdentifier); + foreach (var item in itemsToSell) { var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - Location.StoreCurrentBalance -= itemValue; + store.Balance -= itemValue; campaign.GetWallet(client).Give(itemValue); - PurchasedItems.Remove(item); + storeSpecificItems?.Remove(item); } } - public void BuyBackSoldItems(List itemsToBuy, Client client) + public void BuyBackSoldItems(Identifier storeIdentifier, List itemsToBuy) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - var sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var storeSpecificItems = SoldItems.GetValueOrDefault(storeIdentifier); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToBuy.Select(i => i.ItemPrefab)); foreach (var item in itemsToBuy) { int itemValue = sellValues[item.ItemPrefab]; - if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } - Location.StoreCurrentBalance += itemValue; + if (store.Balance < itemValue || item.Removed) { continue; } + store.Balance += itemValue; campaign.Bank.TryDeduct(itemValue); - SoldItems.Remove(item); + storeSpecificItems.Remove(item); } } - public void SellItems(List itemsToSell, Client client) + public void SellItems(Identifier storeIdentifier, List itemsToSell, Client client) { + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } 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 - var sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); + var itemsSoldAtStore = SoldItems.GetValueOrDefault(storeIdentifier); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); foreach (var item in itemsToSell) { 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; } + if (store.Balance < 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) { @@ -66,8 +72,8 @@ namespace Barotrauma item.Removed = true; Entity.Spawner.AddItemToRemoveQueue(entity); } - SoldItems.Add(item); - Location.StoreCurrentBalance -= itemValue; + itemsSoldAtStore?.Add(item); + store.Balance -= itemValue; campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fbd7d030e..d8cc3b09b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -616,8 +616,17 @@ namespace Barotrauma } // Store balance - msg.Write(true); - msg.Write((UInt16)map.CurrentLocation.StoreCurrentBalance); + bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any(); + msg.Write(hasStores); + if (hasStores) + { + msg.Write((byte)map.CurrentLocation.Stores.Count); + foreach (var store in map.CurrentLocation.Stores.Values) + { + msg.Write(store.Identifier); + msg.Write((UInt16)store.Balance); + } + } } else { @@ -626,36 +635,10 @@ namespace Barotrauma msg.Write(false); } - msg.Write((UInt16)CargoManager.ItemsInBuyCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - 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, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.SoldItems.Count); - foreach (SoldItem si in CargoManager.SoldItems) - { - msg.Write(si.ItemPrefab.Identifier); - msg.Write((UInt16)si.ID); - msg.Write(si.Removed); - msg.Write(si.SellerID); - msg.Write((byte)si.Origin); - } + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteItems(msg, CargoManager.PurchasedItems); + WriteItems(msg, CargoManager.SoldItems); msg.Write((ushort)UpgradeManager.PendingUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades) @@ -700,44 +683,10 @@ namespace Barotrauma bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); - UInt16 buyCrateItemCount = msg.ReadUInt16(); - List buyCrateItems = new List(); - for (int i = 0; i < buyCrateItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); - } - - 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, sender)); - } - - UInt16 purchasedItemCount = msg.ReadUInt16(); - List purchasedItems = new List(); - for (int i = 0; i < purchasedItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); - } - - UInt16 soldItemCount = msg.ReadUInt16(); - List soldItems = new List(); - for (int i = 0; i < soldItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - UInt16 id = msg.ReadUInt16(); - bool removed = msg.ReadBoolean(); - byte sellerId = msg.ReadByte(); - byte origin = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); - } + var buyCrateItems = ReadPurchasedItems(msg, sender); + var subSellCrateItems = ReadPurchasedItems(msg, sender); + var purchasedItems = ReadPurchasedItems(msg, sender); + var soldItems = ReadSoldItems(msg); ushort purchasedUpgradeCount = msg.ReadUInt16(); List purchasedUpgrades = new List(); @@ -839,42 +788,83 @@ namespace Barotrauma bool allowedToUseStore = AllowedToManageCampaign(sender, ClientPermissions.CampaignStore); if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { - var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity, sender)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity, sender)); - CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false, sender); + var prevBuyCrateItems = new Dictionary>(CargoManager.ItemsInBuyCrate); + foreach (var store in prevBuyCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); + } + } + foreach (var store in buyCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); + } + } + var prevPurchasedItems = new Dictionary>(CargoManager.PurchasedItems); + foreach (var store in prevPurchasedItems) + { + CargoManager.SellBackPurchasedItems(store.Key, store.Value); + } + foreach (var store in purchasedItems) + { + CargoManager.PurchaseItems(store.Key, store.Value, false, sender); + } } bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { - var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); - currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity, sender)); - subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity, sender)); + var prevSubSellCrateItems = new Dictionary>(CargoManager.ItemsInSellFromSubCrate); + foreach (var store in prevSubSellCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); + } + } + foreach (var store in subSellCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, item.Quantity, sender); + } + } } - bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); if (allowedToManageCampaign || allowedToUseStore || (allowedToSellInventoryItems && allowedToSellSubItems)) { // 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), sender); - CargoManager.SellItems(soldItems, sender); + var prevSoldItems = new Dictionary>(CargoManager.SoldItems); + foreach (var store in prevSoldItems) + { + CargoManager.BuyBackSoldItems(store.Key, store.Value); + } + foreach (var store in soldItems) + { + CargoManager.SellItems(store.Key, store.Value, sender); + } } else if (allowedToSellInventoryItems || allowedToSellSubItems) { - if (allowedToSellInventoryItems) + var prevSoldItems = new Dictionary>(CargoManager.SoldItems); + foreach (var store in prevSoldItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); - soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); + store.Value.RemoveAll(predicate); + CargoManager.BuyBackSoldItems(store.Key, store.Value); } - else + foreach (var store in soldItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); - soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); + store.Value.RemoveAll(predicate); } - CargoManager.SellItems(soldItems, sender); + foreach (var store in soldItems) + { + CargoManager.SellItems(store.Key, store.Value, sender); + } + bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } if (allowedToManageCampaign) @@ -960,7 +950,7 @@ namespace Barotrauma public void ServerReadRewardDistribution(IReadMessage msg, Client sender) { - NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); + NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); if (!AllowedToManageCampaign(sender)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 2310120a0..7bf233c95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -42,8 +42,8 @@ namespace Barotrauma.Items.Components msg.Write(item.CurrentHull?.ID ?? Entity.NullEntityID); msg.Write(item.SimPosition.X); msg.Write(item.SimPosition.Y); - msg.Write(stickJoint.Axis.X); - msg.Write(stickJoint.Axis.Y); + msg.Write(jointAxis.X); + msg.Write(jointAxis.Y); if (StickTarget.UserData is Structure structure) { msg.Write(structure.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 8f1783707..f7db7648e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -56,7 +56,6 @@ namespace Barotrauma if (containerIndex < 0) { throw error($"container index out of range ({containerIndex})"); - break; } if (!(components[containerIndex] is ItemContainer itemContainer)) { @@ -66,7 +65,7 @@ namespace Barotrauma msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case StatusEventData _: + case ItemStatusEventData _: msg.Write(condition); break; case AssignCampaignInteractionEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 0707bd526..9645311ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -1,12 +1,13 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using System; -using System.Xml.Linq; namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { + const float DamageUpdateInterval = 1.0f; + private float damageUpdateTimer; partial void LoadPrefab(ContentXElement element) @@ -31,16 +32,38 @@ namespace Barotrauma.MapCreatures.Behavior } } + partial void UpdateDamage(float deltaTime) + { + damageUpdateTimer -= deltaTime; + if (damageUpdateTimer > 0.0f) { return; } + + const int maxMessagesPerSecond = 10; + int messages = 0; + foreach (BallastFloraBranch branch in Branches) + { + //don't notify about minuscule amounts of damage (<= 1.0f) + if (branch.AccumulatedDamage > 1.0f) + { + CreateNetworkMessage(new BranchDamageEventData(branch)); + branch.AccumulatedDamage = 0.0f; + messages++; + //throttle a bit: if a large ballast flora is withering, it can lead to a very large number of events otherwise + if (messages > maxMessagesPerSecond) { break; } + } + } + damageUpdateTimer = DamageUpdateInterval; + } + public void ServerWrite(IWriteMessage msg, IEventData eventData) { msg.Write((byte)eventData.NetworkHeader); switch (eventData) { - case SpawnEventData spawnEventData: + case SpawnEventData _: ServerWriteSpawn(msg); break; - case KillEventData killEventData: + case KillEventData _: //do nothing break; case BranchCreateEventData branchCreateEventData: @@ -72,6 +95,7 @@ namespace Barotrauma.MapCreatures.Behavior var (x, y) = branch.Position; msg.Write(parentId); msg.Write((int)branch.ID); + msg.Write(branch.IsRootGrowth); msg.WriteRangedInteger((byte)branch.Type, 0b0000, 0b1111); msg.WriteRangedInteger((byte)branch.Sides, 0b0000, 0b1111); msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); @@ -103,7 +127,7 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ID); } - public void SendNetworkMessage(IEventData extraData) + public void CreateNetworkMessage(IEventData extraData) { GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 7a8d742eb..93a36099e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -28,7 +28,11 @@ namespace Barotrauma.Networking public static string GetCompressedModPath(ContentPackage mod) { string dir = mod.Dir; - string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); + string resultFileName + = dir.StartsWith(ContentPackage.LocalModsDir) + ? $"Local_{mod.Name}" + : $"Workshop_{mod.Name}"; + resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 804c11d40..4373de52d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,7 +814,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; - case ClientPacketHeader.MONEY: + case ClientPacketHeader.TRANSFER_MONEY: ReadMoneyMessage(inc, connectedClient); break; case ClientPacketHeader.REWARD_DISTRIBUTION: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 334e62414..5580a12fe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -321,11 +321,16 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); + if (HiddenSubs.Any()) + { + UpdateFlag(NetFlags.HiddenSubs); + } SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); string[] defaultAllowedClientNameChars = - new string[] { + new string[] + { "32-33", "38-46", "48-57", diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index 715ef5a3a..ba77838e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 989383c24..96429c964 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub deleted file mode 100644 index cdd96a32b..000000000 Binary files a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml deleted file mode 100644 index a6d330d8d..000000000 --- a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 48ef25dce..e01c32de2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -56,7 +56,7 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; - private readonly float attackLimbResetInterval = 2; + private readonly float attackLimbSelectionInterval = 3; // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. private const float minPriority = 10; @@ -65,10 +65,10 @@ namespace Barotrauma private float updateTargetsTimer; private float updateMemoriesTimer; - private float attackLimbResetTimer; + private float attackLimbSelectionTimer; - private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; + private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning; + private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; @@ -77,25 +77,25 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - private Limb _attackingLimb; - private Limb _previousAttackingLimb; - public Limb AttackingLimb + private Limb _attackLimb; + private Limb _previousAttackLimb; + public Limb AttackLimb { - get { return _attackingLimb; } + get { return _attackLimb; } private set { - attackLimbResetTimer = 0; - if (_attackingLimb != value) + if (_attackLimb != value) { - _previousAttackingLimb = _attackingLimb; + _previousAttackLimb = _attackLimb; + _previousAttackLimb?.AttachedRope?.Snap(); } - if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) + else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) { - SetAimTimer(); + _attackLimb.AttachedRope?.Snap(); } - _attackingLimb = value; + _attackLimb = value; attackVector = null; - Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; + Reverse = _attackLimb != null && _attackLimb.attack.Reverse; } } @@ -425,7 +425,8 @@ namespace Barotrauma private void ReleaseDragTargets() { - if (Character.Inventory != null) + AttackLimb?.AttachedRope?.Snap(); + if (Character.Params.CanInteract && Character.Inventory != null) { Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); } @@ -600,7 +601,7 @@ namespace Barotrauma UpdatePatrol(deltaTime); break; case AIState.Attack: - run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; + run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); break; case AIState.Eat: @@ -620,7 +621,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -875,7 +876,10 @@ namespace Barotrauma if (followLastTarget) { var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) + if (target?.Entity != null && !target.Entity.Removed && + PreviousState == AIState.Attack && Character.CurrentHull == null && + (_previousAttackLimb?.attack == null || + _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) { // Keep heading to the last known position of the target var memory = GetTargetMemory(target, false); @@ -1126,31 +1130,42 @@ namespace Barotrauma return; } } + + attackLimbSelectionTimer -= deltaTime; + if (AttackLimb == null || attackLimbSelectionTimer <= 0) + { + attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f); + if (!IsAttackRunning && !IsCoolDownRunning) + { + AttackLimb = GetAttackLimb(attackWorldPos); + } + } bool canAttack = true; bool pursue = false; - if (IsCoolDownRunning) + if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { - var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; + var currentAttackLimb = AttackLimb ?? _previousAttackLimb; if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - switch (currentAttackLimb.attack.AfterAttack) + AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; + switch (activeBehavior) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1162,13 +1177,13 @@ namespace Barotrauma if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) + if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } - AttackingLimb = null; + AttackLimb = null; } else { @@ -1177,19 +1192,19 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1204,10 +1219,15 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: + case AIBehaviorAfterAttack.ReverseUntilCanAttack: + if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack) + { + Reverse = true; + } if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1217,7 +1237,7 @@ namespace Barotrauma // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1227,12 +1247,12 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1240,7 +1260,7 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1269,7 +1289,7 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { @@ -1291,7 +1311,12 @@ namespace Barotrauma UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; case AIBehaviorAfterAttack.FallBack: + case AIBehaviorAfterAttack.Reverse: default: + if (activeBehavior == AIBehaviorAfterAttack.Reverse) + { + Reverse = true; + } UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); return; } @@ -1303,12 +1328,13 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) + if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity)) { - AttackingLimb = GetAttackLimb(attackWorldPos); + AttackLimb = GetAttackLimb(attackWorldPos); } - canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; + canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0; } + if (!AIParams.CanOpenDoors) { if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) @@ -1347,8 +1373,8 @@ namespace Barotrauma // Target a specific limb instead of the target center position if (wallTarget == null && targetCharacter != null) { - var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; - attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); + var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType; + attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType); if (attackTargetLimb == null) { State = AIState.Idle; @@ -1361,7 +1387,7 @@ namespace Barotrauma } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) @@ -1389,23 +1415,23 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } - float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } + float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } - float distanceOffset = diff * AttackingLimb.attack.Duration; + float distanceOffset = diff * AttackLimb.attack.Duration; // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. return targetVelocity * distanceOffset * dot; } // Check that we can reach the target distance = toTarget.Length(); - canAttack = distance < AttackingLimb.attack.Range; + canAttack = distance < AttackLimb.attack.Range; // Crouch if the target is down (only humanoids), so that we can reach it. - if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2) + if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) { - if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range) + if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) { humanoidAnimController.Crouching = true; } @@ -1413,14 +1439,14 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { // Check that is facing the target - float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir); float angle = VectorExtensions.Angle(forward, toTarget); - canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle); - if (canAttack && AttackingLimb.attack.AvoidFriendlyFire) + canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); + if (canAttack && AttackLimb.attack.AvoidFriendlyFire) { float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; @@ -1434,11 +1460,11 @@ namespace Barotrauma } if (canAttack) { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range)); + canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); bool IsBlocked(Vector2 targetPosition) { - foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { Character hitTarget = null; if (body.UserData is Character c) @@ -1460,22 +1486,8 @@ namespace Barotrauma } } } - else if (!IsAttackRunning && !IsCoolDownRunning) - { - // If not, reset the attacking limb, if the cooldown is not running - // Don't use the property, because we don't want cancel reversing, if we are reversing. - if (attackLimbResetTimer > attackLimbResetInterval) - { - _attackingLimb = null; - attackLimbResetTimer = 0; - } - else - { - attackLimbResetTimer += deltaTime; - } - } } - Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null; + Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. @@ -1490,9 +1502,9 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackingLimb != null && AttackingLimb.attack.Retreat) + if (AttackLimb != null && AttackLimb.attack.Retreat) { - UpdateFallBack(attackWorldPos, deltaTime, false); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } else { @@ -1527,7 +1539,7 @@ namespace Barotrauma } // When pursuing, we don't want to pursue too close float max = 300; - float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; + float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; if (!canAttack || distance > margin) { // Steer towards the target if in the same room and swimming @@ -1558,10 +1570,10 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { float dir = Character.AnimController.Dir; - if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) + if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) { SteeringManager.Reset(); } @@ -1658,9 +1670,9 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) @@ -1703,10 +1715,10 @@ namespace Barotrauma // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; } else { @@ -1741,9 +1753,9 @@ namespace Barotrauma } } } - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } canAttack = false; @@ -1800,7 +1812,7 @@ namespace Barotrauma } } - if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) + if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1811,7 +1823,7 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (AttackingLimb.attack.Ranged) + else if (AttackLimb.attack.Ranged) { // Too close UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1824,18 +1836,18 @@ namespace Barotrauma } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } } else if (IsAttackRunning) { - AttackingLimb.attack.ResetAttackTimer(); + AttackLimb.attack.ResetAttackTimer(); } } - private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) { if (attackingLimb == null) { return false; } if (target == null) { return false; } @@ -1854,10 +1866,11 @@ namespace Barotrauma // Check that is approximately facing the target Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; } float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - float angle = VectorExtensions.Angle(forward, toTarget); - if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); + if (angle > attack.RequiredAngle) { return false; } } return true; } @@ -1867,7 +1880,7 @@ namespace Barotrauma private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; + Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1901,12 +1914,13 @@ namespace Barotrauma float CalculatePriority(Limb limb, Vector2 attackPos) { - if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } + float prio = 1 + limb.attack.Priority; + if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); - return (1 + limb.attack.Priority) * distanceFactor; + return prio * distanceFactor; } } @@ -1919,7 +1933,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - if (Character.Params.CanInteract && attackResult.Damage > 10) + if (attackResult.Damage >= AIParams.DamageThreshold) { ReleaseDragTargets(); } @@ -2007,9 +2021,11 @@ namespace Barotrauma if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { - // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; - avoidGunFire = false; + if (IsAttackRunning) + { + avoidGunFire = false; + } } if (retaliate) { @@ -2022,7 +2038,7 @@ namespace Barotrauma } } } - else if (avoidGunFire) + else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold) { State = AIState.Escape; avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); @@ -2114,7 +2130,7 @@ namespace Barotrauma { if (attackingLimb.attack.CoolDownTimer > 0) { - SetAimTimer(); + SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; if (!(damageTarget is Character)) @@ -2245,19 +2261,19 @@ namespace Barotrauma // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } - Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); - if (!MathUtils.IsValid(attackDir)) + Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); + if (!MathUtils.IsValid(dir)) { - attackDir = Vector2.UnitY; + dir = Vector2.UnitY; } - steeringManager.SteeringManual(deltaTime, attackDir); - if (Character.AnimController.InWater) + steeringManager.SteeringManual(deltaTime, dir); + if (Character.AnimController.InWater && !Reverse) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } if (checkBlocking) { - return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); + return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2)); } return true; } @@ -2810,7 +2826,7 @@ namespace Barotrauma if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null) { - if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) + if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) { if (!isAnyTargetClose) { @@ -2990,7 +3006,7 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition; + Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { Vector2 rayEnd = SelectedAiTarget.SimPosition; @@ -3303,6 +3319,7 @@ namespace Barotrauma foreach (var triggerObject in activeTriggers) { AITrigger trigger = triggerObject.Key; + if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) { @@ -3471,7 +3488,7 @@ namespace Barotrauma disableTailCoroutine = null; } Character.AnimController.ReleaseStuckLimbs(); - AttackingLimb = null; + AttackLimb = null; movementMargin = 0; ResetEscape(); if (isStateChanged && to == AIState.Idle && from != to) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index a98cef44d..5dc813a8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -148,14 +148,14 @@ namespace Barotrauma } if (TargetCharacter != null) { - if (enemyAI.AttackingLimb?.attack == null) + if (enemyAI.AttackLimb?.attack == null) { DeattachFromBody(reset: true, cooldown: 1); } else { - float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; - if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + float range = enemyAI.AttackLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range) { DeattachFromBody(reset: true, cooldown: 1); } @@ -265,11 +265,11 @@ namespace Barotrauma if (enemyAI.IsSteeringThroughGap) { break; } if (_attachPos == Vector2.Zero) { break; } if (!AttachToSub && !AttachToCharacters) { break; } - if (enemyAI.AttackingLimb == null) { break; } + if (enemyAI.AttackLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); - if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 4f818f175..5dcbdb17e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -112,6 +112,13 @@ namespace Barotrauma } foreach (FireSource fs in targetHull.FireSources) { + if (fs == null) { continue; } + if (fs.Removed) { continue; } + if (character.CurrentHull == null) + { + Abandon = true; + break; + } float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); bool inRange = xDist + yDist < extinguisher.Range; @@ -153,7 +160,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref gotoObjective))) { - gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull); + gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8a2f1ef0e..99adb5e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -164,7 +164,7 @@ namespace Barotrauma TargetName = Leak.FlowTargetHull?.DisplayName, requiredCondition = () => Leak.Submarine == character.Submarine && - (Leak.FlowTargetHull != null && character.CurrentHull == Leak.FlowTargetHull || character.CanSeeTarget(Leak)), + Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h), // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) SpeakCannotReachCondition = () => !CheckObjectiveSpecific() }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 65597452e..e282061e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -99,8 +99,7 @@ namespace Barotrauma { if (InWater || !CanWalk) { - float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; - return TargetMovement.LengthSquared() > avg * avg; + return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 699f94e37..f898ac8d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -448,21 +448,18 @@ namespace Barotrauma movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; var mainLimb = MainLimb; - if (isMoving) + float t = 0.5f; + if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - float t = 0.5f; - if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) { - Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); - float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); - if (dot < 0) - { - // Reduce the linear movement speed when not facing the movement direction - t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); - } + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b386fdd83..30fc791e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -443,8 +443,6 @@ namespace Barotrauma if (CurrentGroundedParams == null) { return; } Vector2 handPos; - //if you're allergic to magic numbers, stop reading now - Limb leftFoot = GetLimb(LimbType.LeftFoot); Limb rightFoot = GetLimb(LimbType.RightFoot); Limb head = GetLimb(LimbType.Head); @@ -599,16 +597,20 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } + if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (HeadAngle.HasValue) + if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } + else + { + RotateHead(head); + } if (!onGround) { @@ -883,23 +885,17 @@ namespace Barotrauma } } float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f) + if (aiming) { - if (!aiming) - { - float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * Dir; + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } - else + else if (targetSpeed > 0.1f) { - if (aiming) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - torso.SimPosition) * Dir; - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); @@ -914,13 +910,14 @@ namespace Barotrauma { torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } - if (HeadAngle.HasValue) + + if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); + RotateHead(head); } //dont try to move upwards if head is already out of water @@ -951,7 +948,18 @@ namespace Barotrauma if (isNotRemote) { - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp); + float t = movementLerp; + if (targetSpeed > 0.00001f && !SimplePhysicsEnabled) + { + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } + } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } WalkPos += movement.Length(); @@ -1227,7 +1235,11 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (!aiming) + if (aiming) + { + RotateHead(head); + } + else { float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); @@ -1710,6 +1722,24 @@ namespace Barotrauma } } + private void RotateHead(Limb head) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 dir = (mousePos - head.SimPosition) * Dir; + float rot = MathUtils.VectorToAngle(dir); + var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); + if (neckJoint != null) + { + float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); + float lowerLimit = neckJoint.LowerLimit + offset; + float upperLimit = neckJoint.UpperLimit + offset; + float min = Math.Min(lowerLimit, upperLimit); + float max = Math.Max(lowerLimit, upperLimit); + rot = Math.Clamp(rot, min, max); + } + head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); + } + private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 874e6e000..9277a79ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -805,7 +805,7 @@ namespace Barotrauma SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData()); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index ff3af7ccf..e405a887e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,7 +37,9 @@ namespace Barotrauma Pursue, FollowThrough, FollowThroughUntilCanAttack, - IdleUntilCanAttack + IdleUntilCanAttack, + Reverse, + ReverseUntilCanAttack } struct AttackResult @@ -102,7 +104,7 @@ namespace Barotrauma public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float Range { get => _range * RangeMultiplier; @@ -110,13 +112,16 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, IsPropertySaveable.Yes, 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)] + [Serialize(0.0f, IsPropertySaveable.Yes, 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 = 10000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } + [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + public float MinRange { get; private set; } + [Serialize(0.25f, IsPropertySaveable.Yes, 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; } @@ -356,17 +361,17 @@ namespace Barotrauma { Deserialize(element); - if (element.Attribute("damage") != null || - element.Attribute("bluntdamage") != null || - element.Attribute("burndamage") != null || - element.Attribute("bleedingdamage") != null) + if (element.GetAttribute("damage") != null || + element.GetAttribute("bluntdamage") != null || + element.GetAttribute("burndamage") != null || + element.GetAttribute("bleedingdamage") != null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); } //if level wall damage is not defined, default to the structure damage - if (element.Attribute("LevelWallDamage") == null && - element.Attribute("levelwalldamage") == null) + if (element.GetAttribute("LevelWallDamage") == null && + element.GetAttribute("levelwalldamage") == null) { LevelWallDamage = StructureDamage; } @@ -382,7 +387,7 @@ namespace Barotrauma break; case "affliction": AfflictionPrefab afflictionPrefab; - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); @@ -686,7 +691,7 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(IDamageable target) + public bool IsValidTarget(Entity target) { return TargetType switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3f6245bc7..c63af2b86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -893,6 +893,7 @@ namespace Barotrauma public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; + public Identifier MerchantIdentifier; private bool accessRemovedCharacterErrorShown; public override Vector2 SimPosition @@ -1885,7 +1886,7 @@ namespace Barotrauma if (!attack.IsValidContext(currentContexts)) { return false; } if (attackTarget != null) { - if (!attack.IsValidTarget(attackTarget)) { return false; } + if (!attack.IsValidTarget(attackTarget as Entity)) { return false; } if (attackTarget is ISerializableEntity se && attackTarget is Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } @@ -2011,6 +2012,8 @@ namespace Barotrauma public bool CanSeeCharacter(Character target) { + System.Diagnostics.Debug.Assert(target != null); + if (target == null) { return false; } if (target.Removed) { return false; } Limb seeingLimb = GetSeeingLimb(); if (CanSeeTarget(target, seeingLimb)) { return true; } @@ -2045,7 +2048,6 @@ namespace Barotrauma if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; } if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; } } - return false; } @@ -2056,6 +2058,8 @@ namespace Barotrauma public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) { + System.Diagnostics.Debug.Assert(target != null); + if (target == null) { return false; } seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity; if (seeingEntity == null) { return false; } ISpatialEntity sourceEntity = seeingEntity ; @@ -3148,7 +3152,8 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } - foreach (Item inventoryItem in Inventory.AllItemsMod) + List inventoryItems = new List(Inventory.AllItemsMod); + foreach (Item inventoryItem in inventoryItems) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) { @@ -3156,17 +3161,25 @@ namespace Barotrauma inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents); } } + //this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed) + Spawner.AddEntityToRemoveQueue(this); } } - - Spawner.AddEntityToRemoveQueue(this); + else + { + Spawner.AddEntityToRemoveQueue(this); + } } public void DespawnNow(bool createNetworkEvents = true) { despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); - Spawner.Update(createNetworkEvents); + //update twice: first to spawn the duffel bag and move the items into it, then to remove the character + for (int i = 0; i < 2; i++) + { + Spawner.Update(createNetworkEvents); + } } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -4012,7 +4025,7 @@ namespace Barotrauma if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); } isDead = true; @@ -4168,6 +4181,11 @@ namespace Barotrauma } DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")"); +#if CLIENT + //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns + Inventory?.ApplyReceivedState(); +#endif + base.Remove(); foreach (Item heldItem in HeldItems.ToList()) @@ -4179,12 +4197,12 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); + + if (Controlled == this) { Controlled = null; } #endif CharacterList.Remove(this); - if (Controlled == this) { Controlled = null; } - if (Inventory != null) { foreach (Item item in Inventory.AllItems) @@ -4266,7 +4284,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); + newItem.CreateStatusEvent(); } #if SERVER newItem.GetComponent()?.SyncHistory(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 11eab9eba..731461475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -51,7 +51,7 @@ namespace Barotrauma } } - public struct StatusEventData : IEventData + public struct CharacterStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index f2bc10d8b..df1cede68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -852,6 +852,19 @@ namespace Barotrauma #if CLIENT public void RecreateHead(MultiplayerPreferences characterSettings) { + if (characterSettings.HairIndex == -1 && + characterSettings.BeardIndex == -1 && + characterSettings.MoustacheIndex == -1 && + characterSettings.FaceAttachmentIndex == -1) + { + //randomize if nothing is set + SetAttachments(Rand.RandSync.Unsynced); + characterSettings.HairIndex = Head.HairIndex; + characterSettings.BeardIndex = Head.BeardIndex; + characterSettings.MoustacheIndex = Head.MoustacheIndex; + characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex; + } + RecreateHead( characterSettings.TagSet.ToImmutableHashSet(), characterSettings.HairIndex, @@ -859,9 +872,14 @@ namespace Barotrauma characterSettings.MoustacheIndex, characterSettings.FaceAttachmentIndex); - Head.SkinColor = characterSettings.SkinColor; - Head.HairColor = characterSettings.HairColor; - Head.FacialHairColor = characterSettings.FacialHairColor; + Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor); + Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor); + Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor); + + Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor) + { + return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced); + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index da10f3719..065a7cc10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -287,7 +287,7 @@ namespace Barotrauma StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } - if (element.Attribute("interval") != null) + if (element.GetAttribute("interval") != null) { MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f); } @@ -409,7 +409,7 @@ namespace Barotrauma HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); - if (element.Attribute("nameidentifier") != null) + if (element.GetAttribute("nameidentifier") != null) { Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 320d295d0..86957c713 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -47,22 +47,24 @@ namespace Barotrauma HighlightSprite = new Sprite(subElement); break; case "vitalitymultiplier": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); continue; } - - Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); - float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - if (!afflictionIdentifier.IsEmpty) + var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); + if (vitalityMultipliers == null) { - VitalityMultipliers.Add(afflictionIdentifier, multiplier); + vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); + } + if (vitalityMultipliers != null) + { + float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); + vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); } else { - VitalityTypeMultipliers.Add(afflictionType, multiplier); + DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b08cafe51..c430090d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -540,6 +540,8 @@ namespace Barotrauma private set; } + public Items.Components.Rope AttachedRope { get; set; } + public string Name => Params.Name; // These properties are exposed for status effects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 2fdb19b98..a1920a820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -103,6 +103,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation @@ -131,7 +134,7 @@ namespace Barotrauma get => MathHelper.ToDegrees(FootAngleInRadians); set { - FootAngleInRadians = MathHelper.ToRadians(value); + FootAngleInRadians = MathHelper.ToRadians(value); } } public float FootAngleInRadians { get; private set; } @@ -156,6 +159,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } public interface IHumanAnimation @@ -166,5 +172,7 @@ namespace Barotrauma float ArmMoveStrength { get; set; } float HandMoveStrength { get; set; } + + bool FixedHeadAngle { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 640bf791c..b1314218f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -104,6 +104,9 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool DrawLast { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -127,31 +130,36 @@ namespace Barotrauma public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; - public static XElement CreateVariantXml(XElement selfXml, XElement parentXml) + public static XElement CreateVariantXml(XElement variantXML, XElement baseXML) { - XElement newXml = selfXml.CreateVariantXML(parentXml); - - XElement selfAi = selfXml.GetChildElement("ai"); - XElement parentAi = parentXml.GetChildElement("ai"); - - if (parentAi is null || parentAi.Elements().None() - || selfAi is null || selfAi.Elements().None()) + XElement newXml = variantXML.CreateVariantXML(baseXML); + XElement variantAi = variantXML.GetChildElement("ai"); + XElement baseAi = baseXML.GetChildElement("ai"); + if (baseAi is null || baseAi.Elements().None() + || variantAi is null || variantAi.Elements().None()) { return newXml; } - - //discard the inherited targets, just keep the new ones + // CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions. var finalAiElement = newXml.GetChildElement("ai"); - foreach (var finalTarget in finalAiElement!.Elements().ToArray()) + var processedTags = new HashSet(); + foreach (var aiTarget in finalAiElement.Elements().ToArray()) { - finalTarget.Remove(); + string tag = aiTarget.GetAttributeString("tag", null); + if (tag == null) { continue; } + if (processedTags.Contains(tag)) + { + aiTarget.Remove(); + continue; + } + processedTags.Add(tag); + var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); + var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); + if (matchInSelf != null && matchInParent != null) + { + aiTarget.ReplaceWith(new XElement(matchInSelf)); + } } - - foreach (var inheritorTarget in selfAi.Elements()) - { - finalAiElement.Add(new XElement(inheritorTarget)); - } - return newXml; } @@ -574,6 +582,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)] + public float DamageThreshold { get; private set; } + [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } @@ -785,6 +796,9 @@ namespace Barotrauma [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] public AttackPattern AttackPattern { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable] + public bool PrioritizeSubCenter { get; set; } + #region Sweep [Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs index 9bbb48bf5..f237deea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - if (conditionElement.Attribute("submarinetype") != null) + if (conditionElement.GetAttribute("submarinetype") != null) { submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index 59c8254b6..9e85d367e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - if (conditionElement.Attribute("hasoutpost") != null) + if (conditionElement.GetAttribute("hasoutpost") != null) { hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs index 484e0c7bd..f602cd4c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -1,7 +1,4 @@ - -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionInHull : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs index a6191d470..9ffd65c6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -1,7 +1,4 @@ - -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionInWater : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index ad164626d..2e505890e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -45,7 +45,7 @@ namespace Barotrauma } } - if (element.Attribute("description") != null) + if (element.GetAttribute("description") != null) { string description = element.GetAttributeString("description", string.Empty); Description = Description.Fallback(TextManager.Get(description)).Fallback(description); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 73e07dcf1..35865bb05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -30,6 +30,7 @@ namespace Barotrauma public readonly bool NotSyncedInMultiplayer; public readonly ImmutableHashSet? AlternativeTypes; public readonly ImmutableHashSet Names; + private readonly MethodInfo? contentPathMutator; public TypeInfo(Type type) { @@ -40,9 +41,11 @@ namespace Barotrauma var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; + contentPathMutator + = Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public); HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; - if (type.GetCustomAttribute()?.Names is { } altNames) + if (type.GetCustomAttribute(inherit: false)?.Names is { } altNames) { names.UnionWith(altNames); } @@ -50,6 +53,10 @@ namespace Barotrauma Names = names.ToImmutableHashSet(); } + public ContentPath MutateContentPath(ContentPath path) + => (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path }) + ?? path; + public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); } @@ -80,6 +87,7 @@ namespace Barotrauma } try { + filePath = type.MutateContentPath(filePath); if (!File.Exists(filePath.FullPath)) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs new file mode 100644 index 000000000..85f6a6fa1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs @@ -0,0 +1,28 @@ +using System; +using Barotrauma.IO; + +namespace Barotrauma +{ + sealed class ServerExecutableFile : OtherFile + { + //This content type doesn't do very much on its own, it's handled manually by the Host Server menu + public ServerExecutableFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public static ContentPath MutateContentPath(ContentPath path) + { + if (File.Exists(path.FullPath)) { return path; } + + string rawValueWithoutExtension() + => Barotrauma.IO.Path.Combine( + Barotrauma.IO.Path.GetDirectoryName(path.RawValue ?? ""), + Barotrauma.IO.Path.GetFileNameWithoutExtension(path.RawValue ?? "")).CleanUpPath(); + + path = ContentPath.FromRaw(path.ContentPackage, rawValueWithoutExtension()); + if (File.Exists(path.FullPath)) { return path; } + + path = ContentPath.FromRaw(path.ContentPackage, + rawValueWithoutExtension() + ".exe"); + return path; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index dae572835..586da6857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -110,7 +110,7 @@ namespace Barotrauma !expectedHash.IsNullOrWhiteSpace() && !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); - public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); + public IEnumerable GetFiles() where T : ContentFile => Files.OfType(); public IEnumerable GetFiles(Type type) => !type.IsSubclassOf(typeof(ContentFile)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 33daaec39..02a6f4401 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RequiredByCorePackage : Attribute { public readonly ImmutableHashSet AlternativeTypes; @@ -18,7 +18,7 @@ namespace Barotrauma } } - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class AlternativeContentTypeNames : Attribute { public readonly ImmutableHashSet Names; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index bf2faed95..0758b7042 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma Core?.UnloadPackage(); Core = newCore; foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } - ThrowIfDuplicates(All); + SortContent(); yield return new LoadProgress(1.0f); } @@ -60,7 +60,7 @@ namespace Barotrauma if (Core == null) { return; } Core.UnloadPackage(); Core.LoadPackage(); - ThrowIfDuplicates(All); + SortContent(); } public static void EnableRegular(RegularPackage p) @@ -133,7 +133,7 @@ namespace Barotrauma } } - public static void SortContent() + private static void SortContent() { ThrowIfDuplicates(All); All @@ -165,6 +165,20 @@ namespace Barotrauma SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray()); } + public static void RefreshUpdatedMods() + { + if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) + { + SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ?? + ContentPackageManager.CorePackages.First()); + } + SetRegular(Regular + .Select(p => ContentPackageManager.RegularPackages.Contains(p) + ? p + : ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.SteamWorkshopId == p.SteamWorkshopId)) + .ToArray()); + } + public static void BackUp() { if (BackupPackages.Core != null || BackupPackages.Regular != null) @@ -327,7 +341,8 @@ namespace Barotrauma => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); public static IEnumerable AllPackages - => LocalPackages.CollectionConcat(WorkshopPackages); + => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages) + .OfType(); public static void UpdateContentPackageList() { @@ -447,13 +462,13 @@ namespace Barotrauma int pkgCount = 1 + enabledRegularPackages.Count; //core + regular - loadingRange = new Range(0.01f, 1.0f / pkgCount); + loadingRange = new Range(0.01f, 0.01f + (0.99f / pkgCount)); foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage)) { yield return p.Transform(loadingRange); } - loadingRange = new Range(1.0f / pkgCount, 1.0f); + loadingRange = new Range(0.01f + (0.99f / pkgCount), 1.0f); foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages)) { yield return p.Transform(loadingRange); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 8b328672a..d105e09cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -49,7 +49,7 @@ namespace Barotrauma .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); } } - var allPackages = ContentPackageManager.AllPackages; + var allPackages = ContentPackageManager.EnabledPackages.All; foreach (Identifier otherModName in otherMods) { if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 60f048a4e..5e378bcb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -53,8 +53,6 @@ namespace Barotrauma public IEnumerable GetChildElements(string name) => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase)); - public XAttribute? Attribute(string name) => Element.Attribute(name); - public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); public IEnumerable Attributes() => Element.Attributes(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs index 2c6d0df5e..f6fb91198 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs @@ -11,7 +11,7 @@ namespace Barotrauma { Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " + $"with name or ID \"{missingPackage ?? "[NULL]"}\" " + - $"that is not currently installed."; + $"that is not currently enabled."; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8562d6800..f9b027f33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -168,25 +168,34 @@ namespace Barotrauma }; })); + void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab + { + NewMessage("***************", Color.Cyan); + foreach (T prefab in prefabs) + { + if (prefab.Name.IsNullOrEmpty()) { continue; } + string text = $"- {prefab.Name}"; + if (prefab.Tags.Any()) + { + text += $" ({string.Join(", ", prefab.Tags)})"; + } + if (prefab.AllowedLinks?.Any() ?? false) + { + text += $", Links: {string.Join(", ", prefab.AllowedLinks)}"; + } + NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple); + } + NewMessage("***************", Color.Cyan); + } commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) => { - NewMessage("***************", Color.Cyan); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - if (itemPrefab.Name.IsNullOrEmpty()) { continue; } - string text = $"- {itemPrefab.Name}"; - if (itemPrefab.Tags.Any()) - { - text += $" ({string.Join(", ", itemPrefab.Tags)})"; - } - if (itemPrefab.AllowedLinks.Any()) - { - text += $", Links: {string.Join(", ", itemPrefab.AllowedLinks)}"; - } - NewMessage(text, Color.Cyan); - } - NewMessage("***************", Color.Cyan); + printMapEntityPrefabs(ItemPrefab.Prefabs); + })); + + commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) => + { + printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs); })); @@ -202,6 +211,7 @@ namespace Barotrauma string[] creatureAndJobNames = CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) + .OrderBy(s => s) .ToArray(); return new string[][] @@ -732,9 +742,16 @@ namespace Barotrauma { #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { return; } - Character.Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - GameMain.Client?.SendConsoleCommand("freecam"); + + if (GameMain.Client == null) + { + Character.Controlled = null; + GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; + } + else + { + GameMain.Client?.SendConsoleCommand("freecam"); + } #endif }, isCheat: true)); @@ -1786,15 +1803,26 @@ namespace Barotrauma { if (GameMain.GameSession?.Map?.CurrentLocation is Location location) { - - var msg = "--- Location: " + location.Name + " ---"; - msg += "\nBalance: " + location.StoreCurrentBalance; - msg += "\nPrice modifier: " + location.StorePriceModifier + "%"; - msg += "\nDaily specials:"; - location.DailySpecials.ForEach(i => msg += "\n - " + i.Name.Value); - msg += "\nRequested goods:"; - location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name.Value); - NewMessage(msg); + if (location.Stores != null) + { + var msg = "--- Location: " + location.Name + " ---"; + foreach (var store in location.Stores) + { + msg += $"\nStore identifier: {store.Value.Identifier}"; + msg += $"\nBalance: {store.Value.Balance}"; + msg += $"\nPrice modifier: {store.Value.PriceModifier}%"; + msg += "\nDaily specials:"; + store.Value.DailySpecials.ForEach(i => msg += $"\n - {i.Name}"); + msg += "\nRequested goods:"; + store.Value.RequestedGoods.ForEach(i => msg += $"\n - {i.Name}"); + + } + NewMessage(msg); + } + else + { + NewMessage($"No stores at {location}, can't show store info."); + } } else { @@ -2382,7 +2410,7 @@ namespace Barotrauma public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { - ThrowError(error.Value); + ThrowError(error.Value, e, createMessageBox, appendStackTrace); } public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index eb6756fe0..3c8e23fd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -32,7 +32,7 @@ namespace Barotrauma public ArtifactEvent(EventPrefab prefab) : base(prefab) { - if (prefab.ConfigElement.Attribute("itemname") != null) + if (prefab.ConfigElement.GetAttribute("itemname") != null) { DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item."); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index edd2baefa..b01ede512 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -30,7 +30,7 @@ namespace Barotrauma public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) { - Text = elem.Attribute("text")?.Value ?? ""; + Text = elem.GetAttribute("text")?.Value ?? ""; Actions = new List(); EndConversation = elem.GetAttributeBool("endconversation", false); foreach (var e in elem.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 6b09ce371..d7d042774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -153,6 +153,13 @@ namespace Barotrauma swarmSpawned = true; } +#if DEBUG || UNSTABLE + if (State == 1 && !level.CheckBeaconActive()) + { + DebugConsole.ThrowError("Beacon became inactive!"); + State = 2; + } +#endif } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 934b9df66..8a02928df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -248,7 +248,7 @@ namespace Barotrauma { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); + int deliveredItemCount = items.Count(it => IsItemDelivered(it)); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { GiveReward(); @@ -267,5 +267,12 @@ namespace Barotrauma items.Clear(); failed = !completed; } + + private bool IsItemDelivered(Item item) + { + if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } + var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; + return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index a525764bd..413e835e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -419,17 +419,17 @@ namespace Barotrauma public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) { int remainingRewards = totalReward; - HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); - float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); - if (sum == 0) { return remainingRewards; } - foreach (Character character in nonBotCrew) + float sum = GetRewardDistibutionSum(crew); + if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } + foreach (Character character in crew) { - float rewardWeight = character.Wallet.RewardDistribution / sum; - int reward = (int)Math.Floor(totalReward * rewardWeight); - reward = Math.Max(remainingRewards, reward); + int rewardDistribution = character.Wallet.RewardDistribution; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + int reward = (int)(totalReward * rewardWeight); + reward = Math.Min(remainingRewards, reward); character.Wallet.Give(reward); remainingRewards -= reward; - if (0 >= remainingRewards) { break; } + if (remainingRewards <= 0) { break; } } return remainingRewards; @@ -442,26 +442,27 @@ namespace Barotrauma IEnumerable characters = crewManager.GetCharacters(); #if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters); #elif CLIENT return characters; #endif - static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; } } + public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + + public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { - float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - if (sum == 0) { return (0, 0); } + float sum = GetRewardDistibutionSum(crew, rewardDistribution); + if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); } - float rewardWeight = rewardDistribution / sum; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); return reward switch { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), - None _ => (0, rewardPercentage), + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), + None _ => (0, rewardPercentage, sum), _ => throw new ArgumentOutOfRangeException() }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 8b6c7e1a1..192830b91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -209,7 +209,7 @@ namespace Barotrauma break; case "locationtype": case "connectiontype": - if (subElement.Attribute("identifier") != null) + if (subElement.GetAttribute("identifier") != null) { AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", "")); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index ec2aea487..c51ad855c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -228,7 +228,7 @@ namespace Barotrauma } GiveReward(); completed = true; - if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))) + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) { level.LevelData.HasHuntingGrounds = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7526d1b25..858e8a74d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -48,7 +48,7 @@ namespace Barotrauma { containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); - if (prefab.ConfigElement.Attribute("itemname") != null) + if (prefab.ConfigElement.GetAttribute("itemname") != null) { DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 585de85c8..23a121111 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -30,23 +30,21 @@ namespace Barotrauma } #if CLIENT - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) + public PurchasedItem(ItemPrefab itemPrefab, int quantity) + : this(itemPrefab, quantity, buyer: null) { } +#endif + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; } -#elif SERVER - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) - { - ItemPrefab = itemPrefab; - Quantity = quantity; - IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; - } -#endif + public override string ToString() + { + return $"{ItemPrefab.Name} ({Quantity})"; + } } class SoldItem @@ -133,11 +131,11 @@ namespace Barotrauma public const int MaxQuantity = 100; - public List ItemsInBuyCrate { get; } = new List(); - public List ItemsInSellCrate { get; } = new List(); - public List ItemsInSellFromSubCrate { get; } = new List(); - public List PurchasedItems { get; } = new List(); - public List SoldItems { get; } = new List(); + public Dictionary> ItemsInBuyCrate { get; } = new Dictionary>(); + public Dictionary> ItemsInSellCrate { get; } = new Dictionary>(); + public Dictionary> ItemsInSellFromSubCrate { get; } = new Dictionary>(); + public Dictionary> PurchasedItems { get; } = new Dictionary>(); + public Dictionary> SoldItems { get; } = new Dictionary>(); private readonly CampaignMode campaign; @@ -154,6 +152,60 @@ namespace Barotrauma this.campaign = campaign; } + private List GetItems(Identifier identifier, Dictionary> items, bool create = false) + { + if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null) + { + return storeSpecificItems; + } + else if (create) + { + storeSpecificItems = new List(); + items.Add(identifier, storeSpecificItems); + return storeSpecificItems; + } + else + { + return new List(); + } + } + + public List GetBuyCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInBuyCrate, create); + + public List GetBuyCrateItems(Location.StoreInfo store, bool create = false) => GetBuyCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab) => GetBuyCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetBuyCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetBuyCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSellCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellCrate, create); + + public List GetSellCrateItems(Location.StoreInfo store, bool create = false) => GetSellCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab) => GetSellCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetSellCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSellCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSubCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellFromSubCrate, create); + + public List GetSubCrateItems(Location.StoreInfo store, bool create = false) => GetSubCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab) => GetSubCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetSubCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSubCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetPurchasedItems(Identifier identifier, bool create = false) => GetItems(identifier, PurchasedItems, create); + + public List GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create); + + public List GetSoldItems(Location.StoreInfo store, bool create = false) => GetSoldItems(store?.Identifier ?? Identifier.Empty, create); + public void ClearItemsInBuyCrate() { ItemsInBuyCrate.Clear(); @@ -172,61 +224,62 @@ namespace Barotrauma OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void SetPurchasedItems(List items) + public void SetPurchasedItems(Dictionary> purchasedItems) { PurchasedItems.Clear(); - PurchasedItems.AddRange(items); + foreach (var entry in purchasedItems) + { + PurchasedItems.Add(entry.Key, entry.Value); + } OnPurchasedItemsChanged?.Invoke(); } - public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) + public void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { - var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemInCrate != null) + if (GetBuyCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemInCrate.Quantity += changeInQuantity; - if (itemInCrate.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInBuyCrate.Remove(itemInCrate); + GetBuyCrateItems(storeIdentifier, create: true).Remove(item); } } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); - ItemsInBuyCrate.Add(itemInCrate); + GetBuyCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client)); } OnItemsInBuyCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) + public void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { - var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemInCrate != null) + if (GetSubCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemInCrate.Quantity += changeInQuantity; - if (itemInCrate.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInSellFromSubCrate.Remove(itemInCrate); + GetSubCrateItems(storeIdentifier)?.Remove(item); } } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); - ItemsInSellFromSubCrate.Add(itemInCrate); + GetSubCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client)); } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void PurchaseItems(List itemsToPurchase, bool removeFromCrate, Client client = null) + public void PurchaseItems(Identifier storeIdentifier, List itemsToPurchase, bool removeFromCrate, Client client = null) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab)); - + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var itemsPurchasedFromStore = GetPurchasedItems(storeIdentifier, create: true); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab)); + var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true); foreach (PurchasedItem item in itemsToPurchase) { // Add to the purchased items - var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab); + var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab); if (purchasedItem != null) { purchasedItem.Quantity += item.Quantity; @@ -234,53 +287,54 @@ namespace Barotrauma else { purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); - PurchasedItems.Add(purchasedItem); + itemsPurchasedFromStore.Add(purchasedItem); } - // Exchange money - var itemValue = item.Quantity * buyValues[item.ItemPrefab]; + int itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.GetWallet(client).TryDeduct(itemValue); GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); - Location.StoreCurrentBalance += itemValue; - + store.Balance += itemValue; if (removeFromCrate) { // Remove from the shopping crate - var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab); - if (crateItem != null) + if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem) { crateItem.Quantity -= item.Quantity; - if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); } + if (crateItem.Quantity < 1) { itemsInStoreCrate.Remove(crateItem); } } } } OnPurchasedItemsChanged?.Invoke(); } - public Dictionary GetBuyValuesAtCurrentLocation(IEnumerable items) + public Dictionary GetBuyValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable items) { var buyValues = new Dictionary(); + var store = Location?.GetStore(storeIdentifier); + if (store == null) { return buyValues; } foreach (var item in items) { if (item == null) { continue; } if (!buyValues.ContainsKey(item)) { - var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0; + int buyValue = store?.GetAdjustedItemBuyPrice(item) ?? 0; buyValues.Add(item, buyValue); } } return buyValues; } - public Dictionary GetSellValuesAtCurrentLocation(IEnumerable items) + public Dictionary GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable items) { var sellValues = new Dictionary(); + var store = Location?.GetStore(storeIdentifier); + if (store == null) { return sellValues; } foreach (var item in items) { if (item == null) { continue; } if (!sellValues.ContainsKey(item)) { - var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0; + int sellValue = store?.GetAdjustedItemSellPrice(item) ?? 0; sellValues.Add(item, sellValue); } } @@ -289,7 +343,13 @@ namespace Barotrauma public void CreatePurchasedItems() { - CreateItems(PurchasedItems, Submarine.MainSub); + var items = new List(); + foreach (var storeSpecificItems in PurchasedItems) + { + items.AddRange(storeSpecificItems.Value); + } + CreateItems(items, Submarine.MainSub); + PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(); } @@ -509,33 +569,41 @@ namespace Barotrauma public void SavePurchasedItems(XElement parentElement) { var itemsElement = new XElement("cargo"); - foreach (PurchasedItem item in PurchasedItems) + foreach (var storeSpecificItems in PurchasedItems) { - if (item?.ItemPrefab == null) { continue; } - itemsElement.Add(new XElement("item", - new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity), - new XAttribute("buyer", item.BuyerCharacterInfoId))); + foreach (var item in storeSpecificItems.Value) + { + if (item?.ItemPrefab == null) { continue; } + itemsElement.Add(new XElement("item", + new XAttribute("id", item.ItemPrefab.Identifier), + new XAttribute("qty", item.Quantity), + new XAttribute("storeid", storeSpecificItems.Key), + new XAttribute("buyer", item.BuyerCharacterInfoId))); + } } parentElement.Add(itemsElement); } public void LoadPurchasedItems(XElement element) { - var purchasedItems = new List(); + var purchasedItems = new Dictionary>(); if (element != null) { foreach (XElement itemElement in element.GetChildElements("item")) { - string id = itemElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); + string prefabId = itemElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(prefabId)) { continue; } + var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId); if (prefab == null) { continue; } int qty = itemElement.GetAttributeInt("qty", 0); + Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant"); int buyerId = itemElement.GetAttributeInt("buyer", 0); - - purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId)); - + if (!purchasedItems.TryGetValue(storeId, out var storeItems)) + { + storeItems = new List(); + purchasedItems.Add(storeId, storeItems); + } + storeItems.Add(new PurchasedItem(prefab, qty, buyerId)); } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index d32c25575..abd84ef03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -25,12 +25,18 @@ namespace Barotrauma public int Balance; } + /// + /// Network message for the server to update wallet values to clients + /// internal struct NetWalletUpdate : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] public NetWalletTransaction[] Transactions; } + /// + /// Network message for the client to transfer money between wallets + /// [NetworkSerialize] internal struct NetWalletTransfer : INetSerializableStruct { @@ -39,7 +45,10 @@ namespace Barotrauma public int Amount; } - internal struct NetWalletSalaryUpdate : INetSerializableStruct + /// + /// Network message for the client to set the salary of someone + /// + internal struct NetWalletSetSalaryUpdate : INetSerializableStruct { [NetworkSerialize] public ushort Target; @@ -48,6 +57,10 @@ namespace Barotrauma public int NewRewardDistribution; } + /// + /// Represents the difference in balance and salary when a wallet gets updated + /// Not really used right now but could be used for notifications when receiving funds similar to how talents do it + /// [NetworkSerialize] internal struct WalletChangedData : INetSerializableStruct { @@ -82,6 +95,9 @@ namespace Barotrauma } } + /// + /// Represents an update that changed the amount of money or salary of the wallet + /// [NetworkSerialize] internal struct NetWalletTransaction : INetSerializableStruct { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index dbde33069..64bd03cb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -321,15 +321,32 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { - DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); + DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); } else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); + // Adjust the prefab commonness based on the difficulty tag + var prefabs = huntingGroundsMissionPrefabs.ToList(); + var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList(); + for (int i = 0; i < prefabs.Count; i++) + { + var prefab = prefabs[i]; + var weight = weights[i]; + if (prefab.Tags.Contains("easy")) + { + weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty)); + } + else if (prefab.Tags.Contains("hard")) + { + weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty)); + } + weights[i] = weight; + } + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -596,22 +613,22 @@ namespace Barotrauma if (map != null && CargoManager != null) { map.CurrentLocation.RegisterTakenItems(takenItems); - map.CurrentLocation.AddToStock(CargoManager.SoldItems); + map.CurrentLocation.AddStock(CargoManager.SoldItems); CargoManager.ClearSoldItemsProjSpecific(); - map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems); + map.CurrentLocation.RemoveStock(CargoManager.PurchasedItems); } if (GameMain.NetworkMember == null) { - CargoManager.ClearItemsInBuyCrate(); - CargoManager.ClearItemsInSellCrate(); - CargoManager.ClearItemsInSellFromSubCrate(); + CargoManager?.ClearItemsInBuyCrate(); + CargoManager?.ClearItemsInSellCrate(); + CargoManager?.ClearItemsInSellFromSubCrate(); } else { if (GameMain.NetworkMember.IsServer) { CargoManager?.ClearItemsInBuyCrate(); - // TODO: CargoManager?.ClearItemsInSellFromSubCrate(); + CargoManager?.ClearItemsInSellFromSubCrate(); } else if (GameMain.NetworkMember.IsClient) { @@ -772,6 +789,11 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && + character.HumanPrefab is { Identifier: var merchantId }) + { + character.MerchantIdentifier = merchantId; + } character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 81c8cfbf2..970f2eaff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -211,7 +212,6 @@ namespace Barotrauma #endif } - public static List GetCampaignSubs() { bool isSubmarineVisible(SubmarineInfo s) @@ -242,5 +242,78 @@ namespace Barotrauma return availableSubs; } + private static void WriteItems(IWriteMessage msg, Dictionary> purchasedItems) + { + msg.Write((byte)purchasedItems.Count); + foreach (var storeItems in purchasedItems) + { + msg.Write(storeItems.Key); + msg.Write((UInt16)storeItems.Value.Count); + foreach (var item in storeItems.Value) + { + msg.Write(item.ItemPrefab.Identifier); + msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity); + } + } + } + + private static Dictionary> ReadPurchasedItems(IReadMessage msg, Client sender) + { + var items = new Dictionary>(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier storeId = msg.ReadIdentifier(); + items.Add(storeId, new List()); + UInt16 itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Identifier itemId = msg.ReadIdentifier(); + int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + items[storeId].Add(new PurchasedItem(ItemPrefab.Prefabs[itemId], quantity, sender)); + } + } + return items; + } + + private static void WriteItems(IWriteMessage msg, Dictionary> soldItems) + { + msg.Write((byte)soldItems.Count); + foreach (var storeItems in soldItems) + { + msg.Write(storeItems.Key); + msg.Write((UInt16)storeItems.Value.Count); + foreach (var item in storeItems.Value) + { + msg.Write(item.ItemPrefab.Identifier); + msg.Write((UInt16)item.ID); + msg.Write(item.Removed); + msg.Write(item.SellerID); + msg.Write((byte)item.Origin); + } + } + } + + private static Dictionary> ReadSoldItems(IReadMessage msg) + { + var soldItems = new Dictionary>(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier storeId = msg.ReadIdentifier(); + soldItems.Add(storeId, new List()); + UInt16 itemCount = msg.ReadUInt16(); + for (int j = 0; j < storeCount; j++) + { + Identifier prefabId = msg.ReadIdentifier(); + UInt16 itemId = msg.ReadUInt16(); + bool removed = msg.ReadBoolean(); + byte sellerId = msg.ReadByte(); + byte origin = msg.ReadByte(); + soldItems[storeId].Add(new SoldItem(ItemPrefab.Prefabs[prefabId], itemId, removed, sellerId, (SoldItem.SellOrigin)origin)); + } + } + return soldItems; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 0da770a21..e6fc1f3e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -754,9 +754,9 @@ namespace Barotrauma try { - IEnumerable crewCharacters = GetSessionCrewCharacters(); + ImmutableArray crewCharacters = GetSessionCrewCharacters().ToImmutableArray(); - int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution + int prevMoney = GetAmountOfMoney(crewCharacters); foreach (Mission mission in missions) { @@ -828,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -840,6 +840,17 @@ namespace Barotrauma { RoundEnding = false; } + + int GetAmountOfMoney(IEnumerable crew) + { + if (!(GameMode is CampaignMode campaign)) { return 0; } + + return GameMain.NetworkMember switch + { + null => campaign.Bank.Balance, + _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance + }; + } } public void LogEndRoundStats(string eventId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 6a8df4f5e..f85b6bafe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets + if (Campaign.GetWallet(client).TryDeduct(price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index e34ef8f1b..f8249369e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 6f3383913..384b88db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components { int index = i - 1; string attributeName = "handle" + i; - var attribute = element.Attribute(attributeName); + var attribute = element.GetAttribute(attributeName); // If no value is defind for handle2, use the value of handle1. var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue; handlePos[index] = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 9477d5a76..1319254d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Items.Components { this.item = item; - if (element.Attribute("limbfixamount") != null) + if (element.GetAttribute("limbfixamount") != null) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); } @@ -140,10 +140,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "fixable": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); - fixableEntities.Add(subElement.Attribute("name").Value.ToIdentifier()); + fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier()); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8726d8462..845ae5efe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -344,7 +344,7 @@ namespace Barotrauma.Items.Components break; case "requiredskill": case "requiredskills": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 40bd8a5b0..891fc3fce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -424,6 +424,15 @@ namespace Barotrauma.Items.Components } } + public override void UpdateBroken(float deltaTime, Camera cam) + { + //update when the item is broken too to get OnContaining effects to execute and contained item positions to update + if (IsActive) + { + Update(deltaTime, cam); + } + } + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 03c061552..7d03844eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components { if (subElement.Name != "limbposition") { continue; } string limbStr = subElement.GetAttributeString("limb", ""); - if (!Enum.TryParse(subElement.Attribute("limb").Value, out LimbType limbType)) + if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) { DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index d0ad2a519..27b9ae460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -186,8 +186,19 @@ namespace Barotrauma.Items.Components if (!isClient) { MoveIngredientsToInputContainer(selectedItem); + if (selectedItem.RequiredMoney > 0) + { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + { + user.Wallet.Deduct(selectedItem.RequiredMoney); + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + campaign.Bank.Deduct(selectedItem.RequiredMoney); + } + } } - + requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; @@ -508,6 +519,22 @@ namespace Barotrauma.Items.Components if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } + if (fabricableItem.RequiredMoney > 0) + { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + { + if (character?.Wallet == null || character.Wallet.Balance < fabricableItem.RequiredMoney) { return false; } + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; } + } + else + { + return false; + } + } + return fabricableItem.RequiredItems.All(requiredItem => { int availablePrefabsAmount = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index b375eb205..0ee0d9d50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); + hull.BallastFlora.CreateNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index be83e2267..0a0d2af0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; using Microsoft.Xna.Framework; using System.Collections.Generic; #if CLIENT @@ -151,20 +150,6 @@ namespace Barotrauma.Items.Components } set { - if (powerIn != null) - { - if (powerIn.Grid != null) - { - powerIn.Grid.Voltage = Math.Max(0.0f, value); - } - } - else if (powerOut != null) - { - if (powerOut.Grid != null) - { - powerOut.Grid.Voltage = Math.Max(0.0f, value); - } - } voltage = Math.Max(0.0f, value); } } @@ -213,11 +198,6 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif - if (powerIn == null) - { - //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) - Voltage -= deltaTime; - } } public override void Update(float deltaTime, Camera cam) @@ -470,6 +450,11 @@ namespace Barotrauma.Items.Components //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { + //Make voltage decay to ensure the device powers down. + //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery) + //or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead. + powered.Voltage -= deltaTime; + //Handle the device if it's got a power connection if (powered.powerIn != null && powered.powerOut != powered.powerIn) { @@ -500,7 +485,7 @@ namespace Barotrauma.Items.Components } else { - powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); powered.GridResolved(powered.powerIn); } } @@ -541,7 +526,7 @@ namespace Barotrauma.Items.Components else { //Perform power calculations for the singular connection - float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); if (powered is PowerTransfer pt2) { pt2.PowerLoad = loadOut; @@ -667,7 +652,7 @@ namespace Barotrauma.Items.Components public static bool ValidPowerConnection(Connection conn1, Connection conn2) { - return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fcddb442b..fe52d7480 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -7,7 +7,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; namespace Barotrauma.Items.Components @@ -49,9 +48,8 @@ namespace Barotrauma.Items.Components //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; - //a duration during which the projectile won't drop from the body it's stuck to - private const float PersistentStickJointDuration = 1.0f; - private PrismaticJoint stickJoint; + private Joint stickJoint; + private Vector2 jointAxis; public Attack Attack { get; private set; } @@ -86,8 +84,6 @@ namespace Barotrauma.Items.Components get { return hits; } } - private float persistentStickJointTimer; - [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } @@ -116,13 +112,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] - public bool StickPermanently - { - get; - set; - } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -151,6 +140,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "")] + public bool StickToLightTargets + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -212,16 +208,36 @@ namespace Barotrauma.Items.Components set; } + private float stickTimer; + [Serialize(0f, IsPropertySaveable.No)] + public float StickDuration + { + get; + set; + } + + [Serialize(-1f, IsPropertySaveable.No)] + public float MaxJointTranslation + { + get; + set; + } + private float maxJointTranslationInSimUnits = -1; + + [Serialize(true, IsPropertySaveable.No)] + public bool Prismatic + { + get; + set; + } + public Body StickTarget { get; private set; } - public bool IsStuckToTarget - { - get { return StickTarget != null; } - } + public bool IsStuckToTarget => StickTarget != null; private Category originalCollisionCategories; private Category originalCollisionTargets; @@ -660,23 +676,22 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - if (persistentStickJointTimer > 0.0f && !StickPermanently) + if (StickDuration > 0 && stickTimer > 0) { - persistentStickJointTimer -= deltaTime; + stickTimer -= deltaTime; return; } + float absoluteMaxTranslation = 100; // Update the item's transform to make sure it's inside the same sub as the target (or outside) - if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f) + if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || stickJoint is PrismaticJoint prismaticJoint && Math.Abs(prismaticJoint.JointTranslation) > absoluteMaxTranslation) { item.UpdateTransform(); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (StickTargetRemoved() || - (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) || - Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far + if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits) { Unstick(); #if SERVER @@ -706,7 +721,7 @@ namespace Barotrauma.Items.Components if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine) { - if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } + if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } } else if (target.Body.UserData is Limb limb) { @@ -778,7 +793,10 @@ namespace Barotrauma.Items.Components Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); - if (hits.Contains(target.Body)) { return true; } + if (hits.Contains(target.Body)) + { + return true; + } } else { @@ -936,14 +954,12 @@ namespace Barotrauma.Items.Components { 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 && - hits.Count() >= MaxTargetsToHit && - target.Body.Mass > item.body.Mass * 0.5f && + else if ( stickJoint == null && StickTarget == null && + StickToStructures && target.Body.UserData is Structure || + ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToStructures && target.Body.UserData is Structure) || - (StickToItems && target.Body.UserData is Item))) + (StickToItems && target.Body.UserData is Item)))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), @@ -1025,30 +1041,39 @@ namespace Barotrauma.Items.Components private void StickToTarget(Body targetBody, Vector2 axis) { if (stickJoint != null) { return; } - - stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) + jointAxis = axis; + item.body.ResetDynamics(); + if (Prismatic) { - MotorEnabled = true, - MaxMotorForce = 30.0f, - LimitEnabled = true, - Breakpoint = 1000.0f - }; + stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, useWorldCoordinates: true) + { + MotorEnabled = true, + MaxMotorForce = 30.0f, + LimitEnabled = true, + Breakpoint = 1000.0f + }; - if (StickPermanently) - { - stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; - item.body.ResetDynamics(); + if (maxJointTranslationInSimUnits == -1) + { + if (item.Sprite != null && MaxJointTranslation < 0) + { + MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale; + } + MaxJointTranslation = Math.Min(MaxJointTranslation, 1000); + maxJointTranslationInSimUnits = ConvertUnits.ToSimUnits(MaxJointTranslation); + } } - else if (item.Sprite != null) + else { - stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); - stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); + stickJoint = new WeldJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, item.body.SimPosition, useWorldCoordinates: true) + { + FrequencyHz = 10.0f, + DampingRatio = 0.5f + }; } - - persistentStickJointTimer = PersistentStickJointDuration; + stickTimer = StickDuration; StickTarget = targetBody; GameMain.World.Add(stickJoint); - IsActive = true; if (targetBody.UserData is Limb limb) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 7c5b320ba..6d1c899b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -1,9 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -12,8 +12,36 @@ namespace Barotrauma.Items.Components private ISpatialEntity source; private Item target; + private Vector2? launchDir; + + private void SetSource(ISpatialEntity source) + { + this.source = source; + if (source is Limb sourceLimb) + { + sourceLimb.AttachedRope = this; + float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir); + } + } + + private void ResetSource() + { + if (source is Limb sourceLimb && sourceLimb.AttachedRope == this) + { + sourceLimb.AttachedRope = null; + } + source = null; + } + private float snapTimer; - private const float SnapAnimDuration = 1.0f; + + [Serialize(1.0f, IsPropertySaveable.No, description: "")] + public float SnapAnimDuration + { + get; + set; + } private float raycastTimer; private const float RayCastInterval = 0.2f; @@ -46,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")] + public float MaxAngle + { + get; + set; + } + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { @@ -115,8 +150,8 @@ namespace Barotrauma.Items.Components { System.Diagnostics.Debug.Assert(source != null); System.Diagnostics.Debug.Assert(target != null); - this.source = source; this.target = target; + SetSource(source); Snapped = false; ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; @@ -127,7 +162,7 @@ namespace Barotrauma.Items.Components if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed)) { - source = null; + ResetSource(); target = null; IsActive = false; return; @@ -144,12 +179,27 @@ namespace Barotrauma.Items.Components } Vector2 diff = target.WorldPosition - source.WorldPosition; - if (diff.LengthSquared() > MaxLength * MaxLength) + float lengthSqr = diff.LengthSquared(); + if (lengthSqr > MaxLength * MaxLength) { Snap(); return; } + if (MaxAngle < 180 && lengthSqr > 2500) + { + if (launchDir == null) + { + launchDir = diff; + } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); + if (angle > MaxAngle) + { + Snap(); + return; + } + } + #if CLIENT item.ResetCachedVisibleSize(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index f9c057603..579e1d49c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components string displayNameTag = "", fallbackTag = ""; //if displayname is not present, attempt to find it from the prefab - if (element.Attribute("displayname") == null) + if (element.GetAttribute("displayname") == null) { foreach (var subElement in item.Prefab.ConfigElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index cdeb9bf46..92149904e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components HasPropertyName = !PropertyName.IsEmpty; IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; - if (element.Attribute("signal") is XAttribute attribute) + if (element.GetAttribute("signal") is XAttribute attribute) { Signal = attribute.Value; ShouldSetProperty = HasPropertyName; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index e274d06eb..6cbb632a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -154,7 +154,7 @@ namespace Barotrauma.Items.Components IsActive = true; //backwards compatibility - if (element.Attribute("range") != null) + if (element.GetAttribute("range") != null) { rangeX = rangeY = element.GetAttributeFloat("range", 0.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 8c148b411..b783a1126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -323,7 +323,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!"); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 1e2638509..93c68e6ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1389,7 +1389,10 @@ namespace Barotrauma /// public static void UpdateHulls() { - foreach (Item item in ItemList) item.FindHull(); + foreach (Item item in ItemList) + { + item.FindHull(); + } } public Hull FindHull() @@ -1677,12 +1680,17 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + CreateStatusEvent(); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } + public void CreateStatusEvent() + { + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + } + private bool isActive = true; public override void Update(float deltaTime, Camera cam) @@ -2955,7 +2963,7 @@ namespace Barotrauma /// public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { - string name = element.Attribute("name").Value; + string name = element.GetAttribute("name").Value; Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index a4a939944..e0c65a747 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -63,7 +63,7 @@ namespace Barotrauma } } - private readonly struct StatusEventData : IEventData + private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index eaa18470a..33d5b6289 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -106,12 +106,13 @@ namespace Barotrauma public readonly Identifier TargetItemPrefabIdentifier; public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; - private Lazy displayName; + private readonly Lazy displayName; public LocalizedString DisplayName => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; public readonly ImmutableArray RequiredItems; public readonly ImmutableArray SuitableFabricatorIdentifiers; public readonly float RequiredTime; + public readonly int RequiredMoney; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly ImmutableArray RequiredSkills; @@ -130,6 +131,7 @@ namespace Barotrauma var requiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); + RequiredMoney = element.GetAttributeInt("requiredmoney", 0); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { @@ -322,8 +324,13 @@ namespace Barotrauma private PriceInfo defaultPrice; public PriceInfo DefaultPrice => defaultPrice; - - private ImmutableDictionary locationPrices; + private ImmutableDictionary StorePrices { get; set; } + public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || + (StorePrices != null && StorePrices.Any(p => p.Value.CanBeBought)); + /// + /// Any item with a Price element in the definition can be sold everywhere. + /// + public bool CanBeSold => DefaultPrice != null; /// /// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character @@ -393,13 +400,6 @@ namespace Barotrauma /// public bool? AllowAsExtraCargo { get; private set; } - public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); - - /// - /// Any item with a Price element in the definition can be sold everywhere. - /// - public bool CanBeSold => DefaultPrice != null; - public bool RandomDeconstructionOutput { get; private set; } public int RandomDeconstructionOutputAmount { get; private set; } @@ -675,11 +675,11 @@ namespace Barotrauma var deconstructItems = new List(); var fabricationRecipes = new Dictionary(); var treatmentSuitability = new Dictionary(); - var locationPrices = new Dictionary(); + var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; - if (ConfigElement.Attribute("allowasextracargo") != null) + if (ConfigElement.GetAttribute("allowasextracargo") != null) { AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false); } @@ -690,7 +690,7 @@ namespace Barotrauma this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty()).ToImmutableHashSet(); } - if (ConfigElement.Attribute("cargocontainername") != null) + if (ConfigElement.GetAttribute("cargocontainername") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); } @@ -731,39 +731,46 @@ namespace Barotrauma CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && - subElement.Attribute("sheetindex") == null) + if (subElement.GetAttribute("sourcerect") == null && + subElement.GetAttribute("sheetindex") == null) { DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); } Size = Sprite.size; - if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) + if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace()) { Sprite.Name = Name.Value; } Sprite.EntityIdentifier = Identifier; break; case "price": - if (subElement.Attribute("baseprice") != null) + if (subElement.GetAttribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out this.defaultPrice)) + foreach (var priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice)) { - if (priceInfo == null) { continue; } - locationPrices.Add(priceInfo.Item1, priceInfo.Item2); + if (priceInfo.StoreIdentifier.IsEmpty) { continue; } + if (storePrices.ContainsKey(priceInfo.StoreIdentifier)) + { + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once."); + storePrices[priceInfo.StoreIdentifier] = priceInfo; + } + else + { + storePrices.Add(priceInfo.StoreIdentifier, priceInfo); + } } } - else if (subElement.Attribute("buyprice") != null) + else if (subElement.GetAttribute("buyprice") != null && subElement.GetAttributeIdentifier("locationtype", "") is { IsEmpty: false } locationType) // Backwards compatibility { - Identifier locationType = subElement.GetAttributeIdentifier("locationtype", ""); - if (locationPrices.ContainsKey(locationType)) + if (storePrices.ContainsKey(locationType)) { - DebugConsole.AddWarning($"Error in item prefab \"{ToString()}\": price for the location type \"{locationType}\" defined more than once."); - locationPrices[locationType] = new PriceInfo(subElement); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once."); + storePrices[locationType] = new PriceInfo(subElement); } else { - locationPrices.Add(locationType, new PriceInfo(subElement)); + storePrices.Add(locationType, new PriceInfo(subElement)); } } break; @@ -851,7 +858,7 @@ namespace Barotrauma } break; case "suitabletreatment": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); } @@ -870,15 +877,15 @@ namespace Barotrauma this.DeconstructItems = deconstructItems.ToImmutableArray(); this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary(); this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary(); - this.locationPrices = locationPrices.ToImmutableDictionary(); + StorePrices = storePrices.ToImmutableDictionary(); this.PreferredContainers = preferredContainers.ToImmutableArray(); this.LevelCommonness = levelCommonness.ToImmutableDictionary(); this.LevelQuantity = levelQuantity.ToImmutableDictionary(); // Backwards compatibility - if (locationPrices != null && locationPrices.Any()) + if (storePrices.Any()) { - this.defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar); @@ -930,31 +937,109 @@ namespace Barotrauma return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } - public PriceInfo GetPriceInfo(Location location) + #region Pricing + + public PriceInfo GetPriceInfo(Location.StoreInfo store) { - if (location?.Type == null) { return null; } - var locationTypeId = location.Type.Identifier; - if (locationPrices != null && locationPrices.ContainsKey(locationTypeId)) + if (!store.Identifier.IsEmpty && StorePrices != null && StorePrices.TryGetValue(store.Identifier, out var storePriceInfo)) { - return locationPrices[locationTypeId]; + return storePriceInfo; } else { return DefaultPrice; } } - - public bool CanBeBoughtAtLocation(Location location, out PriceInfo priceInfo) + + public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { - priceInfo = null; - if (location?.Type == null) { return false; } - priceInfo = GetPriceInfo(location); - return - priceInfo != null && - priceInfo.CanBeBought && - (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + priceInfo = GetPriceInfo(store); + return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } + public bool CanBeBoughtFrom(Location location) + { + if (location?.Stores == null) { return false; } + foreach (var store in location.Stores) + { + var priceInfo = GetPriceInfo(store.Value); + if (priceInfo == null) { continue; } + if (!priceInfo.CanBeBought) { continue; } + if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } + return true; + } + return false; + } + + public int? GetMinPrice() + { + int? minPrice = StorePrices.Values.Min(p => p.Price); + if (minPrice.HasValue) + { + if (DefaultPrice != null) + { + return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; + } + else + { + return minPrice.Value; + } + } + else + { + return DefaultPrice?.Price; + } + } + + public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) + { + var prices = new Dictionary(); + if (StorePrices != null) + { + foreach (var storePrice in StorePrices) + { + var priceInfo = storePrice.Value; + if (priceInfo == null) + { + continue; + } + if (!priceInfo.CanBeBought) + { + continue; + } + if (priceInfo.Price < maxCost || maxCost == 0) + { + prices.Add(storePrice.Key, priceInfo); + } + } + } + return prices.ToImmutableDictionary(); + } + + public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + { + var prices = new Dictionary(); + if (!CanBeSold && sellingImportant) + { + return prices.ToImmutableDictionary(); + } + foreach (var storePrice in StorePrices) + { + var priceInfo = storePrice.Value; + if (priceInfo == null) + { + continue; + } + if (priceInfo.Price > minCost) + { + prices.Add(storePrice.Key, priceInfo); + } + } + return prices.ToImmutableDictionary(); + } + + #endregion + public static ItemPrefab Find(string name, Identifier identifier) { if (string.IsNullOrEmpty(name) && identifier.IsEmpty) @@ -988,77 +1073,6 @@ namespace Barotrauma return prefab; } - public int? GetMinPrice() - { - int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; - if (minPrice.HasValue) - { - if (DefaultPrice != null) - { - return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; - } - else - { - return minPrice.Value; - } - } - else - { - return DefaultPrice?.Price; - } - } - - public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) - { - Dictionary priceLocations = new Dictionary(); - if (locationPrices != null) - { - foreach (KeyValuePair locationPrice in locationPrices) - { - PriceInfo priceInfo = locationPrice.Value; - - if (priceInfo == null) - { - continue; - } - if (!priceInfo.CanBeBought) - { - continue; - } - if (priceInfo.Price < maxCost || maxCost == 0) - { - priceLocations.Add(locationPrice.Key, priceInfo); - } - } - } - return priceLocations.ToImmutableDictionary(); - } - - public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) - { - Dictionary priceLocations = new Dictionary(); - - if (!CanBeSold && sellingImportant) - { - return priceLocations.ToImmutableDictionary(); - } - - foreach (KeyValuePair locationPrice in locationPrices) - { - PriceInfo priceInfo = locationPrice.Value; - - if (priceInfo == null) - { - continue; - } - if (priceInfo.Price > minCost) - { - priceLocations.Add(locationPrice.Key, priceInfo); - } - } - return priceLocations.ToImmutableDictionary(); - } - public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false) { isPreferencesDefined = PreferredContainers.Any(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 136178582..46ae8392c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -167,7 +167,7 @@ namespace Barotrauma public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) { Identifier[] identifiers; - if (element.Attribute("name") != null) + if (element.GetAttribute("name") != null) { //backwards compatibility + a console warning DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 9708fd2e4..2be6ddb93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -520,8 +520,9 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; if (branchHealAmount <= 0.0f) { continue; } + float prevHealth = branch.Health; branch.Health += branchHealAmount; - branch.AccumulatedDamage -= branchHealAmount; + branch.AccumulatedDamage += (prevHealth - branch.Health); } } StateMachine.Update(deltaTime); @@ -633,7 +634,8 @@ namespace Barotrauma.MapCreatures.Behavior { if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) { - DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); + float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth); + DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); } if (branch.Health <= 0.0f) { @@ -836,7 +838,7 @@ namespace Barotrauma.MapCreatures.Behavior } #if SERVER - SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); + CreateNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -878,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); + CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -955,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior /// private void CreateBody(BallastFloraBranch branch) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; @@ -975,6 +975,14 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { float damage = amount; + if (damage > 0) + { + damage = Math.Min(damage, branch.Health); + } + else + { + damage = Math.Max(damage, branch.Health - branch.MaxHealth); + } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -983,8 +991,28 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } - // damage is handled server side currently - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + Anger += damage * 0.001f; + } + + if (GameMain.NetworkMember != null) + { + // damage is handled server side + if (GameMain.NetworkMember.IsClient) + { + return; + } + else + { + //accumulate damage on the server's side to ensure clients get notified + if (type == AttackType.Other || type == AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + } + } + } if (attacker != null && toxinsCooldown <= 0) { @@ -1014,11 +1042,6 @@ namespace Barotrauma.MapCreatures.Behavior } branch.Health -= damage; - if (type != AttackType.Other && type != AttackType.CutFromRoot) - { - branch.AccumulatedDamage += damage; - Anger += damage * 0.001f; - } #if SERVER GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); @@ -1110,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - SendNetworkMessage(new BranchRemoveEventData(branch)); + CreateNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1141,7 +1164,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1159,7 +1182,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } @@ -1181,7 +1204,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index a26a12e44..89ee34a92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -88,7 +88,7 @@ namespace Barotrauma flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); - if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } + if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 698152a24..d0c9e6d0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -739,7 +739,7 @@ namespace Barotrauma { Rectangle rect = Rectangle.Empty; - if (element.Attribute("rect") != null) + if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); } @@ -747,15 +747,15 @@ namespace Barotrauma { //backwards compatibility rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), - int.Parse(element.Attribute("width").Value), - int.Parse(element.Attribute("height").Value)); + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), + int.Parse(element.GetAttribute("width").Value), + int.Parse(element.GetAttribute("height").Value)); } bool isHorizontal = rect.Height > rect.Width; - var horizontalAttribute = element.Attribute("horizontal"); + var horizontalAttribute = element.GetAttribute("horizontal"); if (horizontalAttribute != null) { isHorizontal = horizontalAttribute.Value.ToString() == "true"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index c83684db8..e531500bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1564,7 +1564,7 @@ namespace Barotrauma public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; - if (element.Attribute("rect") != null) + if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); } @@ -1572,10 +1572,10 @@ namespace Barotrauma { //backwards compatibility rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), - int.Parse(element.Attribute("width").Value), - int.Parse(element.Attribute("height").Value)); + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), + int.Parse(element.GetAttribute("width").Value), + int.Parse(element.GetAttribute("height").Value)); } var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element)) @@ -1639,7 +1639,7 @@ namespace Barotrauma } SerializableProperty.DeserializeProperties(hull, element); - if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; } + if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; } return hull; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 13d72bc6a..05539c6e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -160,17 +160,20 @@ namespace Barotrauma public void Delete() { Dispose(); - if (File.Exists(ContentFile.Path)) + try { - try + if (ContentPackage is { Files: { Length: 1 } } + && ContentPackageManager.LocalPackages.Contains(ContentPackage)) { - File.Delete(ContentFile.Path); - } - catch (Exception e) - { - DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + Directory.Delete(ContentPackage.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); } } + catch (Exception e) + { + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + } } public override void Dispose() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 42b982c38..f184f08f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3826,12 +3826,12 @@ namespace Barotrauma if (location != null) { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } else { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } foreach (string categoryToHide in locationType.HideEntitySubcategories) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4fd6d7166..8356a9461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -31,8 +31,20 @@ namespace Barotrauma public bool HasHuntingGrounds, OriginallyHadHuntingGrounds; + /// + /// Minimum difficulty of the level before hunting grounds can appear. + /// + public const float HuntingGroundsDifficultyThreshold = 25; + + /// + /// Probability of hunting grounds appearing in 100% difficulty levels. + /// + public const float MaxHuntingGroundsProbability = 0.3f; + public OutpostGenerationParams ForceOutpostGenerationParams; + public bool AllowInvalidOutpost; + public readonly Point Size; public readonly int InitialDepth; @@ -150,11 +162,7 @@ namespace Barotrauma } else { - //minimum difficulty of the level before hunting grounds can appear - float huntingGroundsDifficultyThreshold = 25; - //probability of hunting grounds appearing in 100% difficulty levels - float maxHuntingGroundsProbability = 0.3f; - HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability; HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); } IsBeaconActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index ec1d9fbe4..02140f9a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -235,7 +235,7 @@ namespace Barotrauma UseNetworkSyncing = element.GetAttributeBool("networksyncing", false); unrotatedForce = - element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ? + element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ? element.GetAttributeVector2("force", Vector2.Zero) : new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index b70413c68..4b77d08fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -89,39 +89,272 @@ namespace Barotrauma public int TurnsInRadiation { get; set; } #region Store + + public class StoreInfo + { + private int balance; + + public Identifier Identifier { get; } + public int Balance + { + get + { + return balance; + } + set + { + balance = value; + ActiveBalanceStatus = Location.GetStoreBalanceStatus(value); + } + } + public List Stock { get; } = new List(); + public List DailySpecials { get; } = new List(); + public List RequestedGoods { get; } = new List(); + /// + /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. + /// + public int PriceModifier { get; set; } + public StoreBalanceStatus ActiveBalanceStatus { get; private set; } + public Color BalanceColor => ActiveBalanceStatus.Color; + public Location Location { get; } + + private StoreInfo(Location location) + { + Location = location; + } + + /// + /// Create new StoreInfo + /// + public StoreInfo(Location location, Identifier identifier) : this(location) + { + Identifier = identifier; + Balance = location.StoreInitialBalance; + Stock = CreateStock(); + GenerateSpecials(); + GeneratePriceModifier(); + } + + /// + /// Load previously saved StoreInfo + /// + public StoreInfo(Location location, XElement storeElement) : this(location) + { + Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); + PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); + // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute + if (storeElement.Attribute("stepssincespecialsupdated") != null) + { + location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); + } + foreach (var stockElement in storeElement.GetChildElements("stock")) + { + var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty); + if (identifier.IsEmpty || !(ItemPrefab.FindByIdentifier(identifier) is ItemPrefab prefab)) { continue; } + int qty = stockElement.GetAttributeInt("qty", 0); + if (qty < 1) { continue; } + Stock.Add(new PurchasedItem(prefab, qty, buyer: null)); + } + if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement) + { + var loadedDailySpecials = LoadStoreSpecials(specialsElement); + DailySpecials.AddRange(loadedDailySpecials); + } + if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement) + { + var loadedRequestedGoods = LoadStoreSpecials(goodsElement); + RequestedGoods.AddRange(loadedRequestedGoods); + } + + static List LoadStoreSpecials(XElement element) + { + var specials = new List(); + foreach (var childElement in element.GetChildElements("item")) + { + var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); + if (id.IsEmpty || !(ItemPrefab.FindByIdentifier(id) is ItemPrefab prefab)) { continue; } + specials.Add(prefab); + } + return specials; + } + } + + public List CreateStock() + { + var stock = new List(); + foreach (var prefab in ItemPrefab.Prefabs) + { + if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; } + int quantity = PriceInfo.DefaultAmount; + if (priceInfo.MaxAvailableAmount > 0) + { + if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) + { + quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); + } + else + { + quantity = priceInfo.MaxAvailableAmount; + } + } + else if (priceInfo.MinAvailableAmount > 0) + { + quantity = priceInfo.MinAvailableAmount; + } + stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); + } + return stock; + } + + public void AddStock(List items) + { + if (items == null || items.None()) { return; } + DebugConsole.NewMessage($"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true); + foreach (var item in items) + { + if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) + { + stockItem.Quantity += 1; + DebugConsole.NewMessage($"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true); + } + else + { + DebugConsole.NewMessage($"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly: true); + } + } + } + + public void RemoveStock(List items) + { + if (items == null || items.None()) { return; } + DebugConsole.NewMessage($"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true); + foreach (PurchasedItem item in items) + { + if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) + { + stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0); + DebugConsole.NewMessage($"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true); + } + } + } + + public void GenerateSpecials() + { + var availableStock = new Dictionary(); + foreach (var stockItem in Stock) + { + if (stockItem.Quantity < 1) { continue; } + float weight = 1.0f; + if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo) + { + if (!priceInfo.CanBeSpecial) { continue; } + var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; + weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; + if (weight < 0.0f) { continue; } + } + availableStock.Add(stockItem.ItemPrefab, weight); + } + DailySpecials.Clear(); + int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); + for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) + { + if (availableStock.None()) { break; } + var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); + if (item == null) { break; } + DailySpecials.Add(item); + availableStock.Remove(item); + } + RequestedGoods.Clear(); + for (int i = 0; i < RequestedGoodsCount; i++) + { + var item = ItemPrefab.Prefabs.GetRandom(p => + p.CanBeSold && !RequestedGoods.Contains(p) && + p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); + if (item == null) { break; } + RequestedGoods.Add(item); + } + Location.StepsSinceSpecialsUpdated = 0; + } + + public void GeneratePriceModifier() + { + PriceModifier = Rand.Range(-Location.StorePriceModifierRange, Location.StorePriceModifierRange + 1); + } + + /// If null, item.GetPriceInfo() will be used to get it. + /// /// If false, the price won't be affected by + public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true) + { + priceInfo ??= item?.GetPriceInfo(this); + if (priceInfo == null) { return 0; } + float price = priceInfo.Price; + // Adjust by random price modifier + price = (100 + PriceModifier) / 100.0f * price; + price *= priceInfo.BuyingPriceMultiplier; + // Adjust by daily special status + if (considerDailySpecials && DailySpecials.Contains(item)) + { + price = Location.DailySpecialPriceModifier * price; + } + // Adjust by current location reputation + if (Location.Reputation.Value > 0.0f) + { + price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; + } + else + { + price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; + } + // Price should never go below 1 mk + return Math.Max((int)price, 1); + } + + /// If null, item.GetPriceInfo() will be used to get it. + /// If false, the price won't be affected by + public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true) + { + priceInfo ??= item?.GetPriceInfo(this); + if (priceInfo == null) { return 0; } + float price = Location.StoreSellPriceModifier * priceInfo.Price; + // Adjust by random price modifier + price = (100 - PriceModifier) / 100.0f * price; + // Adjust by current store balance + price = ActiveBalanceStatus.SellPriceModifier * price; + // Adjust by requested good status + if (considerRequestedGoods && RequestedGoods.Contains(item)) + { + price = Location.RequestGoodPriceModifier * price; + } + // Adjust by current location reputation + if (Location.Reputation.Value > 0.0f) + { + price = MathHelper.Lerp(1.0f, 1.0f + Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MaxReputation) * price; + } + else + { + price = MathHelper.Lerp(1.0f, 1.0f - Location.StoreMaxReputationModifier, Location.Reputation.Value / Location.Reputation.MinReputation) * price; + } + // Price should never go below 1 mk + return Math.Max((int)price, 1); + } + + public override string ToString() + { + return Identifier.Value; + } + } + + public Dictionary Stores { get; set; } + private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier; private float StoreSellPriceModifier => Type.StoreSellPriceModifier; private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier; private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; public int StoreInitialBalance => Type.StoreInitialBalance; private int StorePriceModifierRange => Type.StorePriceModifierRange; - /// - /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. - /// - public int StorePriceModifier { get; private set; } - - public Color BalanceColor => ActiveStoreBalanceStatus.Color; - public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; } private List StoreBalanceStatuses => Type.StoreBalanceStatuses; - private int storeCurrentBalance; - public int StoreCurrentBalance - { - get - { - return storeCurrentBalance; - } - set - { - storeCurrentBalance = value; - ActiveStoreBalanceStatus = GetStoreBalanceStatus(value); - } - } - - public List StoreStock { get; set; } - public List DailySpecials { get; } = new List(); - public List RequestedGoods { get; } = new List(); - /// /// How many map progress steps it takes before the discounts should be updated. /// @@ -129,6 +362,7 @@ namespace Barotrauma private const int DailySpecialsCount = 3; private const int RequestedGoodsCount = 3; private int StepsSinceSpecialsUpdated { get; set; } + public HashSet StoreIdentifiers { get; } = new HashSet(); #endregion @@ -261,33 +495,18 @@ namespace Barotrauma Connections = new List(); } + /// + /// Create a location from save data + /// public Location(XElement element) { - Identifier locationType = element.GetAttributeIdentifier("type", ""); - Type = LocationType.Prefabs[locationType]; - bool typeNotFound = false; - if (Type == null) - { - //turn lairs into abandoned outposts - if (locationType == "lair") - { - Type ??= LocationType.Prefabs["Abandoned"]; - addInitialMissionsForType = Type; - } - if (Type == null) - { - DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); - Type ??= LocationType.Prefabs["None"] ?? LocationType.Prefabs.First(); - } - if (Type != null) - { - element.SetAttributeValue("type", Type.Identifier); - } - typeNotFound = true; - } + Identifier locationTypeId = element.GetAttributeIdentifier("type", ""); + bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type); + Type = type; - Identifier originalLocationType = element.GetAttributeIdentifier("originaltype", locationType); - OriginalType = LocationType.Prefabs[locationType]; + Identifier originalLocationTypeId = element.GetAttributeIdentifier("originaltype", locationTypeId); + GetTypeOrFallback(originalLocationTypeId, out LocationType originalType); + OriginalType = originalType; baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); @@ -297,6 +516,7 @@ namespace Barotrauma IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); if (!typeNotFound) { @@ -340,7 +560,7 @@ namespace Barotrauma killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); - System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!"); + System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); if (Type == null) { Type = LocationType.Prefabs.First(); @@ -350,8 +570,36 @@ namespace Barotrauma PortraitId = ToolBox.StringToInt(Name); - LoadStore(element); + LoadStores(element); LoadMissions(element); + + bool GetTypeOrFallback(Identifier identifier, out LocationType type) + { + if (!LocationType.Prefabs.TryGet(identifier, out type)) + { + //turn lairs into abandoned outposts + if (identifier == "lair") + { + LocationType.Prefabs.TryGet("Abandoned".ToIdentifier(), out type); + addInitialMissionsForType = Type; + } + if (type == null) + { + DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead."); + LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); + if (type == null) + { + type = LocationType.Prefabs.First(); + } + } + if (type != null) + { + element.SetAttributeValue("type", type.Identifier); + } + return false; + } + return true; + } } public void LoadLocationTypeChange(XElement locationElement) @@ -408,7 +656,6 @@ namespace Barotrauma } } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable existingLocations = null) { return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); @@ -427,7 +674,7 @@ namespace Barotrauma DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; - Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); if (Type.MissionIdentifiers.Any()) { @@ -438,7 +685,7 @@ namespace Barotrauma UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); } - CreateStore(force: true); + CreateStores(force: true); } public void UnlockInitialMissions() @@ -714,90 +961,50 @@ namespace Barotrauma return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } - public void LoadStore(XElement locationElement) + public void LoadStores(XElement locationElement) { - StoreStock?.Clear(); - DailySpecials.Clear(); - RequestedGoods.Clear(); - - if (locationElement.GetChildElement("store") is XElement storeElement) + UpdateStoreIdentifiers(); + Stores?.Clear(); + foreach (var storeElement in locationElement.GetChildElements("store")) { - StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance); - StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); - - StoreStock ??= new List(); - foreach (XElement stockElement in storeElement.GetChildElements("stock")) + Stores ??= new Dictionary(); + var identifier = storeElement.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) { - var id = stockElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var qty = stockElement.GetAttributeInt("qty", 0); - if (qty < 1) { continue; } - StoreStock.Add(new PurchasedItem(prefab, qty, buyer: null)); + // Previously saved store data (with no identifier) is discarded and new store data will be created + continue; } - - StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); - - if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement) + if (StoreIdentifiers.Contains(identifier)) { - var loadedDailySpecials = LoadStoreSpecials(specialsElement); - DailySpecials.AddRange(loadedDailySpecials); - } - - if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement) - { - var loadedRequestedGoods = LoadStoreSpecials(goodsElement); - RequestedGoods.AddRange(loadedRequestedGoods); - } - - static List LoadStoreSpecials(XElement element) - { - List specials = new List(); - foreach (var childElement in element.GetChildElements("item")) + if (!Stores.ContainsKey(identifier)) { - var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); - if (id.IsEmpty) { continue; } - var prefab = ItemPrefab.Find(null, id); - if (prefab == null) { continue; } - specials.Add(prefab); + Stores.Add(identifier, new StoreInfo(this, storeElement)); + } + else + { + string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier."; + DebugConsole.ThrowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg); + continue; } - return specials; } + else + { + string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; + DebugConsole.ThrowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg); + continue; + } + } + // Backwards compatibility: create new stores for any identifiers not present in the save data + foreach (var id in StoreIdentifiers) + { + AddNewStore(id); } } public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this); - private List CreateStoreStock() - { - var stock = new List(); - foreach (ItemPrefab prefab in ItemPrefab.Prefabs) - { - if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo)) - { - int quantity = PriceInfo.DefaultAmount; - if (priceInfo.MaxAvailableAmount > 0) - { - if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) - { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); - } - else - { - quantity = priceInfo.MaxAvailableAmount; - } - } - else if (priceInfo.MinAvailableAmount > 0) - { - quantity = priceInfo.MinAvailableAmount; - } - stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); - } - } - return stock; - } - /// /// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost /// @@ -836,73 +1043,6 @@ namespace Barotrauma } } - /// If null, item.GetPriceInfo() will be used to get it. - /// /// If false, the price won't be affected by - public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true) - { - priceInfo ??= item?.GetPriceInfo(this); - if (priceInfo == null) { return 0; } - float price = priceInfo.Price; - - // Adjust by random price modifier - price = ((100 + StorePriceModifier) / 100.0f) * price; - - price *= priceInfo.BuyingPriceMultiplier; - - // Adjust by daily special status - if (considerDailySpecials && DailySpecials.Contains(item)) - { - price = DailySpecialPriceModifier * price; - } - - // Adjust by current location reputation - if (Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; - } - - // Price should never go below 1 mk - return Math.Max((int)price, 1); - } - - /// If null, item.GetPriceInfo() will be used to get it. - /// If false, the price won't be affected by - public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true) - { - priceInfo ??= item?.GetPriceInfo(this); - if (priceInfo == null) { return 0; } - float price = StoreSellPriceModifier * priceInfo.Price; - - // Adjust by random price modifier - price = ((100 - StorePriceModifier) / 100.0f) * price; - - // Adjust by current store balance - price = ActiveStoreBalanceStatus.SellPriceModifier * price; - - // Adjust by requested good status - if (considerRequestedGoods && RequestedGoods.Contains(item)) - { - price = RequestGoodPriceModifier * price; - } - - // Adjust by current location reputation - if (Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; - } - - // Price should never go below 1 mk - return Math.Max((int)price, 1); - } - public int GetAdjustedMechanicalCost(int cost) { float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); @@ -915,187 +1055,182 @@ namespace Barotrauma return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); } - /// If true, the store will be recreated if it already exists. - public void CreateStore(bool force = false) + public StoreInfo GetStore(Identifier identifier) + { + if (Stores != null && Stores.TryGetValue(identifier, out var store)) + { + return store; + } + return null; + } + + /// If true, the stores will be recreated if they already exists. + public void CreateStores(bool force = false) { // In multiplayer, stores should be created by the server and loaded from save data by clients if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - - if (!force && StoreStock != null) { return; } - - if (StoreStock != null) + if (!force && Stores != null) { return; } + UpdateStoreIdentifiers(); + if (Stores != null) { - StoreCurrentBalance = Math.Max(StoreCurrentBalance, StoreInitialBalance); - var newStock = CreateStoreStock(); - foreach (PurchasedItem oldStockItem in StoreStock) + // Remove any stores with no corresponding merchants at the location + foreach (var storeIdentifier in Stores.Keys) { - if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is PurchasedItem newStockItem) + if (!StoreIdentifiers.Contains(storeIdentifier)) { - if (oldStockItem.Quantity > newStockItem.Quantity) - { - newStockItem.Quantity = oldStockItem.Quantity; - } + Stores.Remove(storeIdentifier); } } - StoreStock = newStock; - } - else - { - StoreCurrentBalance = StoreInitialBalance; - StoreStock = CreateStoreStock(); - } - - GenerateRandomPriceModifier(); - CreateStoreSpecials(); - } - - public void UpdateStore() - { - // In multiplayer, stores should be updated by the server and loaded from save data by clients - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - - if (StoreStock == null) - { - CreateStore(); - return; - } - - if (StoreCurrentBalance < StoreInitialBalance) - { - StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); - } - - GenerateRandomPriceModifier(); - - var stock = StoreStock; - var stockToRemove = new List(); - foreach (PurchasedItem item in stock) - { - if (item.ItemPrefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo)) + foreach (var identifier in StoreIdentifiers) { - item.Quantity += 1; - if (priceInfo.MaxAvailableAmount > 0) + if (Stores.TryGetValue(identifier, out var store)) { - item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + store.Balance = Math.Max(store.Balance, StoreInitialBalance); + var newStock = store.CreateStock(); + foreach (var oldStockItem in store.Stock) + { + if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem) + { + if (oldStockItem.Quantity > newStockItem.Quantity) + { + newStockItem.Quantity = oldStockItem.Quantity; + } + } + } + store.Stock.Clear(); + store.Stock.AddRange(newStock); + store.GenerateSpecials(); + store.GeneratePriceModifier(); } else { - item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + AddNewStore(identifier); } } - else - { - stockToRemove.Add(item); - } } - stockToRemove.ForEach(i => stock.Remove(i)); - StoreStock = stock; - - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || - DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) + else { - CreateStoreSpecials(); + foreach (var identifier in StoreIdentifiers) + { + AddNewStore(identifier); + } } } - private int GetExtraSpecialSalesCount() + public void UpdateStores() + { + // In multiplayer, stores should be updated by the server and loaded from save data by clients + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (Stores == null) + { + CreateStores(); + return; + } + var storesToRemove = new HashSet(); + foreach (var store in Stores.Values) + { + if (!StoreIdentifiers.Contains(store.Identifier)) + { + storesToRemove.Add(store.Identifier); + continue; + } + if (store.Balance < StoreInitialBalance) + { + store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); + } + var stock = store.Stock; + var stockToRemove = new List(); + foreach (var item in stock) + { + if (item.ItemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) + { + item.Quantity += 1; + if (priceInfo.MaxAvailableAmount > 0) + { + item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + } + else + { + item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + } + } + else + { + stockToRemove.Add(item); + } + } + stockToRemove.ForEach(i => stock.Remove(i)); + store.Stock.Clear(); + store.Stock.AddRange(stock); + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || store.DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) + { + store.GenerateSpecials(); + } + store.GeneratePriceModifier(); + } + foreach (var identifier in storesToRemove) + { + Stores.Remove(identifier); + } + foreach (var identifier in StoreIdentifiers) + { + AddNewStore(identifier); + } + } + + private void UpdateStoreIdentifiers() + { + StoreIdentifiers.Clear(); + foreach (var outpostParam in OutpostGenerationParams.OutpostParams) + { + if (!outpostParam.AllowedLocationTypes.Contains(Type.Identifier)) { continue; } + foreach (var identifier in outpostParam.GetStoreIdentifiers()) + { + StoreIdentifiers.Add(identifier); + } + } + } + + private void AddNewStore(Identifier identifier) + { + Stores ??= new Dictionary(); + if (Stores.ContainsKey(identifier)) { return; } + var newStore = new StoreInfo(this, identifier); + Stores.Add(identifier, newStore); + } + + public void AddStock(Dictionary> items) + { + if (items == null) { return; } + foreach (var storeItems in items) + { + if (GetStore(storeItems.Key) is { } store) + { + store.AddStock(storeItems.Value); + } + } + } + + public void RemoveStock(Dictionary> items) + { + if (items == null) { return; } + foreach (var storeItems in items) + { + if (GetStore(storeItems.Key) is { } store) + { + store.RemoveStock(storeItems.Value); + } + } + } + + public int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(); if (!characters.Any()) { return 0; } return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - private void GenerateRandomPriceModifier() - { - StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange + 1); - } - - private void CreateStoreSpecials() - { - DailySpecials.Clear(); - var availableStock = new Dictionary(); - foreach (var stockItem in StoreStock) - { - if (stockItem.Quantity < 1) { continue; } - var weight = 1.0f; - var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this); - if (priceInfo != null) - { - if (!priceInfo.CanBeSpecial) { continue; } - var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; - weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; - if (weight < 0.0f) { continue; } - } - availableStock.Add(stockItem.ItemPrefab, weight); - } - - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) - { - if (availableStock.None()) { break; } - var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); - if (item == null) { break; } - DailySpecials.Add(item); - availableStock.Remove(item); - } - - RequestedGoods.Clear(); - for (int i = 0; i < RequestedGoodsCount; i++) - { - var item = ItemPrefab.Prefabs.GetRandom(p => - p.CanBeSold && !RequestedGoods.Contains(p) && - p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); - if (item == null) { break; } - RequestedGoods.Add(item); - } - - StepsSinceSpecialsUpdated = 0; - } - - public void AddToStock(List items) - { - if (StoreStock == null || items == null) { return; } -#if DEBUG - if (items.Any()) { DebugConsole.NewMessage("Adding items to stock at " + Name, Color.Purple); } -#endif - foreach (SoldItem item in items) - { - if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) - { - stockItem.Quantity += 1; -#if DEBUG - DebugConsole.NewMessage("Added 1x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan); -#endif - } -#if DEBUG - else - { - DebugConsole.NewMessage(item.ItemPrefab.Name + " not sold at location, can't add", Color.Cyan); - } -#endif - } - } - - public void RemoveFromStock(List items) - { - if (StoreStock == null || items == null) { return; } -#if DEBUG - if (items.Any()) { DebugConsole.NewMessage("Removing items from stock at " + Name, Color.Purple); } -#endif - foreach (PurchasedItem item in items) - { - if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) - { - stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0); -#if DEBUG - DebugConsole.NewMessage("Removed " + item.Quantity + "x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan); -#endif - } - } - } - public StoreBalanceStatus GetStoreBalanceStatus(int balance) { StoreBalanceStatus nextStatus = StoreBalanceStatuses[0]; @@ -1128,7 +1263,7 @@ namespace Barotrauma ChangeType(OriginalType); PendingLocationTypeChange = null; } - CreateStore(force: true); + CreateStores(force: true); ClearMissions(); LevelData?.EventHistory?.Clear(); UnlockInitialMissions(); @@ -1148,7 +1283,8 @@ namespace Barotrauma new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier), new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), - new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation)); + new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), + new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -1201,44 +1337,43 @@ namespace Barotrauma locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers))); } - if (StoreStock != null) + if (Stores != null) { - var storeElement = new XElement("store", - new XAttribute("balance", StoreCurrentBalance), - new XAttribute("pricemodifier", StorePriceModifier), - new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); - - foreach (PurchasedItem item in StoreStock) + foreach (var store in Stores.Values) { - if (item?.ItemPrefab == null) { continue; } - storeElement.Add(new XElement("stock", - new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity))); - } - - if (DailySpecials.Any()) - { - var dailySpecialElement = new XElement("dailyspecials"); - foreach (var item in DailySpecials) + var storeElement = new XElement("store", + new XAttribute("identifier", store.Identifier.Value), + new XAttribute("balance", store.Balance), + new XAttribute("pricemodifier", store.PriceModifier)); + foreach (PurchasedItem item in store.Stock) { - dailySpecialElement.Add(new XElement("item", - new XAttribute("id", item.Identifier))); + if (item?.ItemPrefab == null) { continue; } + storeElement.Add(new XElement("stock", + new XAttribute("id", item.ItemPrefab.Identifier), + new XAttribute("qty", item.Quantity))); } - storeElement.Add(dailySpecialElement); - } - - if (RequestedGoods.Any()) - { - var requestedGoodsElement = new XElement("requestedgoods"); - foreach (var item in RequestedGoods) + if (store.DailySpecials.Any()) { - requestedGoodsElement.Add(new XElement("item", - new XAttribute("id", item.Identifier))); + var dailySpecialElement = new XElement("dailyspecials"); + foreach (var item in store.DailySpecials) + { + dailySpecialElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(dailySpecialElement); } - storeElement.Add(requestedGoodsElement); + if (store.RequestedGoods.Any()) + { + var requestedGoodsElement = new XElement("requestedgoods"); + foreach (var item in store.RequestedGoods) + { + requestedGoodsElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(requestedGoodsElement); + } + locationElement.Add(storeElement); } - - locationElement.Add(storeElement); } if (AvailableMissions is List missions && missions.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index f7b648463..0a793e304 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -182,8 +182,7 @@ namespace Barotrauma { for (int i = 0; i < Connections.Count; i++) { - float maxHuntingGroundsProbability = 0.3f; - Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability; + Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability; connectionElements[i].SetAttributeValue("hashuntinggrounds", true); } } @@ -232,7 +231,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); CurrentLocation.Discover(true); - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); InitProjectSpecific(); } @@ -680,7 +679,7 @@ namespace Barotrauma CurrentLocation.Discover(); SelectedLocation = null; - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) @@ -719,7 +718,7 @@ namespace Barotrauma } } - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } @@ -857,16 +856,14 @@ namespace Barotrauma if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } - ProgressLocationTypeChanges(location); - - if (location.Discovered) + if (!ProgressLocationTypeChanges(location) && location.Discovered) { - location.UpdateStore(); + location.UpdateStores(); } } } - private void ProgressLocationTypeChanges(Location location) + private bool ProgressLocationTypeChanges(Location location) { location.TimeSinceLastTypeChange++; location.LocationTypeChangeCooldown--; @@ -886,9 +883,8 @@ namespace Barotrauma location.PendingLocationTypeChange.Value.parentMission); if (location.PendingLocationTypeChange.Value.delay <= 0) { - ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); } - return; } } @@ -920,9 +916,9 @@ namespace Barotrauma } else { - ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(location, selectedTypeChange); } - return; + return false; } } @@ -941,6 +937,8 @@ namespace Barotrauma } } } + + return false; } public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) @@ -988,7 +986,7 @@ namespace Barotrauma return distance; } - private void ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(Location location, LocationTypeChange change) { string prevName = location.Name; @@ -996,7 +994,7 @@ namespace Barotrauma if (newType == null) { DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); - return; + return false; } if (newType.OutpostTeam != location.Type.OutpostTeam || @@ -1013,6 +1011,7 @@ namespace Barotrauma location.TimeSinceLastTypeChange = 0; location.LocationTypeChangeCooldown = change.CooldownAfterChange; location.PendingLocationTypeChange = null; + return true; } partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); @@ -1091,7 +1090,7 @@ namespace Barotrauma } } - location.LoadStore(subElement); + location.LoadStores(subElement); location.LoadMissions(subElement); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index ba0c54911..9fdc1e786 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -59,8 +59,19 @@ namespace Barotrauma } public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); - - private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static string RemoveWhitespace(string s) + { + StringBuilder sb = new StringBuilder(s.Length / 2); // Reserve half the size of the original string because + // that's probably close enough to the size of the result + for (int i = 0; i < s.Length; i++) + { + if (char.IsWhiteSpace(s[i])) { continue; } + sb.Append(s[i]); + } + return sb.ToString(); + } + //thanks to Jlobblet for this regex private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -175,7 +186,7 @@ namespace Barotrauma } if (options.HasFlag(StringHashOptions.IgnoreWhitespace)) { - str = removeWhitespaceRegex.Replace(str, ""); + str = RemoveWhitespace(str); } byte[] bytes = Encoding.UTF8.GetBytes(str); return CalculateForBytes(bytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 3ceac779c..6d75e6916 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -156,6 +156,8 @@ namespace Barotrauma public Dictionary SerializableProperties { get; private set; } + private ImmutableHashSet StoreIdentifiers { get; set; } + #warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { @@ -230,6 +232,26 @@ namespace Barotrauma return humanPrefabCollections.GetRandom(randSync); } + public ImmutableHashSet GetStoreIdentifiers() + { + if (StoreIdentifiers == null) + { + var storeIdentifiers = new HashSet(); + foreach (var collection in humanPrefabCollections) + { + foreach (var prefab in collection) + { + if (prefab?.CampaignInteractionType == CampaignMode.InteractionType.Store) + { + storeIdentifiers.Add(prefab.Identifier); + } + } + } + StoreIdentifiers = storeIdentifiers.ToImmutableHashSet(); + } + return StoreIdentifiers; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8a92048f3..336462b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -57,17 +57,17 @@ namespace Barotrauma } } - public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, locationType, location: null, onlyEntrance); + return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost); } - public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, location.Type, location, onlyEntrance); + return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost); } - private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) + private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { var outpostModuleFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -197,12 +197,19 @@ namespace Barotrauma AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => flag != "none")) { - remainingTries--; - if (remainingTries <= 0) + if (!allowInvalidOutpost) { - DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + remainingTries--; + if (remainingTries <= 0) + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + } + continue; + } + else + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed."); } - continue; } var outpostInfo = new SubmarineInfo() @@ -328,7 +335,10 @@ namespace Barotrauma selectedModule.Offset = (selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) - selectedModule.ThisGap.WorldPosition; - selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null) + { + selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + } } entities[selectedModule] = moduleEntities; } @@ -464,6 +474,16 @@ namespace Barotrauma pendingModuleFlags.Remove(initialModuleFlag); pendingModuleFlags.Insert(0, initialModuleFlag); + if (pendingModuleFlags.Count > totalModuleCount) + { + DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules..."); + int removeCount = pendingModuleFlags.Count - totalModuleCount; + for (int i = 0; i < removeCount; i++) + { + pendingModuleFlags.Remove(pendingModuleFlags.Last()); + } + } + return pendingModuleFlags; } @@ -486,46 +506,71 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + for (int i = 0; i < 2; i++) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) + //try placing a module meant for this location type first, and if that fails, try choosing whatever fits + bool allowDifferentLocationType = i > 0; + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); - if (newModule != null) { placedModules.Add(newModule); } - if (pendingModuleFlags.Count == 0) { return true; } + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } + + PlacedModule newModule = null; + //try appending to the current module if possible + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) + { + newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + } + + if (newModule != null) + { + placedModules.Add(newModule); + } + else + { + //couldn't append to current module, try one of the other placed modules + foreach (PlacedModule otherModule in selectedModules) + { + if (otherModule == currentModule) { continue; } + foreach (OutpostModuleInfo.GapPosition otherGapPosition in + GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) + { + newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + if (newModule != null) + { + placedModules.Add(newModule); + break; + } + } + if (newModule != null) { break; } + } + } + if (pendingModuleFlags.Count == 0) { return true; } } } - //couldn't place anything, retry - if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule)) + //couldn't place a module anywhere, we're probably fucked! + if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) { - //try to append to some other module first - foreach (PlacedModule otherModule in selectedModules) - { - if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) - { - return true; - } - } //try to replace the previously placed module with something else that we can append to - var failedModule = currentModule; for (int i = 0; i < 10; i++) { selectedModules.Remove(currentModule); + assertAllPreviousModulesPresent(); //readd the module types that the previous module was supposed to fulfill to the pending module types pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes); if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); } //retry - currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); + currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); + assertAllPreviousModulesPresent(); if (currentModule == null) { break; } if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { + assertAllPreviousModulesPresent(); return true; } } @@ -534,9 +579,14 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); } return placedModules.Count > 0; + + void assertAllPreviousModulesPresent() + { + System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); + } } /// @@ -553,7 +603,8 @@ namespace Barotrauma List availableModules, List pendingModuleFlags, List selectedModules, - LocationType locationType) + LocationType locationType, + bool allowDifferentLocationType) { if (pendingModuleFlags.Count == 0) { return null; } @@ -562,7 +613,7 @@ namespace Barotrauma foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; - nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); + nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); if (nextModule != null) { break; } } @@ -603,6 +654,7 @@ namespace Barotrauma foreach (PlacedModule otherModule in modules2) { if (module == otherModule) { continue; } + if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; } if (ModulesOverlap(module, otherModule)) { module1 = module; @@ -775,22 +827,25 @@ namespace Barotrauma } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { IEnumerable availableModules = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { availableModules = modules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + + availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); + if (prevModule != null) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo)); + availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); } if (availableModules.Count() == 0) { return null; } @@ -800,15 +855,22 @@ namespace Barotrauma availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type - if (!modulesSuitableForLocationType.Any()) + if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) { modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); } if (!modulesSuitableForLocationType.Any()) { - DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); - return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + if (allowDifferentLocationType) + { + DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange); + return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); + } + else + { + return null; + } } else { @@ -1039,16 +1101,20 @@ namespace Barotrauma if (hallwayLength <= 1.0f) { continue; } - var suitableModules = availableModules.Where(m => - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); - if (suitableModules.Count() == 0) + Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(); + var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + + var suitableHallwayModules = hallwayModules.Where(m => + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); + if (suitableHallwayModules.Count() == 0) { - suitableModules = availableModules.Where(m => + suitableHallwayModules = hallwayModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); + + var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1170,7 +1236,7 @@ namespace Barotrauma var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap); if (startWaypoint == null) { - DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); + DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); continue; } var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index c79a10a64..794a9671b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -45,6 +45,9 @@ namespace Barotrauma [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } + [Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")] + public GapPosition CanAttachToPrevious { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 0037d0a9f..7e9901866 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -6,30 +6,31 @@ namespace Barotrauma { class PriceInfo { - public readonly int Price; - public readonly bool CanBeBought; + public int Price { get; } + public bool CanBeBought { get; } //minimum number of items available at a given store - public readonly int MinAvailableAmount; + public int MinAvailableAmount { get; } //maximum number of items available at a given store - public readonly int MaxAvailableAmount; + public int MaxAvailableAmount { get; } + /// + /// Can the item be a Daily Special or a Requested Good + /// + public bool CanBeSpecial { get; } + /// + /// The item isn't available in stores unless the level's difficulty is above this value + /// + public int MinLevelDifficulty { get; } + /// + /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. + /// + public float BuyingPriceMultiplier { get; } = 1f; + public bool DisplayNonEmpty { get; } = false; + public Identifier StoreIdentifier { get; } + /// /// Used when both and are set to 0. /// public const int DefaultAmount = 5; - /// - /// Can the item be a Daily Special or a Requested Good - /// - public readonly bool CanBeSpecial; - /// - /// The item isn't available in stores unless the level's difficulty is above this value - /// - public readonly int MinLevelDifficulty; - /// - /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. - /// - public readonly float BuyingPriceMultiplier = 1f; - public bool DisplayNonEmpty { get; } = false; - /// /// Support for the old style of determining item prices @@ -42,14 +43,16 @@ namespace Barotrauma MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); CanBeBought = true; - var minAmount = GetMinAmount(element); + int minAmount = GetMinAmount(element); MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); - var maxAmount = GetMaxAmount(element); + int maxAmount = GetMaxAmount(element); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false) + public PriceInfo(int price, bool canBeBought, + int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, + bool displayNonEmpty = false, string storeIdentifier = null) { Price = price; CanBeBought = canBeBought; @@ -60,48 +63,67 @@ namespace Barotrauma MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; + StoreIdentifier = new Identifier(storeIdentifier); } - public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) + public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { + var priceInfos = new List(); defaultPrice = null; int basePrice = element.GetAttributeInt("baseprice", 0); - bool soldByDefault = element.GetAttributeBool("soldbydefault", true); int minAmount = GetMinAmount(element); int maxAmount = GetMaxAmount(element); int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); bool canBeSpecial = element.GetAttributeBool("canbespecial", true); float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); - var priceInfos = new List>(); - + bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true)); foreach (XElement childElement in element.GetChildElements("price")) { float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); - bool sold = childElement.GetAttributeBool("sold", soldByDefault); - priceInfos.Add(new Tuple(childElement.GetAttributeIdentifier("locationtype", ""), - new PriceInfo((int)(priceMultiplier * basePrice), sold, + bool sold = childElement.GetAttributeBool("sold", soldByDefault); + int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty); + float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier); + string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", ""); + if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier)) + { + backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}"; + } + string[] storeIdentifiers = childElement.GetAttributeStringArray("storeidentifiers", new string[1] { backwardsCompatibleIdentifier }); + foreach (string id in storeIdentifiers) + { + if (string.IsNullOrEmpty(id)) { continue; } + // TODO: Add some error messages if we have defined the min or max amount while the item is not sold + var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), + sold, sold ? GetMinAmount(childElement, minAmount) : 0, sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), - childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier), - displayNonEmpty))); + storeMinLevelDifficulty, + storeBuyingMultiplier, + displayNonEmpty, + id); + priceInfos.Add(priceInfo); + } } - - bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); - defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, - canBeBoughtAtOtherLocations ? minAmount : 0, - canBeBoughtAtOtherLocations ? maxAmount : 0, - canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty); - + bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); + defaultPrice = new PriceInfo(basePrice, + soldElsewhere, + soldElsewhere ? minAmount : 0, + soldElsewhere ? maxAmount : 0, + canBeSpecial, + minLevelDifficulty, + buyingPriceMultiplier, + displayNonEmpty); return priceInfos; } private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ? - element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue; + element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : + defaultValue; private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ? - element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue; + element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : + defaultValue; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index ae5e402d5..1fb8afbae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -48,11 +48,11 @@ namespace Barotrauma { if (!subs.Any()) yield return CoroutineStatus.Success; - Character.Controlled = null; - cam.TargetPos = Vector2.Zero; #if CLIENT + Character.Controlled = null; GameMain.LightManager.LosEnabled = false; #endif + cam.TargetPos = Vector2.Zero; Level.Loaded.TopBarrier.Enabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 7556b281a..d3901b682 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -1374,7 +1374,7 @@ namespace Barotrauma public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { - string name = element.Attribute("name").Value; + string name = element.GetAttribute("name").Value; Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = FindPrefab(name, identifier); @@ -1440,12 +1440,12 @@ namespace Barotrauma if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); } //structures with a body drop a shadow by default - if (element.Attribute("usedropshadow") == null) + if (element.GetAttribute("usedropshadow") == null) { s.UseDropShadow = prefab.Body; } - if (element.Attribute("noaitarget") == null) + if (element.GetAttribute("noaitarget") == null) { s.NoAITarget = prefab.NoAITarget; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 84bf74588..26c2f92e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -162,7 +162,7 @@ namespace Barotrauma tags.Add(tag.Trim().ToIdentifier()); } - if (element.Attribute("ishorizontal") != null) + if (element.GetAttribute("ishorizontal") != null) { IsHorizontal = element.GetAttributeBool("ishorizontal", false); } @@ -177,8 +177,8 @@ namespace Barotrauma { case "sprite": Sprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && - subElement.Attribute("sheetindex") == null) + if (subElement.GetAttribute("sourcerect") == null && + subElement.GetAttribute("sheetindex") == null) { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); } @@ -191,7 +191,7 @@ namespace Barotrauma CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) + if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace()) { Sprite.Name = Name.Value; } @@ -199,7 +199,7 @@ namespace Barotrauma break; case "backgroundsprite": BackgroundSprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && Sprite != null) + if (subElement.GetAttribute("sourcerect") == null && Sprite != null) { BackgroundSprite.SourceRect = Sprite.SourceRect; BackgroundSprite.size = Sprite.size; @@ -223,7 +223,7 @@ namespace Barotrauma int groupID = 0; DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { groupID = subElement.GetAttributeInt("randomgroupid", 0); } @@ -284,10 +284,10 @@ namespace Barotrauma } //backwards compatibility - if (element.Attribute("size") == null) + if (element.GetAttribute("size") == null) { Size = Vector2.Zero; - if (element.Attribute("width") == null && element.Attribute("height") == null) + if (element.GetAttribute("width") == null && element.GetAttribute("height") == null) { Size = Sprite.SourceRect.Size.ToVector2(); } @@ -322,7 +322,7 @@ namespace Barotrauma #endif Tags = tags.ToImmutableHashSet(); - AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); + AllowedLinks = ImmutableHashSet.Empty; } protected override void CreateInstance(Rectangle rect) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index b205c4af5..ec08d2c38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -253,7 +253,7 @@ namespace Barotrauma get { return subBody == null ? Vector2.Zero : subBody.Velocity; } set { - if (subBody == null) return; + if (subBody == null) { return; } subBody.Velocity = value; } } @@ -969,8 +969,6 @@ namespace Barotrauma public void Update(float deltaTime) { - //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Info.IsWreck) { WreckAI?.Update(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index cc6881f34..8dde595c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -266,7 +266,7 @@ namespace Barotrauma GameVersion = original.GameVersion; Type = original.Type; SubmarineClass = original.SubmarineClass; - hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null; + hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null; Dimensions = original.Dimensions; CargoCapacity = original.CargoCapacity; FilePath = original.FilePath; @@ -299,7 +299,7 @@ namespace Barotrauma DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); } - if (doc == null || doc.Root == null) + if (doc?.Root == null) { IsFileCorrupted = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 6b22e9acf..11b32da05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -987,8 +987,8 @@ namespace Barotrauma public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); @@ -1024,9 +1024,9 @@ namespace Barotrauma w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0)); int i = 0; - while (element.Attribute("linkedto" + i) != null) + while (element.GetAttribute("linkedto" + i) != null) { - int srcId = int.Parse(element.Attribute("linkedto" + i).Value); + int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value); int destId = idRemap.GetOffsetId(srcId); if (destId > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 90f123c95..e405ecb4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic - MONEY, //wallet updates + TRANSFER_MONEY, // wallet transfers REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 00fe33585..f844f406a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using Barotrauma.IO; @@ -975,18 +976,33 @@ namespace Barotrauma.Networking private void InitMonstersEnabled() { //monster spawn settings - MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + if (MonsterEnabled is null || MonsterEnabled.Count != CharacterPrefab.Prefabs.Count()) + { + MonsterEnabled = CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + } } + private static IReadOnlyList ExtractAndSortKeys(IReadOnlyDictionary monsterEnabled) + => monsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToImmutableArray(); + public void ReadMonsterEnabled(IReadMessage inc) { InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys - .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) - .ToList(); - foreach (Identifier s in monsterNames) + var monsterNames = ExtractAndSortKeys(MonsterEnabled); + uint receivedMonsterCount = inc.ReadVariableUInt32(); + if (monsterNames.Count != receivedMonsterCount) { - MonsterEnabled[s] = inc.ReadBoolean(); + inc.BitPosition += (int)receivedMonsterCount; + DebugConsole.AddWarning($"Expected monster count {monsterNames.Count}, got {receivedMonsterCount}"); + } + else + { + foreach (Identifier s in monsterNames) + { + MonsterEnabled[s] = inc.ReadBoolean(); + } } inc.ReadPadBits(); } @@ -994,11 +1010,10 @@ namespace Barotrauma.Networking public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) { //monster spawn settings - if (monsterEnabled == null) { monsterEnabled = MonsterEnabled; } - - List monsterNames = monsterEnabled.Keys - .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) - .ToList(); + InitMonstersEnabled(); + monsterEnabled ??= MonsterEnabled; + var monsterNames = ExtractAndSortKeys(monsterEnabled); + msg.WriteVariableUInt32((uint)monsterNames.Count); foreach (Identifier s in monsterNames) { msg.Write(monsterEnabled[s]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index dacc2c8a7..89c8bc1bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -172,11 +172,11 @@ namespace Barotrauma return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); } - public static ContentPath GetAttributeContentPath(this XElement element, string name, - ContentPackage contentPackage) + public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage) { - if (element?.GetAttribute(name) == null) { return null; } - return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null)); } public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index b917b844a..551b48fdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -10,6 +10,7 @@ using System.Xml.Linq; using Barotrauma.IO; #if CLIENT using Barotrauma.ClientSource.Settings; +using Barotrauma.Networking; using Microsoft.Xna.Framework.Input; #endif @@ -453,8 +454,12 @@ namespace Barotrauma bool languageChanged = currentConfig.Language != newConfig.Language; + bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice; + bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice; + + bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7); + currentConfig = newConfig; -#warning TODO: Implement program state updates; #if CLIENT if (setGraphicsMode) @@ -462,6 +467,24 @@ namespace Barotrauma GameMain.Instance.ApplyGraphicsSettings(); } + if (audioOutputChanged) + { + GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice); + } + + if (voiceCaptureChanged) + { + VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); + } + + if (textScaleChanged) + { + foreach (var font in GUIStyle.Fonts.Values) + { + font.Prefabs.ForEach(p => p.LoadFont()); + } + } + GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index bd86e6618..f7445abed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -72,6 +72,8 @@ namespace Barotrauma case "targetitemcomponent": case "targetself": case "targetcontainer": + case "targetgrandparent": + case "targetcontaineditem": return false; default: return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 688ebbfbb..b630c8b11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -68,15 +68,21 @@ namespace Barotrauma public bool IsTriggered { get; private set; } - public float Timer { get; private set; } = -1; + public float Timer { get; private set; } public bool IsActive { get; private set; } + public bool IsPermanent { get; private set; } + public void Launch() { IsTriggered = true; IsActive = true; - Timer = Duration; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } } public void Reset() @@ -88,6 +94,7 @@ namespace Barotrauma public void UpdateTimer(float deltaTime) { + if (IsPermanent) { return; } Timer -= deltaTime; if (Timer < 0) { @@ -410,7 +417,7 @@ namespace Barotrauma public static StatusEffect Load(ContentXElement element, string parentDebugName) { - if (element.Attribute("delay") != null || element.Attribute("delaytype") != null) + if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null) { return new DelayedEffect(element, parentDebugName); } @@ -642,7 +649,7 @@ namespace Barotrauma break; case "affliction": AfflictionPrefab afflictionPrefab; - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", ""); @@ -670,7 +677,7 @@ namespace Barotrauma break; case "reduceaffliction": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); ReduceAffliction.Add(( @@ -1243,24 +1250,27 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) + var target = targets[i]; + Limb targetLimb = target as Limb; + if (targetLimb == null && target is Character character) { foreach (Limb limb in character.AnimController.Limbs) { if (limb.body == sourceBody) { + targetLimb = limb; if (breakLimb) { character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); } - else - { - limb.HideAndDisable(hideLimbTimer); - } break; } } } + if (hideLimb) + { + targetLimb?.HideAndDisable(hideLimbTimer); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 382b21282..9684fbe89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -173,10 +173,13 @@ namespace Barotrauma.Steam private InstallTaskCounter(UInt64 id) { itemId = id; } public static bool IsInstalling(Steamworks.Ugc.Item item) + => IsInstalling(item.Id); + + public static bool IsInstalling(ulong itemId) { lock (mutex) { - return installers.Any(i => i.itemId == item.Id); + return installers.Any(i => i.itemId == itemId); } } @@ -193,9 +196,9 @@ namespace Barotrauma.Steam } } - public static async Task Create(Steamworks.Ugc.Item item) + public static async Task Create(ulong itemId) { - var retVal = new InstallTaskCounter(item.Id); + var retVal = new InstallTaskCounter(itemId); await retVal.Init(); return retVal; } @@ -260,19 +263,15 @@ namespace Barotrauma.Steam public static bool IsInstalling(Steamworks.Ugc.Item item) => InstallTaskCounter.IsInstalling(item); - + private static async Task InstallMod(ulong id) { - var item = await GetItem(id); - if (item is null) { return; } - await InstallMod(item.Value); - } - - private static async Task InstallMod(Steamworks.Ugc.Item item) - { - await Task.Yield(); - using var installCounter = await InstallTaskCounter.Create(item); + using var installCounter = await InstallTaskCounter.Create(id); + var itemNullable = await GetItem(id); + if (!(itemNullable is { } item)) { return; } + await Task.Yield(); + string itemTitle = item.Title.Trim(); UInt64 itemId = item.Id; string itemDirectory = item.Directory; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 7f6743010..30054648d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -8,6 +8,7 @@ using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; +using System.Globalization; namespace Barotrauma { @@ -349,6 +350,11 @@ namespace Barotrauma description += extraDescriptionLine; } + public static LocalizedString FormatCurrency(int amount) + { + return GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); + } + public static LocalizedString GetServerMessage(string serverMessage) { return new ServerMsgLString(serverMessage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index fefee9d61..40f135e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -2,19 +2,19 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma.IO { static class Validation { - private static readonly string[] unwritableDirs = new string[] { "Content" }; - private static readonly string[] unwritableExtensions = new string[] + private static readonly ImmutableArray unwritableDirs = new[] { "Content".ToIdentifier() }.ToImmutableArray(); + private static readonly ImmutableArray unwritableExtensions = new[] { - ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) + ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe, .dll and .json handled separately in CanWrite) ".bat", ".sh", //shell scripts - ".json" //deps.json - }; + }.ToIdentifiers().ToImmutableArray(); /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. @@ -24,25 +24,27 @@ namespace Barotrauma.IO public static bool CanWrite(string path, bool isDirectory) { path = System.IO.Path.GetFullPath(path).CleanUpPath(); + string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); + string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); if (!isDirectory) { - string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); - if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + Identifier extension = System.IO.Path.GetExtension(path).Replace(" ", "").ToIdentifier(); + if (unwritableExtensions.Any(e => e == extension)) { return false; } - if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) - && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) + if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase) + && !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase) + && (extension == ".dll" || extension == ".exe" || extension == ".json")) { return false; } } - foreach (string unwritableDir in unwritableDirs) + foreach (var unwritableDir in unwritableDirs) { - string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath(); + string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath(); if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f5663e4fd..f939ea059 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,104 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.4.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Split outposts stores into several different vendors who sell different types of items. +- Made DockingPort.ApplyEffectsOnDocking editable in the sub editor. +- Some UX improvements in the installed mods lists (unstable only). +- Items can be purchased from vending machines using the personal wallet (WIP). + +Fixes: +- Attempt to fix a crash in AIObjectiveExtinguishFire. +- Fixed bots not being able to repair leaks between rooms (leaks that are not in the outer walls). +- Fixed "deep sea slayer" always giving you a 50% buff to harpoons regardless if you're inside or not. +- Fixed text scale not being taken into account on scrolling text displays. + +Fixes (unstable only): +- Fixed Workshop items not getting uninstalled when they're removed through the Publish tab. +- Fixed mods being disabled when they're updated. +- Fixed the weird sitting animation for the new office chair. +- Fixed a race condition in Workshop item installer. +- Mod list filter works when coming back from looking at an installed mod's info. +- Fixed new submarines being disabled upon restart. +- Fixed stat types being displayed incorrectly in talent tooltips (e.g. "stattypenames.repairspeed" instead of "Repair Speed"). +- Fixed crashing when selecting a client who's not controlling a character in the tab menu. +- Fixed ContentPackageManager.Init failing if more than 100 mods are enabled. +- Fixed structures' "use drop shadow" setting being forced on in the sub editor. +- Fixed tutorials being in a random order in the main menu and some of the tutorials spawning the character with an incorrect job. +- Raised the file transfer size limit for mods from 50 MB to 500 MB. +- Fixed the new abyss monster's tongue sometimes sliding and deattaching from the sub. +- Fixed ServerSettings.MonsterEnabled not syncing correctly in a modded server. +- Fixed crashing when selecting something in the "allow attaching to" dropdown of a module's saving prompt. +- Fixed group names not showing up in the sub editor's "Send selection to group..." context menu. +- Fixed crashing when trying to load a save that contains locations not present in the current content package(s). +- Fixed crashing if a mod adds locations that don't have any name formats specified. +- Fixed crash when saving, immediately deleting, and resaving Unnamed.sub. +- Fixed ai target overrides not working, because only the definitions in the variant file were used. Fixes e.g. Mudraptor Veteran not targeting anything but Tigerthreshers. + +Modding: +- Fixed "targetcontaineditem" still not working correctly. +- Fixed crashing when trying to remove fog of war at the very edges of the campaign map. Doesn't affect the vanilla game because there's enough padding at the edges of the map. + +--------------------------------------------------------------------------------------------------------- +v0.17.3.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- A new abyss monster (placeholder art and sounds, name pending) +- Overhauled colonies: completely new modules, improved layouts, new structures and items, new events. +- Adjustments to reactors and supercapacitors to prevent the increased supercapacitor loads from crippling the subs on default recharge rates: slightly increased Humpback reactor output and decreased the default recharge rate of the capacitors, reduced recharge rates in the 3 new subs and set the supercapacitor efficiency to match the rest of the subs. +- Throw an error in debug and unstable builds if beacon station becomes inactive after it's been activated (hopefully helps us diagnose why beacon missions sometimes fail after the beacon's been activated). +- Reimplemented ServerExecutable to be usable in non-core packages. Now, players must select a server executable from a dropdown in the "Host server" menu if multiple are available. +- Animation adjustment: The head now rotates towards the mouse cursor while aiming or swimming. +- Swim animation adjustment: The body now rotates towards the aim target also when the character is moving, and not only while staying still. Moving while not facing the movement direction results in reduced movement speed. +- Set the bottom hole probability to 0 in Cold Caverns, which reduces the size and the frequency of holes in the level bottom. +- Adjusted the probabilities for spawning the Thalamus in the wrecks. +- Added a (WIP) difficulty hierarchy for the abyss monsters. Easier monsters should spawn more frequently on an easier difficulty level, the harder should spawn more frequently on higher difficulty levels. Currently the new abyss monster is defined as the easiest, and Endworm the hardest. Charybdis is in between. + +Fixes: +- Re-filled Typhon 2 oxygen tank shelves. +- Fixed spawnpoint editing panel being too small on large resolutions. +- Fixed inability to equip one-handed items when there's a suitable container in the other hand (e.g. flashlight when there's a storage container in the other hand). +- Cargo missions don't require the cargo to be inside a hull: being in the sub is enough. Fixes inability to complete cargo missions with unconventional subs where the cargo is stored outside hulls. +- Fixed non-equipped items that can't be put into a duffel bag disappearing when a character despawns. +- Fixed incorrect animation parameters being used for swimming while wearing a regular diving suit. +- Fixed projectiles sometimes staying attached to the target even when they are far from it. +- Fixed monsters sometimes trying to follow targets after losing the track of them even when they should be falling back from them (according to the after attack behavior). +- Fixed monsters sometimes using the after attack behavior of the current attack even when the cooldown of that attack is not active. +- Fixed monsters sometimes being unable to target the submarine, because their attack was incorrectly considered invalid. +- Fixed fractal guardians fleeing to a shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). + +Fixes (unstable only): +- Fixed text scale slider not working. +- Fixed audio capture and output settings changes not being applied until the game was restarted. +- Fixed numerous circumstances where the Publish tab could cause a crash or softlock. +- Fixed creating and deleting item assemblies in the submarine editor. +- Fixed deleting submarines in the submarine editor. +- Trimmed down the filenames of mods transferred from the server to the clients. +- Fixed ballast flora's damage visualizations (particles, branches shaking, healthbar) not working in multiplayer (unstable only). +- Fixed occasional "invalid SetAttackTarget/ExecuteAttack" errors in multiplayer (unstable only). +- Fixed clients not taking control off the previous character properly when using freecam, preventing the character from moving if another player or AI takes control of it (unstable only). +- Fixed "event data was of the wrong type" error when characters spawn with items with depleted condition in their inventory (unstable only). +- Fixed incorrect power displayed on the reactor when unwired (unstable only). +- Fixed devices not powering down if they're disconnected from the grid when their voltage has been set above 0. Could be reproduced by powering up the sub in mp campaign, entering a new level and disconnecting e.g. the oxygen generator from the grid (unstable). +- Fixed hidden subs resetting client-side when restarting a server (unstable only). +- Randomize character appearance in server lobby if it hasn't been set (= when launching the game or v0.17 for the first time). Unstable only. + +Modding: +- ItemContainers apply the OnContaining effects even when the item is broken. Doesn't affect any vanilla items. +- Ropes attached to limbs now automatically snap when another attack is chosen. +- Ropes can now be set to break from the end instead of always breaking from the middle (see the new abyss monster for an example). +- Ropes can be set to break if they are in too steep angle to the target. +- Projectiles always stick permanently unless a stick duration is defined. +- Characters (with deformable sprites) can be set to be drawn after (on top of) other characters. Normally characters are drawn in the order of spawning. +- AI Triggers can now be permanent. +- Added a generic damage threshold that currently defines how much damage the character needs to take from a single hit to hit the avoiding and releasing captured targets. +- Added a support for multiple identifiers and types in the limb health definitions. +- Added a support for min range for ranged attacks. +- Fixed monsters not being able to shoot faster than every ~1.5 second if they change the attacking limb. +- Added new after attack behaviors: Reverse and ReverseUntilCanAttack. + --------------------------------------------------------------------------------------------------------- v0.17.2.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt index f29df7c55..a2c5918d6 100644 --- a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt +++ b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019 FakeFish Games +Copyright (c) 2019 FakeFish Ltd. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/README.md b/README.md index 99597222e..7943f78d2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ If you're interested in working on the code, either to develop mods or to contri ### Windows - [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 8.0 support (VS 2019 or later recommended) ### Linux -- [.NET Core 3.0 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) +- [.NET Core 3.1 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) ### macOS - [Visual Studio 2019 for Mac](https://visualstudio.microsoft.com/vs/mac/)