diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 71271780cf..6f4b24417f 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,7 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.12.6.2 (Spring Update 2026) + - v1.13.3.1 (Summer Update 2026) - Other validations: required: true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 9e38c0b434..4378be8fd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -258,7 +258,9 @@ partial void InitProjSpecific(ContentXElement element, Character character) //RelativeSpacing = 0.05f }; - InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight), + GUIFrame left = new(new RectTransform(new Vector2(0.25f, 1f), characterIndicatorArea.RectTransform), style: null); + + InventorySlotContainer = new GUICustomComponent(new RectTransform(Vector2.One, left.RectTransform), (spriteBatch, component) => { for (int i = 0; i < character.Inventory.Capacity; i++) @@ -266,6 +268,10 @@ partial void InitProjSpecific(ContentXElement element, Character character) if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; } if (character.Inventory.HideSlot(i)) { continue; } + int width = Character.Inventory.visualSlots[i].Rect.Width; + left.RectTransform.MinSize = new Point(width, left.RectTransform.MinSize.Y); + if (afflictionIconList != null) { afflictionIconList.RectTransform.MinSize = new Point(width, afflictionIconList.RectTransform.MinSize.Y); } + //don't draw the item if it's being dragged out of the slot bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); @@ -292,8 +298,7 @@ partial void InitProjSpecific(ContentXElement element, Character character) } }); - - cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") + cprButton = new GUIButton(new RectTransform(new Vector2(0.75f), left.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") { UserData = UIHighlightAction.ElementId.CPRButton, OnClicked = (button, userData) => @@ -316,12 +321,11 @@ partial void InitProjSpecific(ContentXElement element, Character character) return true; }, - ToolTip = TextManager.Get("doctor.cprobjective"), - IgnoreLayoutGroups = true, + ToolTip = TextManager.Get("tutorial.roles.medic.objective.cpr"), Visible = false }; - var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), + var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.5f, 1.0f), characterIndicatorArea.RectTransform), (spriteBatch, component) => { DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true); @@ -368,8 +372,6 @@ partial void InitProjSpecific(ContentXElement element, Character character) CanBeFocused = false }; - characterIndicatorArea.Recalculate(); - healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) { HoverCursor = CursorState.Hand diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 9deffae478..e1ffe6bc91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -866,7 +866,20 @@ async Task gameOwnershipTokenTest() GameSettings.SaveCurrentConfig(); }); }, isCheat: false)); - + + commands.Add(new Command("togglespoofeventmanagerid", "togglespoofeventmanagerid: Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.", (string[] args) => + { + if (GameMain.Client != null) + { + GameMain.Client.SpoofEntityManagerReceivedId = !GameMain.Client.SpoofEntityManagerReceivedId; + DebugConsole.NewMessage(GameMain.Client.SpoofEntityManagerReceivedId ? "Spoofing enabled ": "Spoofing disabled", Color.Green); + } + else + { + DebugConsole.NewMessage("Not connected to server", Color.Red); + } + })); + commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) => { SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid; @@ -4454,17 +4467,24 @@ private static void GetAdjustedPrice(ItemPrefab itemPrefab, ref int componentCos public static void StartLocalMPSession(int numClients = 2) { + string extraArguments = "-multiclienttestmode"; + if (NetConfig.UseLenientHandshake) + { + extraArguments += " -lenienthandshake"; + } + try { if (Process.GetProcessesByName("DedicatedServer").Length == 0) { #if WINDOWS - Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode"); + Process.Start("DedicatedServer.exe", arguments: extraArguments); #else - Process.Start("./DedicatedServer", arguments: "-multiclienttestmode"); + Process.Start("./DedicatedServer", arguments: extraArguments); #endif System.Threading.Thread.Sleep(1000); } + #if DEBUG GameClient.MultiClientTestMode = true; #endif @@ -4478,10 +4498,13 @@ public static void StartLocalMPSession(int numClients = 2) for (int i = 2; i <= numClients; i++) { System.Threading.Thread.Sleep(1000); + + string clientArguments = $"-connect server localhost -username client{i} -skipintro"; + #if WINDOWS - Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode"); + Process.Start("Barotrauma.exe", arguments: $"{clientArguments} {extraArguments}"); #else - Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode"); + Process.Start("./Barotrauma", arguments: $"{clientArguments} {extraArguments}"); #endif } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index bf67f7b93e..6d96280614 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -14,6 +14,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { @@ -649,12 +650,12 @@ public static void Draw(Camera cam, SpriteBatch spriteBatch) DrawMessages(spriteBatch, cam); if (MouseOn != null) - { + { + MouseOn.OnDrawToolTip?.Invoke(MouseOn); if (!MouseOn.ToolTip.IsNullOrWhiteSpace()) { MouseOn.DrawToolTip(spriteBatch); } - MouseOn.OnDrawToolTip?.Invoke(MouseOn); } if (SubEditorScreen.IsSubEditor()) @@ -2323,10 +2324,10 @@ public static GUIMessageBox PromptTextInput(LocalizedString header, string body, /// /// Creates a 7-segment display. /// - /// Returns if is or empty. + /// Returns if is . /// Defaults to TextManager.Get("kilowatt"). /// Defaults to . - public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null) + public static GUITextBlock CreateDigitalDisplay(RectTransform rect, [NotNullIfNotNull(nameof(leftLabelText))] out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null) { GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -2337,7 +2338,7 @@ public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextB }; leftLabel = null; - if (!leftLabelText.IsNullOrEmpty()) + if (leftLabelText != null) { leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index d035b9616a..3e93f115fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -384,14 +384,14 @@ public void Select(int forcedCaretIndex = -1, bool ignoreSelectSound = false) CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; CalculateCaretPos(); ClearSelection(); - bool wasSelected = selected; - selected = true; - GUI.KeyboardDispatcher.Subscriber = this; OnSelected?.Invoke(this, Keys.None); - if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound) + if (!selected && PlaySoundOnSelect && !ignoreSelectSound) { SoundPlayer.PlayUISound(GUISoundType.Select); } + selected = true; + //set this after we've set selected to true -> otherwise the textbox taking keyboard focus will trigger Select again + GUI.KeyboardDispatcher.Subscriber = this; } public void Deselect() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 90ca5d7132..2a1cc6a0e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -113,7 +113,10 @@ public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphics, float deltaTi spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea); + if (currentBackgroundTexture.Texture != null) + { + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea); + } overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); double noiseT = Timing.TotalTime * 0.02f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index b8ddf8f72f..5e8d217dca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -278,6 +278,12 @@ public GameMain(string[] args) GameClient.MultiClientTestMode = true; } #endif + + if (ConsoleArguments.Contains("-lenienthandshake")) + { + NetConfig.UseLenientHandshake = true; + } + GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); PerformanceCounter = new PerformanceCounter(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 59e6c3f119..87086230d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -127,12 +127,6 @@ public override void CreateSlots() (int)SlotPositions[i].X, (int)SlotPositions[i].Y, (int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier)); - - if (SlotTypes[i] == InvSlotType.HealthInterface && - character.CharacterHealth?.InventorySlotContainer != null) - { - slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f); - } ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent(); if (itemContainer != null) @@ -622,6 +616,7 @@ public override void Update(float deltaTime, Camera cam, bool isSubInventory = f for (int i = 0; i < capacity; i++) { + if (HideSlot(i)) { continue; } var item = slots[i].FirstOrDefault(); if (item != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index bee454c79c..95cad607aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -26,7 +26,7 @@ public void UpdateMsg() } else { - DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)); + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)).Fallback(Msg); } CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerDistributor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerDistributor.cs index 91d5f5d0fc..f3e6b4f6ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerDistributor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerDistributor.cs @@ -12,9 +12,12 @@ internal partial class PowerDistributor : PowerTransfer, IServerSerializable, IC private partial class PowerGroup { private GUIFrame? frame; + private GUIFrame? groupContent; + private GUILayoutGroup? nameGroup; private GUITextBox? nameBox; + private GUITextBlock? loadDisplayNameLabel; private GUIScrollBar? ratioSlider; - private readonly List powerUnitLabels = new List(); + private readonly List powerUnitLabels = []; private GUIFrame? divider; public bool IsVisible { get; private set; } = true; @@ -22,9 +25,9 @@ private partial class PowerGroup public void CreateGUI() { frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null); - GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null); + groupContent = new GUIFrame(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null); - GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft) + nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -37,7 +40,7 @@ public void CreateGUI() return true; } }; - nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle") + nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Screen.Selected == GameMain.SubEditorScreen ? Name : DisplayName.Value, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle") { MaxTextLength = MaxNameLength, OverflowClip = true, @@ -48,17 +51,40 @@ public void CreateGUI() return true; } }; + nameBox.OnSelected += (tb, _) => + { + if (tb.Selected) { return; } + tb.Text = Name; + }; nameBox.OnDeselected += (tb, _) => { Name = tb.Text; + tb.CaretIndex = 0; if (GameMain.Client == null) { return; } distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange)); }; + nameBox.GetChild().GetChild().OnDrawToolTip = comp => + { + if (Screen.Selected != GameMain.SubEditorScreen && !nameBox.Selected) + { + comp.ToolTip = null; + return; + } + + LocalizedString localizedText = TextManager.Get(nameBox.Text); + comp.ToolTip = localizedText.IsNullOrEmpty() + ? TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", nameBox.Text) + : TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", localizedText); + }; GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) }, - out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font); + out loadDisplayNameLabel, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font); loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString(); + float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z; + float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth; + nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height)); + ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider") { Step = SupplyRatioStep, @@ -78,16 +104,17 @@ public void CreateGUI() ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height); GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft), - out GUITextBlock? _, out GUITextBlock _, + out GUITextBlock? _, out GUITextBlock ratioDisplayUnitLabel, rightLabelText: "%"); ratioDisplay.TextGetter = () => DisplayRatio.ToString(); GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) }, - out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel, + out GUITextBlock? outputDisplayNameLabel, out GUITextBlock outputDisplayUnitLabel, TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font); outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString(); powerUnitLabels.Add(loadDisplayUnitLabel); + powerUnitLabels.Add(ratioDisplayUnitLabel); powerUnitLabels.Add(outputDisplayUnitLabel); GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); @@ -111,7 +138,14 @@ public void UpdateGUI() IsVisible = PowerOut.Wires.Count >= 1; frame!.Visible = IsVisible; divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this; - if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); } + if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) + { + GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); + + float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z; + float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth; + nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 7d29d3928c..01a3d48ca1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -716,13 +716,19 @@ public override void DrawHUD(SpriteBatch spriteBatch, Character character) GetAvailablePower(out float batteryCharge, out float batteryCapacity); - List availableAmmo = new List(); + List availableAmmo = []; + AddAmmoFromContainer(item.GetComponent()); foreach (MapEntity e in item.linkedTo) { - if (!(e is Item linkedItem)) { continue; } - var itemContainer = linkedItem.GetComponent(); - if (itemContainer == null) { continue; } + if (e is not Item linkedItem) { continue; } + AddAmmoFromContainer(linkedItem.GetComponent()); + } + + void AddAmmoFromContainer(ItemContainer itemContainer) + { + if (itemContainer == null) { return; } availableAmmo.AddRange(itemContainer.Inventory.AllItems); + //add empty slots too for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++) { availableAmmo.Add(null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 3f7bfdfcdb..7c5396020e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -339,6 +339,19 @@ private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, C toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } + + if (item.Prefab.UnlockedRecipeInToolTip.Length > 0 && GameMain.GameSession is { } GameSession) + { + if (item.Prefab.UnlockedRecipeInToolTip.All(id => GameSession.HasUnlockedRecipe(Character.Controlled, id))) + { + toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖"; + } + else + { + toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖"; + } + } + if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) { colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); @@ -356,19 +369,7 @@ private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, C } #if DEBUG toolTip += $" ({item.Prefab.Identifier})"; -#endif - if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession) - { - if (GameSession.HasUnlockedRecipe(Character.Controlled, item.Prefab.UnlockedRecipeInToolTip)) - { - toolTip += TextManager.Get("unlockedrecipe.true"); - } - else - { - toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖"; - } - } - +#endif if (PlayerInput.KeyDown(InputType.ContextualCommand)) { toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖"; @@ -1300,8 +1301,7 @@ void HandleOutsideInventoryDrop() SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List(DraggingItems), true)); } } - - SoundPlayer.PlayUISound(GUISoundType.DropItem); + bool removed = false; if (Screen.Selected is SubEditorScreen editor) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs index 9cfa2667e5..ec30aea96f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs @@ -75,11 +75,11 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase // ReSharper restore FieldCanBeMadeReadOnly.Local - private const string SettingsResetButtonText = "LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings"; - private const string SettingsResetPromptTitle = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title"; - private const string SettingsResetPromptContents = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message"; - private const string SettingsResetPromptYesText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes"; - private const string SettingsResetPromptNoText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No"; + private const string SettingsResetButtonText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetVisibleSettings"; + private const string SettingsResetPromptTitle = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Title"; + private const string SettingsResetPromptContents = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Message"; + private const string SettingsResetPromptYesText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Yes"; + private const string SettingsResetPromptNoText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.No"; private event Action OnApplyInstalledModsChanges; @@ -100,7 +100,7 @@ public ModsGameplaySettingsMenu(GUIFrame contentFrame, var menuTitleLayoutGroup = new GUILayoutGroup( new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft); GUIUtil.Label(menuTitleLayoutGroup, - GetLocalizedString("LuaCsForBarotrauma.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"), + GetLocalizedString($"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"), GUIStyle.LargeFont, new Vector2(1f, 1f)); // page contents diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs index 1d5cb2d4d6..1cbad0e1bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs @@ -46,10 +46,10 @@ private void CreateSettingsMenu(SettingsMenu __instance) var tabControlsIndex = (SettingsMenu.Tab)tabCount+1; _gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance, - GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods", - "LuaCsForBarotrauma.SettingsMenu.ModGameplayButton"); + GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods", + $"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton"); /*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance, - "SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton"); + "SettingsMenuTab.Controls", $"{LuaCsSetup.PackageName}.SettingsMenu.ModControlsButton"); */ _gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs index cecbcef809..05e82e5483 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs @@ -8,7 +8,7 @@ namespace Barotrauma { - class BackgroundCreature : ISteerable + class BackgroundCreature : ISteerable, ILevelRenderableObject { const float MaxDepth = 10000.0f; @@ -76,6 +76,8 @@ public Vector2 Steering set; } + public Vector3 Position => new Vector3(position.X, position.Y, Depth); + public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position) { this.Prefab = prefab; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index d997412acd..dc68ba547f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -15,52 +14,11 @@ class BackgroundCreatureManager private float checkVisibleTimer; - private readonly List creatures = new List(); + private readonly List creatures = []; - private readonly List visibleCreatures = new List(); + private readonly List visibleCreatures = []; - public BackgroundCreatureManager() - { - /*foreach(var file in files) - { - LoadConfig(file.Path); - }*/ - } - - /*public BackgroundCreatureManager(string path) - { - DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}"); - LoadConfig(ContentPath.FromRaw(null, path)); - } - - private void LoadConfig(ContentPath configPath) - { - try - { - XDocument doc = XMLExtensions.TryLoadXml(configPath); - if (doc == null) { return; } - var mainElement = doc.Root.FromPackage(configPath.ContentPackage); - if (mainElement.IsOverride()) - { - mainElement = mainElement.FirstElement(); - Prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple); - } - else if (Prefabs.Any()) - { - DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'"); - } - - foreach (var element in mainElement.Elements()) - { - Prefabs.Add(new BackgroundCreaturePrefab(element)); - }; - } - catch (Exception e) - { - DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e); - } - }*/ + public IEnumerable VisibleCreatures => visibleCreatures; public void SpawnCreatures(Level level, int count, Vector2? position = null) { @@ -161,14 +119,6 @@ public void Update(float deltaTime, Camera cam) } } - public void Draw(SpriteBatch spriteBatch, Camera cam) - { - foreach (BackgroundCreature creature in visibleCreatures) - { - creature.Draw(spriteBatch, cam); - } - } - public void DrawLights(SpriteBatch spriteBatch, Camera cam) { foreach (BackgroundCreature creature in visibleCreatures) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index d3d2b8b53e..323bb74dae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -140,7 +140,7 @@ public void DrawBack(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera ca public void DrawFront(SpriteBatch spriteBatch, Camera cam) { - renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager); + renderer?.DrawForeground(spriteBatch, cam, backgroundCreatureManager, LevelObjectManager); } public void ClientEventRead(IReadMessage msg, float sendingTime) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index d56efd35b7..aa1bea8081 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -12,7 +12,7 @@ namespace Barotrauma { - partial class LevelObject + partial class LevelObject : ILevelRenderableObject { public float SwingTimer; public float ScaleOscillateTimer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index f4b48feb58..08b2d982e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -8,13 +8,20 @@ namespace Barotrauma { + public interface ILevelRenderableObject + { + public Vector3 Position { get; } + + } + partial class LevelObjectManager { + // Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower? - private readonly List visibleObjectsBack = new List(MaxVisibleObjects); - private readonly List visibleObjectsMid = new List(MaxVisibleObjects); - private readonly List visibleObjectsFront = new List(MaxVisibleObjects); - private readonly HashSet allVisibleObjects = new HashSet(MaxVisibleObjects); + private readonly List visibleObjectsBack = new List(MaxVisibleObjects); + private readonly List visibleObjectsMid = new List(MaxVisibleObjects); + private readonly List visibleObjectsFront = new List(MaxVisibleObjects); + private readonly HashSet allVisibleObjects = new HashSet(MaxVisibleObjects); private double NextRefreshTime; @@ -35,35 +42,44 @@ partial void RemoveProjSpecific() partial void UpdateProjSpecific(float deltaTime, Camera cam) { - foreach (LevelObject obj in visibleObjectsBack) + foreach (ILevelRenderableObject obj in visibleObjectsBack) { - obj.Update(deltaTime, cam); + if (obj is LevelObject levelObj) + { + levelObj.Update(deltaTime, cam); + } } - foreach (LevelObject obj in visibleObjectsMid) + foreach (ILevelRenderableObject obj in visibleObjectsMid) { - obj.Update(deltaTime, cam); + if (obj is LevelObject levelObj) + { + levelObj.Update(deltaTime, cam); + } } - foreach (LevelObject obj in visibleObjectsFront) + foreach (ILevelRenderableObject obj in visibleObjectsFront) { - obj.Update(deltaTime, cam); + if (obj is LevelObject levelObj) + { + levelObj.Update(deltaTime, cam); + } } } /// /// Returns all visible objects, but not in order, because internally uses a HashSet. /// - public IEnumerable GetAllVisibleObjects() + public IEnumerable GetAllVisibleObjects() { allVisibleObjects.Clear(); - foreach (LevelObject obj in visibleObjectsBack) + foreach (ILevelRenderableObject obj in visibleObjectsBack) { allVisibleObjects.Add(obj); } - foreach (LevelObject obj in visibleObjectsMid) + foreach (ILevelRenderableObject obj in visibleObjectsMid) { allVisibleObjects.Add(obj); } - foreach (LevelObject obj in visibleObjectsFront) + foreach (ILevelRenderableObject obj in visibleObjectsFront) { allVisibleObjects.Add(obj); } @@ -73,7 +89,7 @@ public IEnumerable GetAllVisibleObjects() /// /// Checks which level objects are in camera view and adds them to the visibleObjects lists /// - private void RefreshVisibleObjects(Rectangle currentIndices, float zoom) + private void RefreshVisibleObjects(Rectangle currentIndices, BackgroundCreatureManager backgroundCreatureManager, float zoom) { visibleObjectsBack.Clear(); visibleObjectsMid.Clear(); @@ -152,6 +168,27 @@ void CheckIndex(int x, int y) } } + foreach (var backgroundCreature in backgroundCreatureManager.VisibleCreatures) + { + int drawOrderIndex = 0; + for (int i = 0; i < visibleObjectsBack.Count; i++) + { + if (visibleObjectsBack[i].Position.Z > backgroundCreature.Position.Z) + { + break; + } + else + { + drawOrderIndex = i + 1; + if (drawOrderIndex >= MaxVisibleObjects) { break; } + } + } + if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects) + { + visibleObjectsBack.Insert(drawOrderIndex, backgroundCreature); + } + } + //object grid is sorted in an ascending order //(so we prefer the objects in the foreground instead of ones in the background if some need to be culled) //rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists @@ -165,28 +202,28 @@ void CheckIndex(int x, int y) /// /// Draw the objects behind the level walls /// - public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam) + public void DrawObjectsBack(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam) { - DrawObjects(spriteBatch, cam, visibleObjectsBack); + DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsBack); } /// /// Draw the objects in front of the level walls, but behind characters /// - public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam) + public void DrawObjectsMid(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam) { - DrawObjects(spriteBatch, cam, visibleObjectsMid); + DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsMid); } /// /// Draw the objects in front of the level walls and characters /// - public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam) + public void DrawObjectsFront(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam) { - DrawObjects(spriteBatch, cam, visibleObjectsFront); + DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsFront); } - private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List objectList) + private void DrawObjects(SpriteBatch spriteBatch, Camera cam, BackgroundCreatureManager backgroundCreatureManager, List objectList) { Rectangle indices = Rectangle.Empty; indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); @@ -207,7 +244,7 @@ private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List float z = 0.0f; if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime)) { - RefreshVisibleObjects(indices, cam.Zoom); + RefreshVisibleObjects(indices, backgroundCreatureManager, cam.Zoom); ForceRefreshVisibleObjects = false; if (cam.Zoom < 0.1f) { @@ -216,61 +253,93 @@ private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List } } - foreach (LevelObject obj in objectList) + bool prevObjectHasDeformableSprite = false; + foreach (ILevelRenderableObject obj2 in objectList) { - Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter; + Vector2 camDiff = new Vector2(obj2.Position.X, obj2.Position.Y) - cam.WorldViewCenter; camDiff.Y = -camDiff.Y; - - Sprite activeSprite = obj.Sprite; - activeSprite?.Draw( - spriteBatch, - new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, - Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth), - activeSprite.Origin, - obj.CurrentRotation, - obj.CurrentScale, - SpriteEffects.None, - z); - - if (obj.ActivePrefab.DeformableSprite != null) + + bool hasDeformableSprite = false; + if (obj2 is LevelObject levelObject) { - if (obj.CurrentSpriteDeformation != null) + hasDeformableSprite = levelObject.ActivePrefab.DeformableSprite != null; + if (hasDeformableSprite != prevObjectHasDeformableSprite) { - obj.ActivePrefab.DeformableSprite.Deform(obj.CurrentSpriteDeformation); + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, + BlendState.NonPremultiplied, + SamplerState.LinearWrap, DepthStencilState.DepthRead, + transformMatrix: cam.Transform); } - else + + Sprite activeSprite = levelObject.Sprite; + activeSprite?.Draw( + spriteBatch, + new Vector2(levelObject.Position.X, -levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, + Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / levelObject.Prefab.FadeOutDepth), + activeSprite.Origin, + levelObject.CurrentRotation, + levelObject.CurrentScale, + SpriteEffects.None, + z); + + if (hasDeformableSprite) { - obj.ActivePrefab.DeformableSprite.Reset(); + if (levelObject.CurrentSpriteDeformation != null) + { + levelObject.ActivePrefab.DeformableSprite.Deform(levelObject.CurrentSpriteDeformation); + } + else + { + levelObject.ActivePrefab.DeformableSprite.Reset(); + } + levelObject.ActivePrefab.DeformableSprite?.Draw(cam, + new Vector3(new Vector2(levelObject.Position.X, levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, z * 10.0f), + levelObject.ActivePrefab.DeformableSprite.Origin, + levelObject.CurrentRotation, + levelObject.CurrentScale, + Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / 5000.0f)); } - obj.ActivePrefab.DeformableSprite?.Draw(cam, - new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f), - obj.ActivePrefab.DeformableSprite.Origin, - obj.CurrentRotation, - obj.CurrentScale, - Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f)); - } - - - if (GameMain.DebugDraw) - { - GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true); + prevObjectHasDeformableSprite = hasDeformableSprite; - if (obj.Triggers == null) { continue; } - foreach (LevelTrigger trigger in obj.Triggers) + if (GameMain.DebugDraw) { - if (trigger.PhysicsBody == null) continue; - GUI.DrawLine(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3); + GUI.DrawRectangle(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true); - Vector2 flowForce = trigger.GetWaterFlowVelocity(); - if (flowForce.LengthSquared() > 1) + if (levelObject.Triggers == null) { continue; } + foreach (LevelTrigger trigger in levelObject.Triggers) { - flowForce.Y = -flowForce.Y; - GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5); + if (trigger.PhysicsBody == null) continue; + GUI.DrawLine(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3); + + Vector2 flowForce = trigger.GetWaterFlowVelocity(); + if (flowForce.LengthSquared() > 1) + { + flowForce.Y = -flowForce.Y; + GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5); + } + trigger.PhysicsBody.UpdateDrawPosition(); + trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan); } - trigger.PhysicsBody.UpdateDrawPosition(); - trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan); } + + } + else if (obj2 is BackgroundCreature backgroundCreature && cam.Zoom > 0.05f) + { + hasDeformableSprite = backgroundCreature.Prefab.DeformableSprite != null; + if (hasDeformableSprite != prevObjectHasDeformableSprite) + { + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, + BlendState.NonPremultiplied, + SamplerState.LinearWrap, DepthStencilState.DepthRead, + transformMatrix: cam.Transform); + } + + backgroundCreature.Draw(spriteBatch, cam); } + prevObjectHasDeformableSprite = hasDeformableSprite; + z += 0.0001f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index a7c47f0664..5e4f475f8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -214,8 +214,9 @@ public void Update(float deltaTime, Camera cam) //calculate the sum of the forces of nearby level triggers //and use it to move the water texture and water distortion effect Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; - foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects()) + foreach (ILevelRenderableObject obj in level.LevelObjectManager.GetAllVisibleObjects()) { + if (obj is not LevelObject levelObject) { continue; } if (levelObject.Triggers == null) { continue; } //use the largest water flow velocity of all the triggers Vector2 objectMaxFlow = Vector2.Zero; @@ -274,11 +275,7 @@ public void DrawBackground(SpriteBatch spriteBatch, Camera cam, SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, cam.Transform); - backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam); - if (cam.Zoom > 0.05f) - { - backgroundCreatureManager?.Draw(spriteBatch, cam); - } + backgroundSpriteManager?.DrawObjectsBack(spriteBatch, backgroundCreatureManager, cam); level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset); @@ -292,17 +289,18 @@ public void DrawBackground(SpriteBatch spriteBatch, Camera cam, BlendState.NonPremultiplied, SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, cam.Transform); - backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam); + backgroundSpriteManager?.DrawObjectsMid(spriteBatch, backgroundCreatureManager, cam); spriteBatch.End(); } - public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null) + public void DrawForeground(SpriteBatch spriteBatch, Camera cam, + BackgroundCreatureManager backgroundCreatureManager, LevelObjectManager backgroundSpriteManager = null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, cam.Transform); - backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam); + backgroundSpriteManager?.DrawObjectsFront(spriteBatch, backgroundCreatureManager, cam); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 5e64723bf4..db4b498bf0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2547,7 +2547,7 @@ private void SendIngameUpdate() segmentTable.StartNewSegment(ClientNetSegment.SyncIds); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); outmsg.WriteUInt16(ChatMessage.LastID); - outmsg.WriteUInt16(EntityEventManager.LastReceivedID); + outmsg.WriteUInt16(SpoofEntityManagerReceivedId ? (ushort)1 : EntityEventManager.LastReceivedID); outmsg.WriteUInt16(LastClientListUpdateID); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) @@ -3370,6 +3370,12 @@ public VotingInterface VotingInterface { get { return votingInterface; } } + + /// + /// Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind. + /// + public bool SpoofEntityManagerReceivedId { get; set; } + private VotingInterface votingInterface; public bool TypingChatMessage(GUITextBox textBox, string text) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 596a03649f..23808ff471 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -89,6 +89,7 @@ public void Read(IReadMessage msg) { byte queueId = msg.ReadByte(); float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8); + bool isRadio = msg.ReadBoolean(); VoipQueue queue = queues.Find(q => q.QueueID == queueId); if (queue == null) @@ -117,19 +118,21 @@ public void Read(IReadMessage msg) float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent senderRadio = null; - var messageType = - !client.VoipQueue.ForceLocal && - ChatMessage.CanUseRadio(client.Character, out senderRadio) && - (spectating || (ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio))) - ? ChatMessageType.Radio : ChatMessageType.Default; + var messageType = isRadio ? ChatMessageType.Radio : ChatMessageType.Default; + client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { + //If the client cannot establish a radio, use a headsets default range as a fallback to calculate the radio noise. + //This cannot happen in an un-modded setting as CanUseRadio is part of the server side check for isRadio to be true. + ChatMessage.CanUseRadio(client.Character, out senderRadio); + float senderRadioRange = (senderRadio == null) ? 35000.0f : senderRadio.Range; + client.VoipSound.UsingRadio = true; - client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier); + client.VoipSound.SetRange(senderRadioRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadioRange * speechImpedimentMultiplier * rangeMultiplier); if (distanceFactor > RangeNear && !spectating) { //noise starts increasing exponentially after 40% range diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 2d1f031145..6bb1ee9781 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -533,6 +533,18 @@ GUITextBlock createRightText(LocalizedString str) return true; } }; + + new GUITickBox(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 280) }, + "Lenient server startup timeouts") + { + Selected = NetConfig.UseLenientHandshake, + ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load.", + OnSelected = (tickBox) => + { + NetConfig.UseLenientHandshake = tickBox.Selected; + return true; + } + }; #endif var minButtonSize = new Point(120, 20); @@ -1126,6 +1138,11 @@ private void StartServer() } #endif + if (NetConfig.UseLenientHandshake) + { + arguments.Add("-lenienthandshake"); + } + var processInfo = new ProcessStartInfo { FileName = fileName, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 72ccad2185..2dab9c67e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,19 +1,22 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Sounds; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +using Steamworks; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IO.Compression; using System.Linq; using System.Threading; +using System.Xml; using System.Xml.Linq; using Barotrauma.LuaCs.Events; -using Barotrauma.Sounds; namespace Barotrauma { @@ -2044,6 +2047,8 @@ public static Type DetermineSubFileType(SubmarineType type) private bool SaveSubToFile(string name, ContentPackage packageToSaveTo) { + bool remoteStorageWasEnabled = Submarine.MainSub.Info.SaveToRemoteStorage; + Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); static string getExistingFilePath(ContentPackage package, string fileName) @@ -2272,6 +2277,16 @@ void addSubAndSave(ModProject modProject, string filePath, string packagePath) linkedSubBox.AddItem(sub.Name, sub); } subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + + if (remoteStorageWasEnabled) + { + Submarine.MainSub.Info.SaveToRemoteStorage = true; + + RemoteStorageHelper.TryWrite( + localPath: MainSub.Info.FilePath, + saveAs: MainSub.Info.FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false), + allowOverwrite: true); + } } } } @@ -3223,6 +3238,42 @@ private void CreateSaveScreen(bool quickSave = false) previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); + if (SteamManager.IsInitialized) + { + GUILayoutGroup remoteStorageArea = new( + new RectTransform(new Vector2(1f, 0.05f), rightColumn.RectTransform, + minSize: new Point(0, minHeight)), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + + new GUITextBlock(new RectTransform(Vector2.One, remoteStorageArea.RectTransform), + TextManager.Get("RemoteStorageToggle.Title"), textAlignment: Alignment.CenterLeft, wrap: true); + + new GUITickBox(new RectTransform(Vector2.One, remoteStorageArea.RectTransform), label: "") + { + OnAddedToGUIUpdateList = component => + { + // Values may change outside of game. + component.Enabled = SteamRemoteStorage.IsCloudEnabledForAccount; + component.ToolTip = !SteamRemoteStorage.IsCloudEnabledForAccount ? TextManager.Get("RemoteStorageToggle.Disabled") : ""; + ((GUITickBox)component).SetSelected(SteamRemoteStorage.IsCloudEnabled && MainSub.Info.SaveToRemoteStorage, callOnSelected: false); + }, + OnSelected = tickBox => + { + if (tickBox.Selected && !SteamRemoteStorage.IsCloudEnabledForApp) + { + RemoteStorageHelper.AskToEnable(onAccepted: () => MainSub.Info.SaveToRemoteStorage = true); + return false; + } + return MainSub.Info.SaveToRemoteStorage = tickBox.Selected; + } + }; + } + var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true); GUIButton createTabberBtn(string labelTag) @@ -3746,7 +3797,7 @@ private void CreateLoadScreen() } var package = GetLocalPackageThatOwnsSub(subInfo); - if (package != null) + if (package != null || subInfo.IsFromRemoteStorage) { deleteBtn.Enabled = true; } @@ -3771,13 +3822,29 @@ private void CreateLoadScreen() searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - var sortedSubs = GetLoadableSubs() - .OrderBy(s => s.Type) - .ThenBy(s => s.Name) - .ToList(); + List allSubs = [.. SubmarineInfo.SavedSubmarines]; + foreach (SteamRemoteStorage.RemoteFile remoteFile in SteamRemoteStorage.Files.Where(file => file.Filename.EndsWith(".sub"))) + { + if (!remoteFile.TryRead(out byte[] bytes)) { continue; } - SubmarineInfo prevSub = null; + using System.IO.MemoryStream stream = new(bytes); + using GZipStream zipStream = new(stream, CompressionMode.Decompress); + if (XMLExtensions.TryLoadXml(zipStream) is not XDocument doc) + { + DebugConsole.ThrowError($"{RemoteStorageHelper.DebugPrefix} Failed to load submarine \"{remoteFile.Filename}\" from remote storage: file is not a valid XML document."); + continue; + } + + SubmarineInfo subInfo = new(remoteFile.Filename, element: doc.Root, tryLoad: false) { IsFromRemoteStorage = true }; + allSubs.Add(subInfo); + } + IOrderedEnumerable sortedSubs = allSubs + .OrderBy(kvp => kvp.Type) + .ThenBy(kvp => kvp.Name) + .ThenBy(kvp => kvp.IsFromRemoteStorage); + + SubmarineInfo prevSub = null; foreach (SubmarineInfo sub in sortedSubs) { if (prevSub == null || prevSub.Type != sub.Type) @@ -3795,35 +3862,44 @@ private void CreateLoadScreen() prevSub = sub; } - string pathWithoutUserName = Path.GetFullPath(sub.FilePath); - string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); - if (pathWithoutUserName.StartsWith(saveFolder)) + string displayPath = sub.FilePath; + if (sub.IsFromRemoteStorage) { - pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; + displayPath += $" {TextManager.Get("RemoteStorage")}"; } else { - pathWithoutUserName = sub.FilePath; + string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); + string fullPath = Path.GetFullPath(displayPath); + if (fullPath.StartsWith(saveFolder)) + { + displayPath = $"...{fullPath[saveFolder.Length..]}"; + } } - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) + LocalizedString limitedName = ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80); + GUITextBlock textBlock = new(new RectTransform(Vector2.UnitX, subList.Content.RectTransform) { MinSize = new Point(0, 30) }, limitedName) { UserData = sub, - ToolTip = pathWithoutUserName + ToolTip = displayPath }; - if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) + if (sub.IsFromRemoteStorage) + { + // remote storage + textBlock.OverrideTextColor(RemoteStorageHelper.SteamColor); + } + else if (ContentPackageManager.VanillaCorePackage == null || ContentPackageManager.VanillaCorePackage.Files.None(f => f.Path == sub.FilePath)) { if (GetLocalPackageThatOwnsSub(sub) == null && ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) { - //workshop mod + // workshop mod textBlock.OverrideTextColor(Color.MediumPurple); } else { - //local mod + // local mod textBlock.OverrideTextColor(GUIStyle.TextColorBright); } } @@ -4018,10 +4094,9 @@ private bool HitLoadSubButton(GUIButton button, object obj) return false; } - if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; } + if (subList.SelectedComponent?.UserData is not SubmarineInfo selectedSubInfo) { return false; } - var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); - if (ownerPackage is null) + if (!selectedSubInfo.IsFromRemoteStorage && GetLocalPackageThatOwnsSub(selectedSubInfo) is null) { if (IsVanillaSub(selectedSubInfo)) { @@ -4183,21 +4258,23 @@ private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } - //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 = GetLocalPackageThatOwnsSub(sub); - if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } - - var msgBox = new GUIMessageBox( - TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + // 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) + ContentPackage subPackage = GetLocalPackageThatOwnsSub(sub); + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { subPackage = null; } + if (!sub.IsFromRemoteStorage && subPackage == null) { return; } + + GUIMessageBox msgBox = new(TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), [TextManager.Get("Yes"), TextManager.Get("Cancel")]); msgBox.Buttons[0].OnClicked += (btn, userData) => { - try + if (sub.IsFromRemoteStorage) + { + RemoteStorageHelper.TryDelete(sub.FilePath); + } + else if (subPackage != null) { - if (subPackage != null) + try { File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false); ModProject modProject = new ModProject(subPackage); @@ -4208,17 +4285,17 @@ private void TryDeleteSub(SubmarineInfo sub) { MainSub.Info.FilePath = null; } - } - sub.Dispose(); - CreateLoadScreen(); - } - catch (Exception e) - { - DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e); + } + catch (Exception e) + { + DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e); + } } - return true; + + sub.Dispose(); + CreateLoadScreen(); + return msgBox.Close(btn, userData); }; - msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += msgBox.Close; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 22b885e50e..bf90e8c542 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -445,7 +445,7 @@ public GUIComponent CreateNewField(SerializableProperty property, ISerializableE { DebugConsole.NewMessage("Missing Localization for property: " + propertyTag); MissingLocalizations.Add($"sp.{propertyTag}.name|{displayName}"); - MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute().Description}"); + MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute()?.Description}"); } } #endif @@ -467,7 +467,7 @@ public GUIComponent CreateNewField(SerializableProperty property, ISerializableE } if (toolTip.IsNullOrEmpty()) { - toolTip = property.GetAttribute().Description; + toolTip = property.GetAttribute()?.Description; } GUIComponent propertyField = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/RemoteStorageHelper.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/RemoteStorageHelper.cs new file mode 100644 index 0000000000..f6c1193985 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/RemoteStorageHelper.cs @@ -0,0 +1,34 @@ +#nullable enable +using Steamworks; +using System; +namespace Barotrauma.Steam; + +internal static partial class RemoteStorageHelper +{ + /// + /// Asks the user if they wish to enable remote storage. Accepting enables it automatically. + /// + /// Invoked when the user accepts enabling remote storage. + /// Invoked when the user rejects enabling remote storage. + /// Closes automatically if remote storage was enabled outside of the game, or if remote storage can not be enabled. + public static void AskToEnable(Action? onAccepted = null, Action? onRejected = null) + { + GUIMessageBox confirmBox = new GUIMessageBox( + TextManager.Get("RemoteStorageEnablePopup.Header"), + TextManager.Get("RemoteStorageEnablePopup.Text"), + [TextManager.Get("Yes"), TextManager.Get("No")], + autoCloseCondition: () => !SteamRemoteStorage.IsCloudEnabledForAccount || SteamRemoteStorage.IsCloudEnabledForApp); + + confirmBox.Buttons[0].OnClicked += (btn, data) => + { + SteamRemoteStorage.IsCloudEnabledForApp = true; + onAccepted?.Invoke(); + return confirmBox.Close(btn, data); + }; + confirmBox.Buttons[1].OnClicked += (btn, data) => + { + onRejected?.Invoke(); + return confirmBox.Close(btn, data); + }; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 20e0c832f1..2fbdb22d0b 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index cc877ab15a..fad9a9f911 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 9bf9ca415f..cef4dfb031 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 411d2e5ee1..4fc23a8d81 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index fb79c6495f..df4100226c 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index dff16fe001..76bcea7682 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1799,22 +1799,35 @@ string eventDebugStr(ServerEntityEvent ev) (Client client, Vector2 cursorWorldPos, string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) { return; } + Submarine submarineToTeleport = Submarine.MainSub; + if (args.Length > 1) + { + foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic)) + { + if ((sub.Info.Name + "_" + sub.TeamID) == args[1]) + { + submarineToTeleport = sub; + break; + } + } + } + if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { - Submarine.MainSub.SetPosition(cursorWorldPos); + submarineToTeleport.SetPosition(cursorWorldPos); } else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { - Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + submarineToTeleport.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * submarineToTeleport.Borders.Height); } else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { - Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + submarineToTeleport.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * submarineToTeleport.Borders.Height); } else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) { - Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); - var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height); + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport); if (Level.Loaded?.EndOutpost == null) { NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bb6f889ec..3b62d212d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1179,6 +1179,7 @@ public void ServerReadMoney(IReadMessage msg, Client sender) NetWalletTransfer transfer = INetSerializableStruct.Read(msg); if (GameMain.Server is null) { return; } + if (transfer.Amount <= 0) { return; } if (transfer.Sender.TryUnwrap(out var id)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 38e09b975e..89ab594073 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -193,13 +193,6 @@ void HandleAddedItems() (item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory)); - // Prevent modified clients from being able to steal items from characters by item swapping with an existing item - // due to drag and drop being enabled - if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null) - { - itemAccessDenied = true; - } - //more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied) { @@ -219,7 +212,7 @@ void HandleAddedItems() continue; } } - TryPutItem(item, slotIndex, true, true, sender.Character, false); + TryPutItem(item, slotIndex, allowSwapping: false, allowCombine: false, user: sender.Character, createNetworkEvent: false); for (int j = 0; j < capacity; j++) { if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 0142259468..1357d180e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -8,6 +8,17 @@ namespace Barotrauma.Networking { partial class ChatMessage { + private static string SanitizeText(Client client, string text) + { + if (!client.HasPermission(ClientPermissions.SpamImmunity)) + { + // Prevent clients without spam immunity from being able to use RichString features + text = text.Replace('‖', ' '); + } + + return text; + } + public static void ServerRead(IReadMessage msg, Client c) { c.KickAFKTimer = 0.0f; @@ -69,8 +80,7 @@ public static void ServerRead(IReadMessage msg, Client c) txt = msg.ReadString() ?? ""; } - // Sanitize incoming text message from client so they can't use RichString features - txt = txt.Replace('‖', ' '); + txt = SanitizeText(c, txt); if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 1600352947..ace0c1a8f2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -904,7 +904,10 @@ private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) string subHash = inc.ReadString(); CampaignSettings settings = INetSerializableStruct.Read(inc); - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); + var matchingSub = + ServerSettings.AllowSubVoting ? + Voting.HighestVoted(VoteType.Sub, connectedClients) : + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (GameStarted) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 81e2246ae0..f27ed0706f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -373,7 +373,7 @@ public void Update(List clients) // in which case we'll wait until the timeout runs out before kicking the client List toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && - (firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); + (!c.NeedsMidRoundSync || firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index 5970af2bfc..f32931dc70 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -54,13 +54,14 @@ public void SendToClients(List clients) { if (recipient == sender) { continue; } - if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; } + if (!CanReceive(sender, recipient, out float distanceFactor, out bool isRadio)) { continue; } IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.VOICE); msg.WriteByte((byte)queue.QueueID); msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8); + msg.WriteBoolean(isRadio); queue.Write(msg); netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); @@ -68,15 +69,17 @@ public void SendToClients(List clients) } } - private static bool CanReceive(Client sender, Client recipient, out float distanceFactor) + private static bool CanReceive(Client sender, Client recipient, out float distanceFactor, out bool isRadio) { if (Screen.Selected != GameMain.GameScreen) { distanceFactor = 0.0f; - return true; + isRadio = false; + return true; } distanceFactor = 0.0f; + isRadio = false; //no-one can hear muted players if (sender.Muted) { return false; } @@ -109,12 +112,14 @@ private static bool CanReceive(Client sender, Client recipient, out float distan if (recipientSpectating) { + isRadio = true; if (recipient.SpectatePos == null) { return true; } distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f); return distanceFactor < 1.0f; } else if (recipientRadio != null && recipientRadio.CanReceive(senderRadio)) { + isRadio = true; distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f); return true; } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index edf236deb8..40f4491821 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.12.7.0 + 1.13.3.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub index 10d816db39..c73783ed94 100644 Binary files a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml index 4b58a87c84..9e2accd4e5 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml @@ -23,6 +23,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index 2e03653179..d3278fed90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -524,8 +524,7 @@ GameMain.GameSession.Campaign is MultiPlayerCampaign && { UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); } - else if (item.Prefab.Identifier == "nuclearshell" || - item.Prefab.Identifier == "nucleardepthcharge") + else if (item.Prefab.Tags.Contains("nuclearexplosive")) { UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index eeab5c9b6b..fdc62eeee3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -197,6 +197,18 @@ private set private readonly List myBodies; +#if SERVER + /// + /// How often the server can send messages about a limb targeting some attack target. + /// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame) + /// + private const double MinSetAttackTargetEventInterval = 0.5; + private IDamageable lastDamageTarget; + private Limb lastTargetLimb; + private Limb lastAttackLimb; + private double lastSetAttackTargetEventTime; +#endif + public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } public PetBehavior PetBehavior { get; private set; } @@ -2679,11 +2691,19 @@ private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable if (!ActiveAttack.IsRunning) { #if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( - AttackLimb, - damageTarget, - targetLimb, - SimPosition)); + if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval || + damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb) + { + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( + AttackLimb, + damageTarget, + targetLimb, + SimPosition)); + lastSetAttackTargetEventTime = Timing.TotalTime; + lastDamageTarget = damageTarget; + lastAttackLimb = AttackLimb; + lastTargetLimb = targetLimb; + } #else Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 320db7e273..c90f049cb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -46,7 +46,7 @@ partial class HumanAIController : AIController private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; - private bool wasConscious; + private bool wasDead; private bool freezeAI; @@ -201,7 +201,7 @@ public override void Update(float deltaTime) } if (isIncapacitated) { return; } - wasConscious = true; + wasDead = false; respondToAttackTimer -= deltaTime; if (respondToAttackTimer <= 0.0f) @@ -1256,14 +1256,15 @@ public override void OnHealed(Character healer, float healAmount) public override void OnAttacked(Character attacker, AttackResult attackResult) { - // The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs - if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f)) + // If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it + // (But if the character is already dead, and was dead before this attack, don't react) + if (Character.IsDead && wasDead) { return; } + if (Character.IsIncapacitated || Character.Stun > 0.0f) { RespondToAttack(attacker, attackResult); - wasConscious = false; + wasDead = Character.IsDead; return; } - if (Character.IsDead) { return; } if (attacker == null || Character.IsPlayer) { // The player characters need to "respond" to the attack always, because the update loop doesn't run for them. @@ -1467,10 +1468,10 @@ void InformOtherNPCs(float cumulativeDamage = 0) otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) || otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true); if (!isWitnessing) - { - if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID) + { + if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID) { - // Dead or in different team -> cannot report. + // Knocked down or in different team -> cannot report. continue; } if (otherHumanAI.objectiveManager.HasOrders()) @@ -1494,6 +1495,14 @@ void InformOtherNPCs(float cumulativeDamage = 0) continue; } } + else if (!otherCharacter.IsSecurity) + { + //witnessed the attack as non-security, trigger security + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID)) + { + TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing)); + } + } float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced); otherHumanAI.AddCombatObjective(combatMode, attacker, delay); } @@ -1926,12 +1935,12 @@ otherCharacter.AIController is not HumanAIController otherHumanAI || character.IsCriminal = true; character.IsActingOffensively = true; } - if (!TriggerSecurity(otherHumanAI, combatMode)) + if (!TriggerSecurity(otherHumanAI, character, combatMode)) { // Else call the others foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) { - if (!TriggerSecurity(security.AIController as HumanAIController, combatMode)) + if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode)) { // Only alert one guard at a time return; @@ -1941,25 +1950,25 @@ otherCharacter.AIController is not HumanAIController otherHumanAI || } } - bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode) - { - if (humanAI == null) { return false; } - if (!humanAI.Character.IsSecurity) { return false; } - if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } - humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), - onCompleted: () => - { - //if the target is arrested successfully, reset the damage accumulator - foreach (Character anyCharacter in Character.CharacterList) + } + private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode) + { + if (humanAI == null) { return false; } + if (!humanAI.Character.IsSecurity) { return false; } + if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } + humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(), + onCompleted: () => + { + //if the target is arrested successfully, reset the damage accumulator + foreach (Character anyCharacter in Character.CharacterList) + { + if (anyCharacter.AIController is HumanAIController anyAI) { - if (anyCharacter.AIController is HumanAIController anyAI) - { - anyAI.structureDamageAccumulator?.Remove(character); - } + anyAI.structureDamageAccumulator?.Remove(targetCharacter); } - }); - return true; - } + } + }); + return true; } public static void ItemTaken(Item item, Character thief) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 0a57628a2c..e603c4b9c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -133,7 +133,7 @@ protected override void Act(float deltaTime) bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); if (operateExtinguisher) { - character.CursorPosition = fs.Position; + character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine)); Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); if (extinguisherItem.RequireAimToUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 72fa4a8709..002e38441c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -173,6 +173,16 @@ protected override void Act(float deltaTime) if (character.CanInteractWith(Item, out _, checkLinked: false)) { waitTimer += deltaTime; + + //if we're climbing upwards to the item, ensure the character stays within arm's length of it + //without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item, + //then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again + if (character.IsClimbing && + Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength)) + { + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY); + } + if (waitTimer < WaitTimeBeforeRepair) { return; } HumanAIController.FaceTarget(Item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index bfed4ae7ab..e32cc8f42f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -758,6 +758,11 @@ private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPo public float CoolDownTimer { get; set; } public float CurrentRandomCoolDown { get; private set; } public float SecondaryCoolDownTimer { get; set; } + + /// + /// The attack is considered to be running from the moment it starts until the reaches the of the attack, or until the attack lands successfully. + /// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge. + /// public bool IsRunning { get; private set; } public float AfterAttackTimer { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 14a0327179..9337648a7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -997,6 +997,9 @@ public AnimController.Animation Anim public bool IsForceRagdolled; public bool FollowCursor = true; + /// + /// Is the character currently dead, unconscious or paralyzed? + /// public bool IsIncapacitated { get @@ -1006,6 +1009,9 @@ public bool IsIncapacitated } } + /// + /// Is the character dead or below 0 vitality and not able to stay conscious? + /// public bool IsUnconscious { get { return CharacterHealth.IsUnconscious; } @@ -1673,7 +1679,8 @@ protected Character(CharacterPrefab prefab, Vector2 position, string seed, Chara AnimController.FindHull(setInWater: true); if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; } - IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f); + //mass < 35 = husk chimera is the largest vanilla monster that can be contained by default + IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f); CharacterList.Add(this); @@ -2262,7 +2269,10 @@ public void Control(float deltaTime, Camera cam) { Vector2 targetMovement = GetTargetMovement(); AnimController.TargetMovement = targetMovement; - AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; + if (SelectedItem?.GetComponent() is not { ControlCharacterPose: true }) + { + AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; + } } if (AnimController is HumanoidAnimController humanAnimController) @@ -3508,9 +3518,11 @@ public virtual void Update(float deltaTime, Camera cam) UpdateAttackers(deltaTime); - foreach (var characterTalent in characterTalents) + //use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328) + //this way we'll just add them to the end of the list without causing a collection was modified exception + for (int i = 0; i < characterTalents.Count; i++) { - characterTalent.UpdateTalent(deltaTime); + characterTalents[i].UpdateTalent(deltaTime); } if (IsDead) { return; } @@ -5801,6 +5813,12 @@ public bool HasTalent(Identifier identifier) return info.UnlockedTalents.Contains(identifier); } + public bool IsTalentLocked(Identifier talentIdentifier) + { + if (info == null) { return true; } + return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1; + } + public bool HasUnlockedAllTalents() { if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index e9f1d2f0a9..b0282a97ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1022,6 +1022,15 @@ public void ReEnable() partial void UpdateProjSpecific(float deltaTime); +#if SERVER + /// + /// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created. + /// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame) + /// + private const double MinExecuteAttackEventInterval = 0.5f; + private double lastExecuteAttackEventTime; +#endif + private readonly List contactBodies = new List(); /// /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. @@ -1142,9 +1151,13 @@ public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable dama ExecuteAttack(damageTarget, targetLimb, out attackResult); } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData( - attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb, - targetSimPos: attackSimPos)); + if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval) + { + GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData( + attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb, + targetSimPos: attackSimPos)); + lastExecuteAttackEventTime = Timing.TotalTime; + } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index c6d9d476a4..fa5e6af9e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -43,14 +43,14 @@ public override void InitializeAbility(bool addingFirstTime) { foreach (Identifier identifier in option.TalentIdentifiers) { - if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; } + if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; } identifiers.Add(identifier); } foreach (var (_, value) in option.ShowCaseTalents) { - var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet(); + var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet(); if (ids.Count is 0) { continue; } identifiers.Add(value.GetRandomUnsynced()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 5af044625f..8cfe229707 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -131,7 +131,7 @@ public static bool IsViableTalentForCharacter(Character character, Identifier ta if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } - if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; } + if (character.IsTalentLocked(talentIdentifier)) { return false; } if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier)) { @@ -163,16 +163,6 @@ public static bool IsViableTalentForCharacter(Character character, Identifier ta return false; } - public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable characterList) - { - foreach (Character c in characterList) - { - if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; } - } - - return false; - } - public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { List viableTalents = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 17b09b461f..a13ab7c04d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -252,7 +252,7 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, + commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)] [name]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { string[] creatureAndJobNames = @@ -271,7 +271,7 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab }; }, isCheat: true)); - commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character", + commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character", (string[] args) => { if (Character.Controlled == null) @@ -292,9 +292,12 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab }, getValidArgs: () => { - return new string[][] + return new string[][] { - GetItemNameOrIdParams().ToArray() + GetItemNameOrIdParams().ToArray(), + new string[] { "1" }, + new string[] { "100" }, + ItemQualityNames.ToArray() }; }, isCheat: true)); @@ -311,7 +314,7 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab }; }, isCheat: true)); - commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".", + commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition] [quality]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".", (string[] args) => { TrySpawnItem(args); @@ -321,7 +324,10 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab return new string[][] { GetItemNameOrIdParams().ToArray(), - GetSpawnPosParams().ToArray() + GetSpawnPosParams().ToArray(), + new string[] { "1" }, + new string[] { "100" }, + ItemQualityNames.ToArray() }; }, isCheat: true)); @@ -1324,6 +1330,7 @@ void ToggleGodMode(Character targetCharacter) } else { + if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); } NewMessage("Level seed: " + Level.Loaded.Seed); NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); @@ -1333,17 +1340,29 @@ void ToggleGodMode(Character targetCharacter) } },null)); - commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", + commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor] [submarine_team]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", onExecute:(string[] args) => { if (Submarine.MainSub == null) { return; } + Submarine submarineToTeleport = Submarine.MainSub; + if (args.Length > 1) + { + foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic)) + { + if ((sub.Info.Name + "_" + sub.TeamID) == args[1]) + { + submarineToTeleport = sub; + break; + } + } + } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { #if SERVER ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client."); #else - Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); + submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); #endif } else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) @@ -1356,9 +1375,9 @@ void ToggleGodMode(Character targetCharacter) Vector2 pos = Level.Loaded.StartPosition; if (Level.Loaded.StartOutpost != null) { - pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2; + pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2; } - Submarine.MainSub.SetPosition(pos); + submarineToTeleport.SetPosition(pos); } else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { @@ -1370,9 +1389,9 @@ void ToggleGodMode(Character targetCharacter) Vector2 pos = Level.Loaded.EndPosition; if (Level.Loaded.EndOutpost != null) { - pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2; + pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2; } - Submarine.MainSub.SetPosition(pos); + submarineToTeleport.SetPosition(pos); } else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) { @@ -1381,8 +1400,8 @@ void ToggleGodMode(Character targetCharacter) NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); return; } - Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); - var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height); + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport); var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); if (submarineDockingPort != null && outpostDockingPort != null) { @@ -1394,7 +1413,8 @@ void ToggleGodMode(Character targetCharacter) { return new string[][] { - new string[] { "start", "end", "endoutpost", "cursor" } + new string[] { "start", "end", "endoutpost", "cursor" }, + ListAvailableSubmarines() }; }, isCheat: true)); @@ -2569,7 +2589,17 @@ private static string[] ListAvailableLocations() return locationNames.ToArray(); } - + + private static string[] ListAvailableSubmarines() + { + List submarineNames = new(); + foreach (var submarine in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic)) + { + submarineNames.Add(submarine.Info.Name + "_" + submarine.TeamID); + } + return submarineNames.ToArray(); + } + private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition) { if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase)) @@ -2952,7 +2982,7 @@ private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, bool u isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName; } - ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew); + ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter); if (usePreConfiguredNPC) { @@ -2983,6 +3013,14 @@ private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, bool u CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter => { + if (renameCharacter != null) + { + if (renameCharacter.Length > 31) + { + renameCharacter = renameCharacter.Substring(0, 32); + } + newCharacter.Info.Name = renameCharacter; + } SetTeamAndCrew(newCharacter); newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint); newCharacter.GiveIdCardTags(spawnPoint); @@ -3010,7 +3048,7 @@ void SetTeamAndCrew(Character newCharacter) } } - void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew) + void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter) { spawnPosition = Vector2.Zero; spawnPoint = null; @@ -3096,6 +3134,12 @@ void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out C ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}."); } } + argIndex++; + renameCharacter = null; + if (args.Length > argIndex) + { + renameCharacter = args[argIndex]; + } } } @@ -3103,6 +3147,7 @@ private static IEnumerable GetSpawnPosParams() { yield return "cursor"; yield return "inventory"; + yield return "cargo"; #if SERVER if (GameMain.Server != null) @@ -3143,6 +3188,8 @@ private static IEnumerable GetItemNameOrIdParams() } } + private static ImmutableArray ItemQualityNames = ["normal", "good", "excellent", "masterwork"]; + private static void TrySpawnItem(string[] args) { try @@ -3203,7 +3250,8 @@ bool TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex) int amount = 1; int conditionPrc = 100; - + int itemQuality = 0; + if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex)) { switch (spawnLocation) @@ -3223,7 +3271,7 @@ bool TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex) break; default: var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray()); - if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } + if (matchingCharacter != null) { spawnInventory = matchingCharacter.Inventory; } break; } @@ -3232,10 +3280,21 @@ bool TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex) if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } amount = Math.Min(amount, 100); } - + if (args.Length > spawnLocationIndex + 2) { - if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; } + if (!int.TryParse(args[spawnLocationIndex + 2], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; } + } + + if (args.Length > spawnLocationIndex + 3) + { + for (int i = 0; i <= Quality.MaxQuality; i++) + { + if (args[spawnLocationIndex + 3].ToLowerInvariant() == ItemQualityNames[i]) + { + itemQuality = i; + } + } } } @@ -3257,7 +3316,7 @@ bool TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex) } else { - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality); } } else if (spawnInventory != null) @@ -3284,6 +3343,7 @@ void onItemSpawned(Item item) } item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f); + item.Quality = itemQuality; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index bf96cb2865..7da8b324a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -31,12 +31,17 @@ public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement element) Actions = new List(); foreach (var e in element.Elements()) { - if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) + if (e.NameAsIdentifier().Equals("statuseffect")) { DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.", contentPackage: element.ContentPackage); continue; } + else if (e.NameAsIdentifier().Equals(nameof(OnRoundEndAction))) + { + DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". {nameof(OnRoundEndAction)} configured as a sub action. Please configure it as an action at the end of the event.", + contentPackage: element.ContentPackage); + } var action = Instantiate(scriptedEvent, e); if (action != null) { Actions.Add(action); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index d448ad9041..4c1d983a43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -123,7 +123,7 @@ private void TagTraitors() AddTargetPredicate( Tags.Traitor, ScriptedEvent.TargetPredicate.EntityType.Character, - e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); + e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c)); } private void TagNonTraitors() @@ -131,7 +131,7 @@ private void TagNonTraitors() AddTargetPredicate( Tags.NonTraitor, ScriptedEvent.TargetPredicate.EntityType.Character, - e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c)); } private void TagNonTraitorPlayers() @@ -139,7 +139,7 @@ private void TagNonTraitorPlayers() AddTargetPredicate( Tags.NonTraitorPlayer, ScriptedEvent.TargetPredicate.EntityType.Character, - e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c)); } private void TagBots(bool playerCrewOnly) @@ -151,7 +151,8 @@ private void TagBots(bool playerCrewOnly) e is Character c && c.IsBot && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && - (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); + (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) && + CharacterTeamMatches(c)); } private void TagCrew() @@ -171,7 +172,7 @@ private void TagHumansByIdentifier(Identifier identifier) private void TagHumansByTag(Identifier tag) { - AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag))); + AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c))); } private void TagHumansByJobIdentifier(Identifier jobIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 75155b00cb..faf53a4687 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -168,6 +168,9 @@ public bool AttachedByDefault set { attachedByDefault = value; } } +#if DEBUG + [Editable] +#endif [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos @@ -177,8 +180,10 @@ public Vector2 HoldPos } //the distance from the holding characters elbow to center of the physics body of the item protected Vector2 holdPos; - +#if DEBUG + [Editable] +#endif [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")] public Vector2 AimPos @@ -279,6 +284,9 @@ public string MsgWhenDropped /// /// For setting the handle positions using status effects /// +#if DEBUG + [Editable] +#endif public Vector2 Handle1 { get { return ConvertUnits.ToDisplayUnits(handlePos[0]); } @@ -299,6 +307,9 @@ public Vector2 Handle1 /// /// For setting the handle positions using status effects /// +#if DEBUG + [Editable] +#endif public Vector2 Handle2 { get { return ConvertUnits.ToDisplayUnits(handlePos[1]); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index b6a70a3f34..343c92aeaf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -119,7 +119,7 @@ public virtual bool OnPicked(Character picker) return OnPicked(picker, pickDroppedStack: true); } - public virtual bool OnPicked(Character picker, bool pickDroppedStack) + public bool OnPicked(Character picker, bool pickDroppedStack, bool playSound = true) { //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa) if (item.GetComponents().Any()) @@ -156,7 +156,7 @@ public virtual bool OnPicked(Character picker, bool pickDroppedStack) ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } + if (!GameMain.Instance.LoadingScreenOpen && playSound && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } PlaySound(ActionType.OnPicked, picker); #endif if (pickDroppedStack) @@ -164,7 +164,7 @@ public virtual bool OnPicked(Character picker, bool pickDroppedStack) foreach (var droppedItem in droppedStack) { if (droppedItem == item) { continue; } - droppedItem.GetComponent().OnPicked(picker, pickDroppedStack: false); + droppedItem.GetComponent().OnPicked(picker, pickDroppedStack: false, playSound: false); } } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs index 6faa6db36b..ed32f1c273 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs @@ -42,6 +42,9 @@ public float MaxDeconstructTimeMultiplier set; } + [Serialize(true, IsPropertySaveable.No, description: $"Should this item be removed if the linked character is null?")] + public bool RemoveItemIfCharacterNull { get; set; } + public Character? Character { get; private set; } public bool DoesBleed => Character?.DoesBleed == true; @@ -50,6 +53,8 @@ public float MaxDeconstructTimeMultiplier public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element) { + IsActive = true; + #if CLIENT spriteOverrides = element.Elements() .Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride") @@ -58,6 +63,16 @@ public LinkedControllerCharacterComponent(Item item, ContentXElement element) : #endif } + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + + if (RemoveItemIfCharacterNull && GameMain.NetworkMember is not { IsClient: true } && (Character == null || Character.Removed)) + { + Entity.Spawner?.AddEntityToRemoveQueue(Item); + } + } + public void UpdateLinkedCharacter(Character? character) { Character = character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 9e2d344fe4..f83763ae79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -525,7 +525,7 @@ private bool CheckSpawnItem() return true; } - if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab)) + if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(item => item == spawnedItemOnSelected)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index c9ab5d0ad8..b1fb956564 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -345,6 +345,8 @@ void OnCombinedOrRefined() if (targetItem.AllowDeconstruct && allowRemove) { + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f); + //drop all items that are inside the deconstructed item foreach (ItemContainer ic in targetItem.GetComponents()) { @@ -480,6 +482,7 @@ private void ApplyDeconstructionStatusEffects(Item targetItem, ActionType type, // Move items again since the status effect could have spawned additional items in the character inventory MoveItemsFromCharacterToOutput(); + character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); Entity.Spawner?.AddEntityToRemoveQueue(character); }, 0.1f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index d0c8b38ac8..2ae8e0ec81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -732,6 +732,7 @@ private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) } private readonly HashSet usedIngredients = new HashSet(); + private readonly Dictionary ingredientFlexibilityCache = new Dictionary(); public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character) { @@ -786,10 +787,24 @@ private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictiona //maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients usedIngredients.Clear(); - return fabricableItem.RequiredItems.All(requiredItem => + // Items are considered more flexible if they can be used in many different requirements + ingredientFlexibilityCache.Clear(); + foreach (var prefab in fabricableItem.RequiredItems.SelectMany(static r => r.ItemPrefabs)) + { + ingredientFlexibilityCache[prefab] = fabricableItem.RequiredItems.Count(r => r.ItemPrefabs.Contains(prefab)); + } + + return fabricableItem.RequiredItems + // Match the most restrictive requirements to least restrictive first, while we still have items that we can use + .OrderBy(static r => r.ItemPrefabs.Count()) + .ThenByDescending(static requiredItem => requiredItem.Amount) + .All(requiredItem => { int availableItemsAmount = 0; - foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs + // Fill in the least flexible and more abundant items first, so we don't end up using unique items first + .OrderBy(GetItemFlexibility) + .ThenByDescending(GetAvailableItemsCount)) { if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; } @@ -811,6 +826,16 @@ private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictiona return false; }); + + int GetAvailableItemsCount(ItemPrefab itemPrefab) + { + return availableIngredients.TryGetValue(itemPrefab.Identifier, out var list) ? list.Count : 0; + } + + int GetItemFlexibility(ItemPrefab itemPrefab) + { + return ingredientFlexibilityCache[itemPrefab]; + } } private float GetRequiredTime(FabricationRecipe fabricableItem, Character user) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerDistributor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerDistributor.cs index 6086b0b246..f9edf52f0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerDistributor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerDistributor.cs @@ -36,7 +36,7 @@ public string Name } } - public LocalizedString? DisplayName { get; private set; } + public LocalizedString DisplayName { get; private set; } private float supplyRatio = 1f; public float SupplyRatio @@ -80,6 +80,7 @@ public PowerGroup(PowerDistributor distributor, Connection power, XElement? elem SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio); } + DisplayName = TextManager.Get(name).Fallback(name); #if CLIENT CreateGUI(); if (Screen.Selected is not { IsEditor: true }) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index b2ff80ee64..e1accd3ca7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -252,7 +252,7 @@ private void RefreshPhysicsBodySize() { PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy)) { - UserData = item + UserData = this }; } else @@ -260,7 +260,7 @@ private void RefreshPhysicsBodySize() currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f); PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy)) { - UserData = item + UserData = this }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index a52d1d94c5..0d3919b803 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -734,8 +734,8 @@ public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIs [Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")] public bool HideConditionInTooltip { get; set; } - [Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")] - public Identifier UnlockedRecipeInToolTip { get; set; } + [Serialize("", IsPropertySaveable.No, description: "If set, the item's tooltip displays if the given fabrication recipe has been unlocked or not. The actual unlocking of the recipe should be handled in a status effect.")] + public Identifier[] UnlockedRecipeInToolTip { get; set; } //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item //if false, trigger areas define areas that can be used to highlight the item diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 03c2501b92..a5986d447b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -332,17 +332,9 @@ private StateMachine SetupStateMachine() void RunStateUnloaded_OnEnter(State currentState) { Logger.LogMessage("LuaCs unloaded state entered"); - - if (PackageManagementService.IsAnyPackageRunning()) - { - Logger.LogResults(PackageManagementService.StopRunningPackages()); - } - - if (PackageManagementService.IsAnyPackageLoaded()) - { - DisposeLuaCsConfig(); - Logger.LogResults(PackageManagementService.UnloadAllPackages()); - } + Logger.LogResults(PackageManagementService.StopRunningPackages()); + DisposeLuaCsConfig(); + Logger.LogResults(PackageManagementService.UnloadAllPackages()); EventService.Reset(); ConfigService.Reset(); @@ -362,11 +354,7 @@ void RunStateUnloaded_OnEnter(State currentState) void RunStateLoadedNoExec_OnEnter(State currentState) { Logger.LogMessage("LuaCs no execution state entered"); - - if (PackageManagementService.IsAnyPackageRunning()) - { - Logger.LogResults(PackageManagementService.StopRunningPackages()); - } + Logger.LogResults(PackageManagementService.StopRunningPackages()); if (!PackageManagementService.IsAnyPackageLoaded()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs index 9b964cf650..7ff0a6f157 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -256,6 +256,7 @@ public FluentResults.Result AddDependencyPaths(ImmutableArray paths) } } + [MethodImpl(MethodImplOptions.NoInlining)] public Result CompileScriptAssembly([NotNull] string assemblyName, bool compileWithInternalAccess, ImmutableArray syntaxTrees, @@ -348,6 +349,7 @@ public Result CompileScriptAssembly([NotNull] string assemblyName, } } + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, ImmutableArray additionalDependencyPaths) { @@ -434,6 +436,8 @@ FluentResults.Result GenerateExceptionReturn(T exception) where T : } } + + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result GetAssemblyByName(string assemblyName) { if (IsDisposed) @@ -481,6 +485,7 @@ public FluentResults.Result GetAssemblyByName(string assemblyName) } } + [MethodImpl(MethodImplOptions.NoInlining)] public FluentResults.Result> GetTypesInAssemblies() { if (IsDisposed) @@ -501,6 +506,7 @@ public FluentResults.Result> GetTypesInAssemblies() } } + [MethodImpl(MethodImplOptions.NoInlining)] public IEnumerable UnsafeGetTypesInAssemblies() { if (IsDisposed) @@ -529,6 +535,7 @@ public IEnumerable UnsafeGetTypesInAssemblies() } } + [MethodImpl(MethodImplOptions.NoInlining)] public Result GetTypeInAssemblies(string typeName) { if (IsDisposed) @@ -557,13 +564,12 @@ public void Dispose() return; // we don't want to invoke events twice nor cause strong GC handles. IsDisposed = true; this.Unload(); - this.DisposeInternal(); GC.SuppressFinalize(this); } ~AssemblyLoader() { - this.DisposeInternal(); + this.Unload(); } private void OnUnload(AssemblyLoadContext context) @@ -578,8 +584,8 @@ private void OnUnload(AssemblyLoadContext context) Thread.Sleep(1000/Timing.FixedUpdateRate-1); } - var wf = new WeakReference(this); _onUnload?.Invoke(this); + this.DisposeInternal(); } private void DisposeInternal() @@ -590,6 +596,9 @@ private void DisposeInternal() base.Unloading -= OnUnload; this._dependencyResolvers.Clear(); this._loadedAssemblyData.Clear(); + + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + GC.WaitForFullGCComplete(10); } protected override Assembly Load(AssemblyName assemblyName) @@ -658,6 +667,7 @@ private readonly record struct AssemblyData public readonly ImmutableArray Types; public readonly ImmutableDictionary TypesByName; + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyData(Assembly assembly, byte[] assemblyImage) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); @@ -667,6 +677,7 @@ public AssemblyData(Assembly assembly, byte[] assemblyImage) TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); } + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyData(Assembly assembly, string path) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); @@ -694,6 +705,7 @@ public AssemblyOrStringKey(Assembly assembly) HashCode = AssemblyName.GetHashCode(); } + [MethodImpl(MethodImplOptions.NoOptimization)] public AssemblyOrStringKey(string assemblyName) { if (assemblyName.IsNullOrWhiteSpace()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs index 99032700db..9286416113 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs @@ -396,7 +396,7 @@ private void SetupEnvironment(bool enableSandbox) typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), - typeof(ISettingList), + typeof(ISettingList) ]; Dictionary> settingsTable = []; @@ -420,9 +420,9 @@ private void SetupEnvironment(bool enableSandbox) _script.Globals[keyPair.Key] = keyPair.Value; } - UserData.RegisterType(typeof(ISettingRangeBase)); #if CLIENT UserData.RegisterType(typeof(ISettingControl)); + _script.Globals["SettingControl"] = UserData.CreateStatic(typeof(ISettingControl)); #endif new LuaConverters(this).RegisterLuaConverters(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs index 798a9a2492..d675c06cb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs @@ -43,7 +43,9 @@ private void AddToMainMenu(MainMenuScreen screen) { if (mainMenuUIAdded) { return; } - var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red) + var textBlock = new GUITextBlock( + new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, + "", Color.Red, textAlignment: Alignment.TopLeft) { IgnoreLayoutGroups = false }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs index e1dc53c7a4..450d51b810 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs @@ -23,6 +23,7 @@ public sealed partial class ModConfigFileParserService : public ModConfigFileParserService(IStorageService storageService) { _storageService = storageService; + _storageService.UseCaching = false; } #region Dispose diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs index d7278c72b6..8d192aaace 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs @@ -45,6 +45,7 @@ public ModConfigService(IStorageService storageService, #if CLIENT _stylesParserService = stylesParserService; #endif + _storageService.UseCaching = false; } #region Dispose diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs index 14b0d141f7..245fba12cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs @@ -194,7 +194,7 @@ public FluentResults.Result LoadPackagesInfo(ImmutableArray pack IService.CheckDisposed(this); var result = new FluentResults.Result(); - var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first. + var packages2 = packages.OrderBy(pkg => pkg.Name == LuaCsSetup.PackageName ? 0 : 1) // always run lua cs first. .ThenBy(packages.IndexOf) .ToImmutableArray(); @@ -318,7 +318,7 @@ public FluentResults.Result ExecuteLoadedPackages(ImmutableArray // get loading order. Note: packages not in the execution order list will load first. var loadingOrderedPackages = _loadedPackages - .OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first. + .OrderBy(pkg => pkg.Key.Name == LuaCsSetup.PackageName ? 0 : 1) // always run lua cs first. .ThenBy(pkg => executionOrder.IndexOf(pkg.Key)) .ToImmutableArray(); var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray(); @@ -415,7 +415,9 @@ public FluentResults.Result StopRunningPackages() if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty) { +#if DEGUG _logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing."); +#endif return FluentResults.Result.Ok(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs index 30210fc6fd..af431b789f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs @@ -10,6 +10,7 @@ using System.Runtime.Loader; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Xml.Serialization; using Barotrauma.Extensions; using Barotrauma.IO; @@ -88,7 +89,15 @@ public class PluginManagementService : IAssemblyManagementService private ImmutableArray _baseMetadataReferences = ImmutableArray.Empty; private ImmutableArray _baseMetadataReferencesNonPublicized = ImmutableArray.Empty; + + private Thread _backgroundGCCleanupThread = null; + private long _backgroundGCWatchdogTicks = 0; + private static readonly int + GC_TASK_COMPLETION_TIMEOUT = 5000, + GC_BACKGND_MAXITERATIONS = 2, + GC_BACKGND_INTERVAL_MILLIS = 200, + GC_BACKGND_GENERATION_WAIT_MILLIS = 100; private IEnumerable BaseMetadataReferences { @@ -174,6 +183,7 @@ private void UnsafeDisposeResourcesInternal() _pluginInjectorContainer?.Dispose(); _pluginInjectorContainer = null; + ReflectionUtils.ResetCache(); foreach (var loader in _assemblyLoaders) { try @@ -184,14 +194,6 @@ private void UnsafeDisposeResourcesInternal() catch (Exception e) { _logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}"); - if (loader.Value.Assemblies.Any()) - { - foreach (var ass in loader.Value.Assemblies) - { - _logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}"); - ReflectionUtils.RemoveAssemblyFromCache(ass); - } - } } } _assemblyLoaders.Clear(); @@ -222,6 +224,7 @@ public FluentResults.Result Reset() private IEventService _pluginEventService; private Lazy _pluginLuaPatcherService; private Func _consoleCommandServiceFactory; + private readonly IConsoleCommandsService _internalConsoleCommandsService; private ILuaCsInfoProvider _luaCsInfoProvider; private readonly ConcurrentDictionary _assemblyLoaders = new(); private readonly ConcurrentDictionary _pluginPackageLookup = new(); @@ -251,6 +254,21 @@ public PluginManagementService( _pluginLuaPatcherService = pluginLuaPatcherService; _consoleCommandServiceFactory = consoleCommandServiceFactory; _luaCsInfoProvider = luaCsInfoProvider; + _internalConsoleCommandsService = consoleCommandServiceFactory.Invoke(); + + RegisterCommands(_internalConsoleCommandsService); + } + + private void RegisterCommands(IConsoleCommandsService cmdService) + { + cmdService.RegisterCommand("plugin_forcerungc", "Forces the GC to run", cmds => + { + _logger.LogMessage("Forcing GC run."); + Task.Factory.StartNew(async () => + { + await RunGC(true, false); + }); + }); } private ServiceContainer CreatePluginServiceContainer() @@ -317,11 +335,13 @@ void AddTypesFromAssembly(Assembly assembly) } } + [MethodImpl(MethodImplOptions.NoInlining)] public bool TryGetPackageForPlugin(out ContentPackage ownerPackage) { return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage); } + [MethodImpl(MethodImplOptions.NoInlining)] public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true) { @@ -370,6 +390,7 @@ public Type GetType(string typeName, bool isByRefType = false, bool includeInter return null; } + [MethodImpl(MethodImplOptions.NoOptimization)] public FluentResults.Result ActivatePluginInstances(ImmutableArray executionOrder, bool excludeAlreadyRunningPackages = true) { if (executionOrder.IsDefaultOrEmpty) @@ -488,6 +509,7 @@ public FluentResults.Result ActivatePluginInstances(ImmutableArray action) { try @@ -502,7 +524,7 @@ FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action resources) { if (resources.IsDefaultOrEmpty) @@ -705,7 +727,7 @@ IEnumerable GetMetadataReferences(bool useNonPublicizedAssemb { builder.AddRange(BaseMetadataReferencesWithBarotrauma); foreach (var loaderService in _assemblyLoaders - .Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase)) + .Where(asl => !asl.Key.Name.Equals(LuaCsSetup.PackageName, StringComparison.InvariantCultureIgnoreCase)) .ToImmutableArray()) { builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null)); @@ -732,7 +754,7 @@ private string DoSourceCodeTextCompatibilityPass(string sourceCode) .Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList") .Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab"); } - + private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName) { Guard.IsNull(callerAssembly, nameof(callerAssembly)); @@ -805,21 +827,58 @@ private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader) { _eventService?.Value?.PublishEvent(sub => sub.OnAssemblyUnloading(assembly)); } + + _unloadingAssemblyLoaders.Add(loader, loader.OwnerPackage); } - + + [MethodImpl(MethodImplOptions.NoOptimization)] public FluentResults.Result UnloadManagedAssemblies() { using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); - if (_assemblyLoaders.Count == 0) + var results = new FluentResults.Result(); + + if (!_pluginInstances.IsEmpty) { - return FluentResults.Result.Ok(); + foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) + { + try + { + instance.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + continue; + } + } + _pluginInstances.Clear(); } - var results = new FluentResults.Result(); + if (_pluginEventService is not null) + { + _eventService.Value.RemoveDispatcherEventService(_pluginEventService); + try + { + _pluginEventService.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + } + _pluginEventService = null; + } - results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons); + try + { + _pluginInjectorContainer?.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + } + _pluginInjectorContainer = null; ReflectionUtils.ResetCache(); foreach (var loaderService in _assemblyLoaders) @@ -827,7 +886,6 @@ public FluentResults.Result UnloadManagedAssemblies() try { loaderService.Value.Dispose(); - _unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key); } catch (Exception e) { @@ -837,38 +895,7 @@ public FluentResults.Result UnloadManagedAssemblies() _assemblyLoaders.Clear(); _storageService.PurgeCache(); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true); - -#if DEBUG - // Print still loaded assembly load ctx after giving some time - CoroutineManager.Invoke(() => - { - if (!_unloadingAssemblyLoaders.Any()) - { - return; - } - - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("The following ContentPackages have not unloaded their assemblies:"); - - foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray()) - { - sb.AppendLine($"- '{kvp.Value.Name}'"); - } - - - // Use DebugConsole in case logger is null by the time this executes. - if (_logger is null) - { - DebugConsole.LogError(sb.ToString()); - } - else - { - _logger.LogWarning(sb.ToString()); - } - }, 3.0f); -#endif + _pluginPackageLookup.Clear(); // clear native libraries if (_loadedNativeLibraries.Any()) @@ -888,41 +915,109 @@ public FluentResults.Result UnloadManagedAssemblies() _loadedNativeLibraries.Clear(); } + + Task.Factory.StartNew(async () => + { + await RunGC(true, false); + }); return results; } - private FluentResults.Result UnsafeDisposeManagedTypeInstances() + private void SafeLogUnloadingPackages() { - var results = new FluentResults.Result(); - - if (!_pluginInstances.IsEmpty) + if (!_unloadingAssemblyLoaders.Any()) { - foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) + return; + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("The following ContentPackages have not unloaded their assemblies:"); + + foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray()) + { + sb.AppendLine($"- '{kvp.Value.Name}'"); + } + + // Use DebugConsole in case logger is null by the time this executes. + if (_logger is null) + { + DebugConsole.Log(sb.ToString()); + } + else + { + _logger.LogWarning(sb.ToString()); + } + } + + private void GCCleanupTask(TaskCompletionSource completionSuccess) + { + GC.RegisterForFullGCNotification(1, 1); + try + { + for (int iter = 0; iter < GC_BACKGND_MAXITERATIONS; iter++) { - try + int maxGen = GC.MaxGeneration; + for (int currGen = 0; currGen < maxGen; currGen++) { - instance.Dispose(); - } - catch (Exception e) - { - results.WithError(new ExceptionalError(e)); - continue; + GC.Collect(currGen, GCCollectionMode.Forced, false, false); // marking pass + GC.WaitForFullGCComplete(GC_BACKGND_GENERATION_WAIT_MILLIS); + GC.Collect(currGen); // generation cleanup } + Thread.Sleep(GC_BACKGND_INTERVAL_MILLIS); } + completionSuccess.SetResult(true); } - - if (_pluginEventService is not null) + catch (ThreadInterruptedException tie) { - _eventService.Value.RemoveDispatcherEventService(_pluginEventService); - _pluginEventService = null; + completionSuccess.SetResult(false); + } + catch (Exception e) + { + completionSuccess.SetException(e); + } + finally + { + GC.CancelFullGCNotification(); + } + } + + private async Task RunGC(bool logResults, bool runOnMainThread) + { + var gcCompletionSuccess = new TaskCompletionSource(); + if (runOnMainThread) + { + GCCleanupTask(gcCompletionSuccess); + if (logResults) + { + SafeLogUnloadingPackages(); + } + + return; } - _pluginInjectorContainer = null; - - _pluginInstances.Clear(); - _pluginPackageLookup.Clear(); - return results; + var gcThread = new Thread(() => + { + GCCleanupTask(gcCompletionSuccess); + }) { IsBackground = true }; + gcThread.Start(); + + try + { + await gcCompletionSuccess.Task.WaitAsync(TimeSpan.FromMilliseconds(GC_TASK_COMPLETION_TIMEOUT)); + } + catch (TimeoutException te) + { + _logger.LogError($"{nameof(RunGC)}: The GC task thread has timed out."); + gcThread.Interrupt(); + gcThread.Join(); + } + + if (logResults) + { + SafeLogUnloadingPackages(); + } } public Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 71f4b07521..cc3e1691ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -2971,11 +2971,39 @@ bool CalculatePositionOnPath(float checkedDist = 0.0f) string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count); DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" + $" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" + - $" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk"); + $" Total value: {GetTotalLevelResourceValue()} mk"); if (AbyssResources.Count > 0) { DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" + - $" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk"); + $" Total value: {GetTotalAbyssResourceValue()} mk"); + } + + int GetTotalLevelResourceValue() + { + int value = 0; + foreach (var pathPoint in PathPoints) + { + foreach (var clusterLocation in pathPoint.ClusterLocations) + { + foreach (var resource in clusterLocation.Resources) + { + value += resource.Prefab.DefaultPrice?.Price ?? 0; + } + } + } + return value; + } + int GetTotalAbyssResourceValue() + { + int value = 0; + foreach (var clusterLocation in AbyssResources) + { + foreach (var resource in clusterLocation.Resources) + { + value += resource.Prefab.DefaultPrice?.Price ?? 0; + } + } + return value; } #endif @@ -3204,7 +3232,6 @@ int GetMaxResourcesOnEdge(ItemPrefab resourcePrefab, ClusterLocation location, o } } - /// Used by clients to set the rotation for the resources public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable targetCaves = null) { var allValidLocations = GetAllValidClusterLocations(); @@ -5150,6 +5177,7 @@ public override void Remove() renderer.Dispose(); renderer = null; } + backgroundCreatureManager?.Clear(); #endif if (LevelObjectManager != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 6d90bcda08..6c8d8cc6b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -11,7 +11,7 @@ namespace Barotrauma partial class LevelObject : ISpatialEntity, IDamageable, ISerializableEntity { public readonly LevelObjectPrefab Prefab; - public Vector3 Position; + public Vector3 Position { get; set; } public float NetworkUpdateTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 59c14535a3..5d6de4696b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -457,7 +457,7 @@ private void AddObject(LevelObject newObject, Level level) if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); } //add some variance to the Z position to prevent z-fighting //(based on the x and y position of the object, scaled to be visually insignificant) - newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f; + newObject.Position += new Vector3(0, 0, (minX + minY) % 100.0f * 0.00001f); int xStart = (int)Math.Floor(minX / GridSize); int xEnd = (int)Math.Floor(maxX / GridSize); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 637d5d2a8a..945f4bc6e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -348,11 +348,22 @@ public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll)); - price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); + price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplierAffiliated)); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); - price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); + price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplier)); } + + static float GetStatValuesForItem(Character character, ItemPrefab item, StatTypes statType) + { + float statValueSum = 0.0f; + foreach (Identifier itemTag in item.Tags) + { + statValueSum += character.Info.GetSavedStatValue(statType, itemTag); + } + return statValueSum; + } + // Price should never go below 1 mk return Math.Max((int)price, 1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 710397b4a4..6f80c0a4af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -487,7 +487,19 @@ void AugmentDifficultyZoneSettings(int zoneIndex, float? zoneCommonness, int? mi var portrait = new Sprite(subElement, lazyLoad: true); if (portrait != null) { +#if CLIENT + if (!File.Exists(portrait.FilePath)) + { + DebugConsole.ThrowError($"Error in location type \"{Identifier}\": cannot find the location portrait \"{portrait.FilePath}\"."); + } + else + { + portraitsList.Add(portrait); + } +#elif SERVER + // Add without checking the path, since servers don't parse the file path of the sprite portraitsList.Add(portrait); +#endif } break; case "store": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 308234df43..9a1f277db3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -261,15 +261,7 @@ private Map(CampaignMode campaign, XElement element) : this(campaign.Settings) } } - foreach (var endLocation in EndLocations) - { - if (endLocation.Type?.ForceLocationName is { IsEmpty: false }) - { - endLocation.ForceName(endLocation.Type.ForceLocationName); - } - } - - AssignEndLocationLevelData(); + AssignEndLocationLevelData(campaign); //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml) float maxX = Locations.Select(l => l.MapPosition.X).Max(); @@ -973,13 +965,43 @@ void FindConnectedEndLocations(Location currLocation) previousToEndLocation.Connections.Add(endConnection); endLocation.Connections.Add(endConnection); - AssignEndLocationLevelData(); + AssignEndLocationLevelData(campaign); } - private void AssignEndLocationLevelData() + /// + /// Assigns the correct outpost generation parameters to the end locations. Also checks and ensures that all of them are correctly assigned to the end biome, and have a location type that can be generated in the end biome. + /// Strangely shaped custom maps may sometimes generate in a way that there aren't enough locations in the last biome to assign as the end locations, and we may end up choosing locations in the second-to-last biome instead - let's correct that here. + /// + /// + /// + private void AssignEndLocationLevelData(CampaignMode campaign) { + Biome endBiome = Biome.Prefabs.OrderBy(p => p.UintIdentifier).FirstOrDefault(b => b.IsEndBiome) ?? throw new InvalidOperationException("Could not find an end biome to assign to the end locations."); + LocationType endLocationType = + LocationType.Prefabs + .OrderBy(p => p.UintIdentifier) + .FirstOrDefault(IsSuitableEndLocationType) + ?? throw new InvalidOperationException("Could not find an a location type to assign to the end locations."); + + bool IsSuitableEndLocationType(LocationType lt) + { + return lt.AreaSettings.Any(s => + s.Commonness > 0 && + (s.MatchesBiome(endBiome.Identifier) || s.MatchesZone(generationParams.DifficultyZones))); + } + for (int i = 0; i < endLocations.Count; i++) { + if (endLocations[i].Biome != endBiome) + { + endLocations[i].Biome = endBiome; + endLocations[i].LevelData = new LevelData(endLocations[i], this, endLocations[i].LevelData.Difficulty); + } + endLocations[i].ChangeType(campaign: campaign, endLocationType); + if (endLocationType.ForceLocationName is { IsEmpty: false }) + { + endLocations[i].ForceName(endLocationType.ForceLocationName); + } endLocations[i].LevelData.ReassignGenerationParams(Seed); var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); if (outpostParams != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index b96b7f1a76..a2d32c05c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -275,7 +275,7 @@ public float Rotation { CreateStairBodies(); } - else if (HasBody) + else if (Prefab.Body) { CreateSections(); UpdateSections(); @@ -346,7 +346,7 @@ public override Rectangle Rect { Rectangle oldRect = Rect; base.Rect = value; - if (HasBody) + if (Prefab.Body) { CreateSections(); UpdateSections(); @@ -668,7 +668,7 @@ private void CreateSections() { prevSections = Sections.ToArray(); } - if (!HasBody) + if (!Prefab.Body) { if (FlippedX && IsHorizontal) { @@ -685,7 +685,7 @@ private void CreateSections() xsections = 1; ysections = 1; } - Sections = new WallSection[xsections]; + Sections = new WallSection[Math.Max(xsections, ysections)]; } else { @@ -1635,7 +1635,7 @@ public override void FlipX(bool relativeToSub, bool force = false) CreateStairBodies(); } - if (HasBody) + if (Prefab.Body) { CreateSections(); UpdateSections(); @@ -1663,7 +1663,7 @@ public override void FlipY(bool relativeToSub, bool force = false) CreateStairBodies(); } - if (HasBody) + if (Prefab.Body) { CreateSections(); UpdateSections(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index e760df983d..6269e39ac2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -29,6 +29,31 @@ public enum SubmarineClass { Undefined, Scout, Attack, Transport } partial class SubmarineInfo : IDisposable { + public static HashSet SubmarinePathsWithRemoteStorage { get; set; } = []; + + public bool SaveToRemoteStorage + { + get + { + if (FilePath == null) { return false; } + + return SubmarinePathsWithRemoteStorage.Contains(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false)); + } + set + { + if (FilePath == null) { return; } + + if (value) + { + SubmarinePathsWithRemoteStorage.Add(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false)); + } + else + { + SubmarinePathsWithRemoteStorage.Remove(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false)); + } + } + } + private static List savedSubmarines = new List(); public static IEnumerable SavedSubmarines => savedSubmarines; @@ -197,6 +222,8 @@ public string FilePath set; } + public bool IsFromRemoteStorage; + /// /// When enabled, the XML element is not loaded until it is accessed. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 8f22a4da0a..ac59bd3ac3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -562,6 +562,19 @@ public static void Init() IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints")); DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping")); CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials")); + var submarineSettings = currentConfigDoc.Root.GetChildElement("submarinesettings"); + if (submarineSettings != null) + { + SubmarineInfo.SubmarinePathsWithRemoteStorage.Clear(); + foreach (XElement subElement in submarineSettings.Elements("SubmarineWithRemoteStorage")) + { + string path = subElement.GetAttributeString("path", ""); + if (!path.IsNullOrEmpty()) + { + SubmarineInfo.SubmarinePathsWithRemoteStorage.Add(path); + } + } + } #endif } else @@ -689,7 +702,14 @@ public static void SaveCurrentConfig() XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement); CompletedTutorials.Instance.SaveTo(tutorialsElement); - + + XElement submarineSettings = new XElement("submarinesettings"); root.Add(submarineSettings); + + SubmarineInfo.SubmarinePathsWithRemoteStorage.ForEach(path => + { + submarineSettings.Add(new XElement("SubmarineWithRemoteStorage", new XAttribute("path", path))); + }); + XElement keyMappingElement = new XElement("keymapping", currentConfig.KeyMap.Bindings.Select(kvp => new XAttribute(kvp.Key.ToString(), kvp.Value.ToString()))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index e53b6f79d4..3160178a3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1786,7 +1786,7 @@ protected Vector2 GetPosition(Entity entity, IReadOnlyList offset *= item.Scale; if (item.FlippedX) { offset.X *= -1; } if (item.FlippedY) { offset.Y *= -1; } - offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(item.body?.Rotation ?? -item.RotationRad)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/RemoteStorageHelper.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/RemoteStorageHelper.cs new file mode 100644 index 0000000000..e9fbed23ee --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/RemoteStorageHelper.cs @@ -0,0 +1,105 @@ +#nullable enable +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Steamworks; +using System; +using System.Diagnostics.CodeAnalysis; +namespace Barotrauma.Steam; + +internal static partial class RemoteStorageHelper +{ + public static readonly Color SteamColor = Color.DodgerBlue; + public static readonly string DebugPrefix = $"‖color:{SteamColor.ToStringHex()}‖[Remote Storage]‖end‖"; + + /// Attempts to read a file from remote storage into a byte array. + /// The remote file to read from. + /// The bytes read from the remote file. Returns if the operation failed. + /// + /// if the operation was successful.
+ /// if the operation failed. + ///
+ public static bool TryRead(this SteamRemoteStorage.RemoteFile remoteFile, [NotNullWhen(returnValue: true)] out byte[]? bytes, bool logError = true) + { + bytes = SteamRemoteStorage.FileRead(remoteFile.Filename); + bool success = bytes != null; + + if (logError && !success) + { + DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{remoteFile.Filename}\" from remote storage: operation failed."); + } + + return success; + } + + /// Attempts to write a file to remote storage. + /// The path of the local file to read from. + /// The name of the remote file to write to. If , the file name of is used. + /// If , overwriting existing remote files is allowed. + /// + /// if the operation was successful.
+ /// if the operation failed. + ///
+ public static bool TryWrite(string localPath, string? saveAs = null, bool allowOverwrite = false, bool logError = true) + { + string fileName = saveAs ?? Path.GetFileName(localPath); + + if (!allowOverwrite && SteamRemoteStorage.FileExists(fileName)) + { + if (logError) + { + DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: file already exists."); + } + return false; + } + + byte[] data; + + try + { + data = File.ReadAllBytes(localPath); + } + catch (Exception exception) + { + if (logError) + { + DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{fileName}\" while writing to remote storage: {exception}"); + } + return false; + } + + bool success = SteamRemoteStorage.FileWrite(fileName, data); + + if (logError && !success) + { + DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: operation failed."); + } + + return success; + } + + /// Attempts to delete a file from remote storage. + /// The name of the remote file to delete. + /// + /// if the operation was successful.
+ /// if the operation failed. + ///
+ public static bool TryDelete(string fileName, bool logError = true) + { + bool success = SteamRemoteStorage.FileDelete(fileName); + + if (logError && !success) + { + DebugConsole.ThrowError($"{DebugPrefix} Failed to delete file \"{fileName}\" from remote storage: operation failed."); + } + + return success; + } + + /// Checks if a file is stored remotely. + /// The name of the remote file to check. + /// + /// if the file is stored.
+ /// if the file is not stored or the operation failed. + ///
+ public static bool IsStored(string fileName) => SteamRemoteStorage.FileExists(fileName); +} diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 1e029d35c0..9f31a1e871 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,58 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.13.3.1 (Summer Update 2026) +------------------------------------------------------------------------------------------------------------------------------------------------- + +Submarine reworks: +- Humpback, Orca 2, Azimuth, Typhon, and Herja have received their visual and gameplay reworks. +- The command room of the Orca 2 is now located at the center of the submarine. +- Typhon now comes with valves and pipe weakpoints. +- Herja has been upgraded with a power distributor, befitting its high-tech theme. + +Changes and additions: +- Added an option to back up your custom submarines in the Steam Cloud. Can be enabled per-submarine using a checkbox in the sub editor's save dialog. +- Added quality parameter to the give/spawnitem console commands. Allows spawning in items with non-default quality. +- The teleportsub console command has a parameter for choosing which submarine to teleport. +- The spawncharacter console command has a parameter for renaming the spawned character. +- The showseed console command displays the map seed too if used in campaign mode. + +Multiplayer: +- Fixed monster attacks that run over time (e.g. when fractal guardians fire the steam cannon) causing an excessive amount of network usage in multiplayer. +- Fixed an exploit that allowed modified clients to cause other clients to eventually get out of sync and disconnect. +- Fixed inability to drag and drop stacks of items to other players in multiplayer. +- Fixed submarine voting not working in campaign mode. + +Miscellaneous fixes: +- Fixed security (or anyone else) not reacting to attacking stunned/incapacitated characters. +- Fixed the item pickup sound playing multiple times, for every item in a stack you're picking up. +- Fixed the item dropping sound playing twice when dropping an item. +- Fixed being unable to fabricate certain items with specific combinations of materials. Happened in some cases where the recipe accepted multiple different materials as ingredients: the fabricator would got through the requirements in order, and always take the first available items without considering that the item could've been necessary for another, more strict requirement. +- Followup to the "infinite explosion" fix in Summer Update 2025: the previous fix only applied to oxygen tank shelves, but it turned out oxygen generators could also cause the same kind of "explosion loop" where tanks keep exploding and getting refilled by the oxygen generator. +- Fixed "inspirational leader" talent not giving bonus XP like the description says it should. +- Fixed characters being able to drop off platforms while using a periscope (inconsistent with other movement inputs being disabled while on a periscope). +- Fixed bots being unable to extinguish fires in connected subs (e.g. in Remora's drone). +- Fixed parts of the CPR button not being clickable on the health HUD on certain resolutions (was getting blocked by the limb indicators). +- Fixed nuclear shells fabricated with the cheaper recipe variant not giving the "I am become death" achievement. +- Fixed gravity spheres (or more generally, any items with a triggercomponent) taking damage when you cut their trigger area with a plasma cutter, rather than the actual collider of the item. +- Fixed equip buttons being clickable despite the slot being hidden. Meant that when you had equipped an item in your hand, you could click an invisible button at the left side of the inventory where the hand slots would appear. +- Fixed turrets not showing the ammo on the HUD if the ammo is inside the turret itself, rather than a linked loader. +- If one of the unique hireable characters (e.g. Ignatius May, Aunt Doris) dies in the outpost before you hire them, they can no longer appear elsewhere or be hired. +- Fixed cargo scooter lights working, but not draining the battery, when the battery is in another slot than the battery slot. +- Fixed custom interaction messages set on items in the sub editor no longer appearing in-game. +- Allow combining defense bot ammo boxes the same way as other ammo boxes and magazines (merging their ammo together). +- Fixed the character deconstruction bag staying in the deconstructor if you do a level transition while a character is inside the deconstructor. +- Fixed items duplicating if a character gets deconstructed without dying first (possible e.g. by taking advantage of the Miracle Worker talent). +- Fixed crafting blueprint tooltips not showing whether the recipe has been unlocked or not. +- Fixed valves potentially getting stuck in a non-interactable state if the round ends immediately after one's been toggled. + +Modding: +- Fixed "LockedTalents" PermanentStat locking the talent for everyone (not used in any vanilla talent). +- Clients are allowed to use colored text in their chat messages when they have the "chat spam immunity" permission. Colored text was disabled in client-sent chat messages in the previous update due to some ways in which it can be abused, but turns out there were some users relying on this functionality. +- Fixed OnDeconstructed status effect triggering when the item is not deconstructed in some cases (e.g. researching unidentified genetic material without stabilozine). +- Fixed the special locations at the end of the campaign map generating incorrectly on very short maps. +- Fixed status effects using OffsetCopiesEntityTransform not taking physics body rotation into account. +- Fixed TagAction's Team setting being ignored when tagging characters in certain ways (e.g. traitors, non-traitors, bots, human prefab tags). + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.12.7.0 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05b530ad55..d0ad6514ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ You need a version of Visual Studio that supports C# 10 to compile game. If you When installing on Windows, make sure you select ".NET desktop development" during the install process to make sure you have the required features to work with Barotrauma. #### Linux -You will need to install the .NET 6 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux +You will need to install the .NET 8 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux To edit the source code, we recommend using [Visual Studio Code](https://code.visualstudio.com/) with [Microsoft's C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp). diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs index f648a63c51..ffa4e2f16a 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs @@ -114,8 +114,8 @@ public static void RemoveAssemblyFromCache(Assembly assembly) public static void ResetCache() { CachedNonAbstractTypes.Clear(); - CachedNonAbstractTypes.TryAdd(typeof(ReflectionUtils).Assembly, typeof(ReflectionUtils).Assembly.GetTypes().Where(t => !t.IsAbstract).ToImmutableArray()); TypeSearchCache.Clear(); + CachedNonAbstractTypes.TryAdd(typeof(ReflectionUtils).Assembly, typeof(ReflectionUtils).Assembly.GetTypes().Where(t => !t.IsAbstract).ToImmutableArray()); } public static Type? GetType(string nameWithNamespace)