From 14f61af41cca78a3446e259bac63d50ae42de37a Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 10 Apr 2025 11:29:43 +0000 Subject: [PATCH] Release 1.8.6.2 - Calm Before the Storm --- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 5 + .../ClientSource/GUI/GUIComponent.cs | 76 +++++++- .../ClientSource/GUI/GUIImage.cs | 63 +++++-- .../ClientSource/GUI/HRManagerUI.cs | 4 +- .../ClientSource/GUI/LoadingScreen.cs | 6 +- .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 2 - .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 16 +- .../Networking/ServerList/ServerInfo.cs | 2 +- .../Screens/MainMenuScreen/MainMenuScreen.cs | 12 +- .../ClientSource/Screens/NetLobbyScreen.cs | 16 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../GameModes/MultiPlayerCampaign.cs | 9 +- .../ServerSource/Networking/BanList.cs | 6 +- .../Peers/Server/LidgrenServerPeer.cs | 22 ++- .../Primitives/Peers/Server/ServerPeer.cs | 10 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 3 +- .../Characters/AI/EnemyAIController.cs | 169 ++++++++++-------- .../Characters/AI/IndoorsSteeringManager.cs | 114 +++++++++--- .../SharedSource/Characters/AI/PathFinder.cs | 88 +++++---- .../SharedSource/DebugConsole.cs | 6 +- .../Events/EventActions/TriggerAction.cs | 4 +- .../Missions/AbandonedOutpostMission.cs | 3 +- .../SharedSource/Events/Missions/Mission.cs | 2 +- .../SharedSource/Settings/GameSettings.cs | 3 + Barotrauma/BarotraumaShared/changelog.txt | 47 +++++ 32 files changed, 505 insertions(+), 199 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index bc88aaa37..e8b7164e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -911,6 +911,11 @@ namespace Barotrauma PauseMenu.AddToGUIUpdateList(); } + foreach (var openAccordion in GUIComponent.OpenAccordionPopups) + { + openAccordion.AddToGUIUpdateList(order: 1); + } + SocialOverlay.Instance?.AddToGuiUpdateList(); GUIContextMenu.AddActiveToGUIUpdateList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 2abaf9b71..918b67d16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1127,7 +1127,7 @@ namespace Barotrauma component = LoadGUIImage(element, parent); break; case "accordion": - return LoadAccordion(element, parent); + return LoadAccordion(element, parent, openOnTop: element.GetAttributeBool("openontop", false)); case "gridtext": LoadGridText(element, parent); return null; @@ -1147,6 +1147,19 @@ namespace Barotrauma component.toolTip = element.GetAttributeString("tooltip", string.Empty); + GUITextBlock textBlock = component as GUITextBlock ?? (component as GUIButton)?.TextBlock; + if (textBlock != null) + { + if (element.GetAttributeBool("autoscalevertical", false)) + { + textBlock.AutoScaleVertical = true; + } + if (element.GetAttributeBool("autoscalehorizontal", false)) + { + textBlock.AutoScaleHorizontal = true; + } + } + if (element.GetAttributeBool("resizetofitchildren", false)) { Vector2 relativeResizeScale = element.GetAttributeVector2("relativeresizescale", Vector2.One); @@ -1209,6 +1222,10 @@ namespace Barotrauma var maxVersion = new Version(attribute.Value); if (GameMain.Version > maxVersion) { return false; } break; + case "identifierdismissed": + Identifier identifier = element.GetAttributeIdentifier(attribute.Name.ToString(), Identifier.Empty); + if (MainMenuScreen.DismissedNotifications.Contains(identifier)) { return false; } + break; case "buildconfiguration": switch (attribute.Value.ToString().ToLowerInvariant()) { @@ -1286,12 +1303,18 @@ namespace Barotrauma private static GUIButton LoadLink(XElement element, RectTransform parent) { + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + var button = LoadGUIButton(element, parent); string url = element.GetAttributeString("url", ""); button.OnClicked = (btn, userdata) => { try { + if (!identifier.IsEmpty) + { + MainMenuScreen.AddDismissedNotification(identifier); + } if (SteamManager.IsInitialized) { SteamManager.OverlayCustomUrl(url); @@ -1348,6 +1371,8 @@ namespace Barotrauma private static GUIButton LoadGUIButton(XElement element, RectTransform parent) { + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + string style = element.GetAttributeString("style", ""); if (style == "null") { style = null; } @@ -1359,10 +1384,19 @@ namespace Barotrauma element.GetAttributeString("text", ""); text = text.Replace(@"\n", "\n"); - return new GUIButton(RectTransform.Load(element, parent), + var button = new GUIButton(RectTransform.Load(element, parent), text: text, textAlignment: textAlignment, style: style); + button.OnClicked = (btn, userdata) => + { + if (!identifier.IsEmpty) + { + MainMenuScreen.AddDismissedNotification(identifier); + } + return true; + }; + return button; } private static GUIListBox LoadGUIListBox(XElement element, RectTransform parent) @@ -1415,11 +1449,14 @@ namespace Barotrauma { sprite = new Sprite(element); } - - return new GUIImage(RectTransform.Load(element, parent), sprite, scaleToFit: true); + var scaleToFit = element.GetAttributeEnum("scaletofit", GUIImage.ScalingMode.ScaleToFitSmallestExtent); + return new GUIImage(RectTransform.Load(element, parent), sprite, scaleToFit: scaleToFit); } - private static GUIButton LoadAccordion(ContentXElement element, RectTransform parent) + public static readonly List OpenAccordionPopups = new List(); + + /// Should the contents of the accordion be forced to open on top of other UI elements? + private static GUIButton LoadAccordion(ContentXElement element, RectTransform parent, bool openOnTop) { var button = LoadGUIButton(element, parent); List content = new List(); @@ -1431,6 +1468,7 @@ namespace Barotrauma contentElement.Visible = false; contentElement.IgnoreLayoutGroups = true; content.Add(contentElement); + contentElement.UserData = (contentElement.RectTransform.Anchor, contentElement.RectTransform.Pivot); } } button.OnClicked = (btn, userdata) => @@ -1438,8 +1476,32 @@ namespace Barotrauma bool visible = content.FirstOrDefault()?.Visible ?? true; foreach (GUIComponent contentElement in content) { - contentElement.Visible = !visible; - contentElement.IgnoreLayoutGroups = !contentElement.Visible; + if (openOnTop) + { + contentElement.rectTransform.Parent = null; + //the element is drawn in screen space over anything (no longer a child of the original parent), + //we need to calculate the screen space position manually + contentElement.rectTransform.SetPosition(Anchor.TopLeft); + (Anchor anchor, Pivot pivot) = ((Anchor anchor, Pivot pivot))contentElement.UserData; + contentElement.rectTransform.ScreenSpaceOffset = + RectTransform.CalculateAnchorPoint(anchor, button.Rect) + + RectTransform.CalculatePivotOffset(pivot, contentElement.Rect.Size); + contentElement.Visible = true; + if (OpenAccordionPopups.Contains(contentElement)) + { + OpenAccordionPopups.Remove(contentElement); + } + else + { + OpenAccordionPopups.Clear(); + OpenAccordionPopups.Add(contentElement); + } + } + else + { + contentElement.Visible = !visible; + contentElement.IgnoreLayoutGroups = !contentElement.Visible; + } } if (button.Parent is GUILayoutGroup layoutGroup) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 7f599ac11..170e262be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Barotrauma @@ -14,6 +13,24 @@ namespace Barotrauma private static bool loadingTextures; + public enum ScalingMode + { + /// + /// No automatic scaling, the image is drawn using + /// + None, + /// + /// Automatically scales the image so it fits the smallest extent of the component + /// (leaving empty space around the image if its aspect ratio is different than that of the GUIImage) + /// + ScaleToFitSmallestExtent, + /// + /// Automatically scales the image so it fits the largest extent of the component, + /// cutting out the parts that go outside the bounds of the component. + /// + ScaleToFitLargestExtent, + } + public static bool LoadingTextures { get @@ -30,7 +47,7 @@ namespace Barotrauma private bool crop; - private readonly bool scaleToFit; + private readonly ScalingMode scaleToFit; private bool lazyLoaded, loading; @@ -89,7 +106,7 @@ namespace Barotrauma sprite = value; sourceRect = value == null ? Rectangle.Empty : value.SourceRect; origin = value == null ? Vector2.Zero : value.size / 2; - if (scaleToFit) { RecalculateScale(); } + if (scaleToFit != ScalingMode.None) { RecalculateScale(); } } } @@ -97,17 +114,27 @@ namespace Barotrauma public ComponentState? OverrideState = null; - public GUIImage(RectTransform rectT, string style, bool scaleToFit = false) + public GUIImage(RectTransform rectT, string style, bool scaleToFit) + : this(rectT, null, null, scaleToFit ? ScalingMode.ScaleToFitSmallestExtent : ScalingMode.None, style) + { + } + + public GUIImage(RectTransform rectT, string style, ScalingMode scaleToFit = ScalingMode.None) : this(rectT, null, null, scaleToFit, style) { } - public GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect = null, bool scaleToFit = false) + public GUIImage(RectTransform rectT, Sprite sprite, bool scaleToFit, Rectangle? sourceRect = null) + : this(rectT, sprite, sourceRect, scaleToFit ? ScalingMode.ScaleToFitSmallestExtent : ScalingMode.None, null) + { + } + + public GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect = null, ScalingMode scaleToFit = ScalingMode.None) : this(rectT, sprite, sourceRect, scaleToFit, null) { } - private GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect, bool scaleToFit, string style) : base(style, rectT) + private GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect, ScalingMode scaleToFit, string style) : base(style, rectT) { this.scaleToFit = scaleToFit; Sprite = sprite; @@ -123,7 +150,7 @@ namespace Barotrauma { color = hoverColor = selectedColor = pressedColor = disabledColor = Color.White; } - if (!scaleToFit) + if (scaleToFit == ScalingMode.None) { Scale = 1.0f; } @@ -176,9 +203,11 @@ namespace Barotrauma Color currentColor = GetColor(State); - if (BlendState != null) + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + if (BlendState != null || scaleToFit == ScalingMode.ScaleToFitLargestExtent) { spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Rect); spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } @@ -205,9 +234,10 @@ namespace Barotrauma Scale, SpriteEffects, 0.0f); } - if (BlendState != null) + if (BlendState != null || scaleToFit == ScalingMode.ScaleToFitLargestExtent) { spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } } @@ -219,9 +249,18 @@ namespace Barotrauma sourceRect = sprite.SourceRect; } - Scale = sprite == null || sprite.SourceRect.Width == 0 || sprite.SourceRect.Height == 0 ? - 1.0f : - Math.Min(RectTransform.Rect.Width / (float)sprite.SourceRect.Width, RectTransform.Rect.Height / (float)sprite.SourceRect.Height); + if (sprite == null || sprite.SourceRect.Width == 0 || sprite.SourceRect.Height == 0) + { + Scale = 1.0f; + } + else if (scaleToFit == ScalingMode.ScaleToFitLargestExtent) + { + Scale = Math.Max(RectTransform.Rect.Width / (float)sprite.SourceRect.Width, RectTransform.Rect.Height / (float)sprite.SourceRect.Height); + } + else + { + Scale = Math.Min(RectTransform.Rect.Width / (float)sprite.SourceRect.Width, RectTransform.Rect.Height / (float)sprite.SourceRect.Height); + } } private async Task LoadTextureAsync() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs index de339e07e..cff44b879 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs @@ -749,8 +749,8 @@ namespace Barotrauma /// private bool ActiveServiceFull() { - return (PendingHires.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + campaign.CrewManager.GetCharacterInfos().Count()) - >= CrewManager.MaxCrewSize; + int pendingHireCount = PendingHires?.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) ?? 0; + return pendingHireCount + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize; } private bool EnoughReputationToHire(CharacterInfo characterInfo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 52749f42b..3231c2c8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -25,8 +25,6 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; - private bool mirrorBackground; - public struct PendingSplashScreen { public string Filename; @@ -108,8 +106,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea, - spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea); overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); double noiseT = Timing.TotalTime * 0.02f; @@ -386,7 +383,6 @@ namespace Barotrauma { currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); } - mirrorBackground = Rand.Range(0.0f, 1.0f) < 0.5f; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index dac7ac2d0..214f0e2cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -255,7 +255,7 @@ namespace Barotrauma pageIndicators = new GUIImage[pageCount]; for (int i = 0; i < pageCount; i++) { - pageIndicators[i] = new GUIImage(new RectTransform(indicatorSize, pageIndicatorHolder.RectTransform) { AbsoluteOffset = new Point(xPos, yPos) }, pageIndicator, null, true); + pageIndicators[i] = new GUIImage(new RectTransform(indicatorSize, pageIndicatorHolder.RectTransform) { AbsoluteOffset = new Point(xPos, yPos) }, pageIndicator, scaleToFit: true); xPos += indicatorSize.X + HUDLayoutSettings.Padding; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index b57c52277..dc2913de1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -799,8 +799,6 @@ namespace Barotrauma customizeTabOpen = !hasUpgradeModules && hasSwappableItems; - customizeTabOpen = false; - GUIComponent[] categoryFrames = GetFrames(category); foreach (GUIComponent itemFrame in itemPreviews.Values) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index ceab34567..008011bf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -467,7 +467,7 @@ namespace Barotrauma }; if (icon != null) { - missionIcon = new GUIImage(new RectTransform(new Point(iconSize), content.RectTransform), icon, null, true) + missionIcon = new GUIImage(new RectTransform(new Point(iconSize), content.RectTransform), icon, scaleToFit: true) { Color = iconColor, HoverColor = iconColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index f6dcd08c4..b54aaef03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -86,6 +86,9 @@ namespace Barotrauma.Items.Components [Serialize(true, IsPropertySaveable.No)] public bool ShowAvailableOnlyTickBox { get; set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowCategoryButtons { get; set; } + public override bool RecreateGUIOnResolutionChange => true; protected override void OnResolutionChanged() @@ -120,7 +123,7 @@ namespace Barotrauma.Items.Components itemCategoryButtons.Clear(); //only create category buttons if there's more than one category in addition to "All" - if (itemCategories.Count > 2) + if (ShowCategoryButtons && itemCategories.Count > 2) { // === Item category buttons === var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.05f, 1.0f), innerArea.RectTransform)) @@ -887,7 +890,7 @@ namespace Barotrauma.Items.Components itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), - color: targetItem.TargetItem.InventoryIconColor * 0.4f, + color: Color.Lerp(targetItem.TargetItem.InventoryIconColor, Color.TransparentBlack, 0.5f), scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f); } } @@ -1259,6 +1262,15 @@ namespace Barotrauma.Items.Components if (!IsActive) { + if (outputContainer != null && outputContainer.Inventory.AllItems.Any()) + { + if (outputContainer.Inventory.visualSlots is { } visualSlots && visualSlots.Any() && + visualSlots[0].HighlightTimer <= 0.0f) + { + visualSlots[0].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f); + } + } + if (selectedItem != null && displayingForCharacter != character) { //reselect to recreate the info based on the new user's skills diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 0fa5a05b4..55a748c19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -180,7 +180,7 @@ namespace Barotrauma.Networking { float playStyleBannerAspectRatio = (float)playStyleBannerSprite.SourceRect.Width / (float)playStyleBannerSprite.SourceRect.Height; playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), frame.RectTransform, scaleBasis: ScaleBasis.BothWidth), - playStyleBannerSprite, null, true); + playStyleBannerSprite, scaleToFit: true); playStyleBannerColor = playStyleBannerSprite.SourceElement.GetAttributeColor("bannercolor", Color.Black); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 228053291..61f536f38 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -23,6 +23,8 @@ namespace Barotrauma { sealed class MainMenuScreen : Screen { + public static HashSet DismissedNotifications = new HashSet(); + private enum Tab { NewGame = 0, @@ -588,6 +590,12 @@ namespace Barotrauma SelectTab(Tab.Empty); } + public static void AddDismissedNotification(Identifier id) + { + DismissedNotifications.Add(id); + GameSettings.SaveCurrentConfig(); + } + private void SetMenuTabPositioning() { foreach (GUIFrame menuTab in menuTabs.Values) @@ -616,7 +624,7 @@ namespace Barotrauma }; var tutorialPreview = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), tutorialContent.RectTransform)) { RelativeSpacing = 0.05f, Stretch = true }; var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "InnerFrame"); - tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: true); + tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: GUIImage.ScalingMode.ScaleToFitSmallestExtent); var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) @@ -1382,7 +1390,7 @@ namespace Barotrauma var playstyleContainer = new GUIFrame(new RectTransform(new Vector2(1.35f, 0.1f), parent.RectTransform), style: null, color: Color.Black); playstyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 0.1f), playstyleContainer.RectTransform), - GUIStyle.GetComponentStyle($"PlayStyleBanner.{PlayStyle.Serious}").GetSprite(GUIComponent.ComponentState.None), scaleToFit: true) + GUIStyle.GetComponentStyle($"PlayStyleBanner.{PlayStyle.Serious}").GetSprite(GUIComponent.ComponentState.None), scaleToFit: GUIImage.ScalingMode.ScaleToFitSmallestExtent) { UserData = PlayStyle.Serious }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index ee2702b79..c542c7ccb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -477,10 +477,20 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(5) }; - Favorite = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), + + var topRightContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight), + isHorizontal: true, childAnchor: Anchor.TopRight) + { + AbsoluteSpacing = GUI.IntScale(5), + CanBeFocused = true + }; + + SettingsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), topRightContainer.RectTransform, Anchor.TopRight), + TextManager.Get("ServerSettingsButton"), style: "GUIButtonFreeScale"); + + Favorite = new GUITickBox(new RectTransform(Vector2.One, topRightContainer.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), "", null, "GUIServerListFavoriteTickBox") { - IgnoreLayoutGroups = true, Selected = false, ToolTip = TextManager.Get("addtofavorites"), OnSelected = (tickbox) => @@ -500,8 +510,6 @@ namespace Barotrauma } }; - SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.4f), serverInfoContent.RectTransform, Anchor.TopRight), - TextManager.Get("ServerSettingsButton"), style: "GUIButtonFreeScale"); } private void CreateServerMessagePopup(string serverName, string message) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5f4ec6914..a1cc9fcc2 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7634a5a52..0c5cf4e05 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 5637e302c..ff49013a8 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 34aa6e924..12339eb8e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index f162ce07f..4646dea45 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2a4e4aa6c..da7fa0676 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1341,12 +1341,13 @@ namespace Barotrauma } match.BotStatus = pendingToReserveBench[i++] ? BotStatus.PendingHireToReserveBench : BotStatus.PendingHireToActiveService; + if (match.BotStatus == BotStatus.PendingHireToActiveService) + { + //can't add more bots to active service is max has been reached + if (pendingHireInfos.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { continue; } + } pendingHireInfos.Add(match); - if (pendingHireInfos.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) - { - break; - } } location.HireManager.PendingHires = pendingHireInfos; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 300853f29..c243d607f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -195,7 +195,11 @@ namespace Barotrauma.Networking public void BanPlayer(string name, Either addressOrAccountId, string reason, TimeSpan? duration) { - if (addressOrAccountId.TryGet(out Address address) && address.IsLocalHost) { return; } + if (addressOrAccountId.TryGet(out Address address) && address.IsLocalHost) + { + DebugConsole.AddWarning($"Cannot ban localhost ({address.StringRepresentation})"); + return; + } var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId); if (existingBan != null) { bannedPlayers.Remove(existingBan); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 6a40cdd8c..81083c420 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -200,11 +200,11 @@ namespace Barotrauma.Networking } PendingClient? pendingClient = pendingClients.Find(c => c.Connection.NetConnection == inc.SenderConnection); - if (pendingClient is null) { pendingClient = new PendingClient(new LidgrenConnection(inc.SenderConnection)); pendingClients.Add(pendingClient); + GameServer.Log($"Incoming connection from {pendingClient.Connection.NetConnection?.RemoteEndPoint?.ToString() ?? "null"}.", ServerLog.MessageType.ServerMessage); } inc.SenderConnection.Approve(); @@ -218,7 +218,25 @@ namespace Barotrauma.Networking IReadMessage inc = lidgrenMsg.ToReadMessage(); - var (_, packetHeader, initialization) = INetSerializableStruct.Read(inc); + PeerPacketHeaders peerPacketHeaders = default; + try + { + peerPacketHeaders = INetSerializableStruct.Read(inc); + } + catch + { + if (pendingClient != null) + { + //pending (= not yet authenticated) client sent malformed data, immediately ban them so they can't use this for spamming + GameServer.Log($"Received an invalid connection attempt from {pendingClient.Connection.NetConnection?.RemoteEndPoint?.ToString() ?? "null"}. Banning the IP.", ServerLog.MessageType.DoSProtection); + serverSettings.BanList.BanPlayer(name: "Unknown", endpoint: pendingClient.Connection.Endpoint, reason: "Invalid connection attempt", duration: null); + } + else + { + throw; + } + } + var (_, packetHeader, initialization) = peerPacketHeaders; if (packetHeader.IsConnectionInitializationStep() && pendingClient != null && initialization.HasValue) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 6a19c93ad..e8a0e2e2e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Networking public ConnectionInitialization InitializationStep; public double UpdateTime; public double TimeOut; - public int Retries; + public int PasswordRetries; public Int32? PasswordSalt; public bool AuthSessionStarted; @@ -52,7 +52,7 @@ namespace Barotrauma.Networking OwnerKey = Option.None; Connection = conn; InitializationStep = ConnectionInitialization.AuthInfoAndVersion; - Retries = 0; + PasswordRetries = 0; PasswordSalt = null; UpdateTime = Timing.TotalTime + Timing.Step * 3.0; TimeOut = NetworkConnection.TimeoutThreshold; @@ -156,8 +156,8 @@ namespace Barotrauma.Networking } else { - pendingClient.Retries++; - if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan) + pendingClient.PasswordRetries++; + if (serverSettings.BanAfterWrongPassword && pendingClient.PasswordRetries > serverSettings.MaxPasswordRetriesBeforeBan) { const string banMsg = "Failed to enter correct password too many times"; BanPendingClient(pendingClient, banMsg, null); @@ -281,7 +281,7 @@ namespace Barotrauma.Networking structToSend = new ServerPeerPasswordPacket { Salt = GetSalt(pendingClient), - RetriesLeft = Option.Some(pendingClient.Retries) + RetriesLeft = Option.Some(pendingClient.PasswordRetries) }; static Option GetSalt(PendingClient client) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4b23b78f2..e4d212b11 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.8.4.1 + 1.8.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 053ef35c7..31db3cded 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -213,7 +213,8 @@ namespace Barotrauma if (targetHull == null) { return false; } if (maxDistance > 0) { - if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; } + // Too far from the gap. + if (Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) > maxDistance * maxDistance) { return false; } } if (SteeringManager is IndoorsSteeringManager pathSteering) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 5caa4e074..5e7fd8e8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1128,9 +1128,9 @@ namespace Barotrauma searchingNewHull = false; } } - if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable) + if (patrolTarget != null && pathSteering.CurrentPath is { Finished: false, Unreachable: false }) { - PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n)); + pathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: PatrolNodeFilter); return; } } @@ -1570,22 +1570,22 @@ namespace Barotrauma Vector2 toTarget = attackWorldPos - attackLimbPos; Vector2 toTargetOffset = toTarget; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute - if (wallTarget != null && Character.Submarine == null) + if (Character.Submarine == null) { - if (wallTarget.Structure.Submarine != null) + if (wallTarget != null) { - Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); + if (wallTarget.Structure.Submarine != null) + { + Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); + toTargetOffset += margin; + } + } + else if (targetCharacter != null) + { + Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); toTargetOffset += margin; } - } - else if (targetCharacter != null) - { - Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); - toTargetOffset += margin; - } - else if (SelectedAiTarget.Entity is MapEntity e) - { - if (e.Submarine != null) + else if (SelectedAiTarget.Entity is MapEntity { Submarine: not null } e) { Vector2 margin = CalculateMargin(e.Submarine.Velocity); toTargetOffset += margin; @@ -1666,12 +1666,15 @@ namespace Barotrauma // Crouch if the target is down (only humanoids), so that we can reach it. if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) { - if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) + if (targetCharacter?.CurrentHull is Hull targetHull && targetHull == Character.CurrentHull && toTarget.Y < 0) { - humanoidAnimController.Crouch(); + if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) + { + humanoidAnimController.Crouch(); + } } } - + if (canAttack) { if (AttackLimb.attack.Ranged) @@ -1706,6 +1709,10 @@ namespace Barotrauma } } } + else if (wallTarget == null && Character.CurrentHull != null && targetCharacter != null) + { + canAttack = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall) == null; + } } } Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; @@ -1720,6 +1727,8 @@ namespace Barotrauma State = AIState.Idle; return; } + // Note that this returns null when we don't currently use IndoorsSteeringManager, even if we are able to path! + // -> Don't use PathSteering, because that returns the reference even if we currently don't use it. var pathSteering = SteeringManager as IndoorsSteeringManager; if (AttackLimb != null && AttackLimb.attack.Retreat) { @@ -1727,58 +1736,71 @@ namespace Barotrauma } else { - Vector2 steerPos = attackSimPos; - if (!Character.AnimController.SimplePhysicsEnabled) - { - // Offset so that we don't overshoot the movement - Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; - steerPos += offset; - } if (pathSteering != null) { - if (pathSteering.CurrentPath != null) + // Attack doors + if (canAttackDoors && HasValidPath()) { - // Attack doors - if (canAttackDoors) + // If the target is in the same hull, there shouldn't be any doors blocking the path + if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { - // If the target is in the same hull, there shouldn't be any doors blocking the path - if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) + var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; + if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character))) { - var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character))) + if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) { - if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) - { - SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); - State = AIState.Attack; - return; - } + SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); + State = AIState.Attack; + return; } } } - // When pursuing, we don't want to pursue too close - float max = 300; - float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; - if (!canAttack || distance > margin) + } + // When pursuing, we don't want to pursue too close + float max = 300; + float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; + if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap) + { + // Steer towards the target if in the same room and swimming + // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering. + if (Character.CurrentHull != null && + Character.Submarine != null && !Character.Submarine.Info.IsRuin && + (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) { - // Steer towards the target if in the same room and swimming - // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering. - if (Character.CurrentHull != null && - Character.Submarine != null && !Character.Submarine.Info.IsRuin && - (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && - targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos)); + } + else + { + Func nodeFilter = null; + float outsideNodePenalty = 0; + if (Character.CurrentHull != null && Character.IsInPlayerSub) { - Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); + // Prefer not to path back outside, if we are inside player sub. + // Fixes monsters sometimes pathing from the airlock to another airlock on the other side of the sub, because the path is technically cheaper than the path through the interiors. + outsideNodePenalty = 50; } - else - { - pathSteering.SteeringSeek(steerPos, weight: 2, - minGapWidth: minGapSize, - startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), - checkVisiblity: true); + pathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, + minGapWidth: minGapSize, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), + nodeFilter: nodeFilter, + checkVisiblity: true, + outsideNodePenalty: outsideNodePenalty); - if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + if (pathSteering.CurrentPath != null) + { + if (pathSteering.IsPathDirty) + { + if (Character.CurrentHull is Hull hull && hull.ConnectedGaps.Any(static g => !g.IsRoomToRoom && g.Open >= 1.0f && g.ConnectedDoor != null)) + { + // Reset in the airlock, because otherwise the character may be too slow to change the steering and keep moving outside. + SteeringManager.Reset(); + } + // Steer towards the target + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + } + else if (pathSteering.CurrentPath.Unreachable) { State = AIState.Idle; IgnoreTarget(SelectedAiTarget); @@ -1787,39 +1809,42 @@ namespace Barotrauma } } } - else if (!IsTryingToSteerThroughGap) + } + else if (!IsTryingToSteerThroughGap) + { + if (AttackLimb.attack.Ranged) { - if (AttackLimb.attack.Ranged) + float dir = Character.AnimController.Dir; + if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) { - float dir = Character.AnimController.Dir; - if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) - { - SteeringManager.Reset(); - } - else - { - // Too close - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); - } + SteeringManager.Reset(); } else { - // Close enough - SteeringManager.Reset(); + // Too close + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } } else { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + // Close enough + SteeringManager.Reset(); } } else { - pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize); + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); } } else { + Vector2 steerPos = attackSimPos; + if (!Character.AnimController.SimplePhysicsEnabled) + { + // Offset so that we don't overshoot the movement + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + steerPos += offset; + } // Sweeping and circling doesn't work well inside if (Character.CurrentHull == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index b9c0d75f3..60ad0799d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -119,10 +119,13 @@ namespace Barotrauma steering += base.DoSteeringSeek(targetSimPos, weight); } - public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) + /// + /// + /// Additional cost applied to outside nodes. When the character is inside an extra cost of 100 is also automatically added for outside nodes, unless the character is protected from the pressure. Used for example to prevent monsters from preferring outside nodes when they already are inside. + public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true, float outsideNodePenalty = 0) { // Have to use a variable here or resetting doesn't work. - Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity, outsideNodePenalty); steering += addition; } @@ -139,9 +142,9 @@ namespace Barotrauma return null; } - private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true, float outsideNodePenalty = 0) { - bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null; + bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.IsAtEndNode || currentPath.CurrentNode == null; if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) { // If the target has moved, we need a new path. @@ -182,7 +185,7 @@ namespace Barotrauma Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; - var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); + var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility, outsideNodePenalty); bool useNewPath = needsNewPath; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { @@ -792,7 +795,7 @@ namespace Barotrauma float? penalty = GetSingleNodePenalty(nextNode); if (penalty == null) { return null; } bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; - if (!character.CanClimb) + if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) { if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) || (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up @@ -888,6 +891,7 @@ namespace Barotrauma return penalty; } + // TODO: Long and complex. Consider refactoring. public static float smallRoomSize = 500; public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true) { @@ -915,36 +919,92 @@ namespace Barotrauma } else { - float leftDist = character.Position.X - currentHull.Rect.X; - float rightDist = currentHull.Rect.Right - character.Position.X; - if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance) + bool isVerySmallRoom = roomWidth < smallRoomSize; + Hull nextRoom = null; + if (!stayStillInTightSpace && isVerySmallRoom) { - if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2) + float closestDistance = 0; + // Try to steer to the next room + foreach (Gap gap in currentHull.ConnectedGaps) { - SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist)); - return; - } - else if (stayStillInTightSpace) - { - Reset(); - return; + if (gap.Open < 1.0f) { continue; } + float feetPos = ConvertUnits.ToDisplayUnits(character.AnimController.FloorY); + float gapTop = gap.Rect.Y; + float gapBottom = gap.Rect.Y - gap.Rect.Height; + const float margin = 25; + if (character.Position.Y > gapTop || feetPos < gapBottom - margin) + { + // If the character is above or below the gap, they can't walk through it. + continue; + } + Hull room = null; + foreach (var entity in gap.linkedTo) + { + if (entity is not Hull hull) { continue; } + if (hull.Submarine != character.Submarine) { continue; } + if (hull.Rect.Width < smallRoomSize) { continue; } + if (hull != currentHull) + { + room = hull; + break; + } + } + if (room == null) { continue; } + Vector2 toGap = gap.Position - character.Position; + float distance = Math.Abs(toGap.X); + if (nextRoom == null || distance < closestDistance) + { + closestDistance = distance; + nextRoom = room; + } } } - if (leftDist < wallAvoidDistance) + if (nextRoom != null) { - float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance; - SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); + float toNextRoom = nextRoom.Position.X - character.Position.X; + SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(toNextRoom)); WanderAngle = 0.0f; } - else if (rightDist < wallAvoidDistance) - { - float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance; - SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); - WanderAngle = MathHelper.Pi; - } else { - wander = true; + if (!stayStillInTightSpace && isVerySmallRoom) + { + // Reset regardless, because moving inside the room would look glitchy. + Reset(); + return; + } + float leftDist = character.Position.X - currentHull.Rect.X; + float rightDist = currentHull.Rect.Right - character.Position.X; + if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance) + { + float diff = rightDist - leftDist; + if (Math.Abs(diff) > wallAvoidDistance / 2) + { + SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(diff)); + return; + } + else if (stayStillInTightSpace) + { + Reset(); + return; + } + } + if (leftDist < wallAvoidDistance) + { + float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance; + SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); + WanderAngle = 0.0f; + } + else if (rightDist < wallAvoidDistance) + { + float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance; + SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); + WanderAngle = MathHelper.Pi; + } + else + { + wander = true; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 338568707..2b43cbefb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -170,7 +170,7 @@ namespace Barotrauma private readonly List sortedNodes; - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true, float outsideNodePenalty = 0) { foreach (PathNode node in nodes) { @@ -325,7 +325,12 @@ namespace Barotrauma #endif return new SteeringPath(true); } - return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); + float outsideNodeCostPenalty = outsideNodePenalty; + if (ApplyPenaltyToOutsideNodes) + { + outsideNodeCostPenalty += 100; + } + return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize, outsideNodeCostPenalty); bool IsValidStartNode(PathNode node) => IsValidNode(node, (isCharacter, start), startNodeFilter); @@ -356,7 +361,7 @@ namespace Barotrauma } } - private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0) + private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0f, float outsideNodePenalty = 0f) { if (start == end) { @@ -398,50 +403,65 @@ namespace Barotrauma for (int i = 0; i < currNode.connections.Count; i++) { PathNode nextNode = currNode.connections[i]; - - //a node that hasn't been searched yet - if (nextNode.state == 0) - { - nextNode.H = Vector2.Distance(nextNode.Position, end.Position); - float penalty = 0.0f; - if (GetNodePenalty != null) + switch (nextNode.state) + { + //a node that hasn't been searched yet + case 0: { - float? nodePenalty = GetNodePenalty(currNode, nextNode); - if (nodePenalty == null) + nextNode.H = Vector2.Distance(nextNode.Position, end.Position); + float cost = CalculateNodeCost(); + if (cost < float.PositiveInfinity) { - nextNode.state = -1; - continue; + nextNode.G = cost; + nextNode.F = nextNode.G + nextNode.H; + nextNode.Parent = currNode; + nextNode.state = 1; } - penalty = nodePenalty.Value; + else + { + // Set searched and invalid. + nextNode.state = -1; + } + break; + } + //node that has been searched + case 1 or -1: + { + float tempG = CalculateNodeCost(); + //only use if this new route is better than the + //route the node was a part of + if (tempG < nextNode.G) + { + nextNode.G = tempG; + nextNode.F = nextNode.G + nextNode.H; + nextNode.Parent = currNode; + nextNode.state = 1; + } + break; } - - nextNode.G = currNode.G + currNode.distances[i] + penalty; - nextNode.F = nextNode.G + nextNode.H; - nextNode.Parent = currNode; - nextNode.state = 1; } - //node that has been searched - else if (nextNode.state == 1 || nextNode.state == -1) + + float CalculateNodeCost() { - float tempG = currNode.G + currNode.distances[i]; - + float penalty = 0f; if (GetNodePenalty != null) { float? nodePenalty = GetNodePenalty(currNode, nextNode); - if (nodePenalty == null) { continue; } - tempG += nodePenalty.Value; + if (nodePenalty.HasValue) + { + penalty += nodePenalty.Value; + } + else + { + return float.PositiveInfinity; + } } - - //only use if this new route is better than the - //route the node was a part of - if (tempG < nextNode.G) + if (currNode.Waypoint.CurrentHull == null) { - nextNode.G = tempG; - nextNode.F = nextNode.G + nextNode.H; - nextNode.Parent = currNode; - nextNode.state = 1; + penalty += outsideNodePenalty; } + return currNode.G + currNode.distances[i] + penalty; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 58128afba..19b4c12fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2926,13 +2926,13 @@ namespace Barotrauma Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { newCharacter.HumanPrefab = humanPrefab; + AddToCrew(newCharacter); humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPoint); humanPrefab.InitializeCharacter(newCharacter); #if SERVER newCharacter.LoadTalents(); GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); #endif - PostSpawnHuman(newCharacter); }); } } @@ -2942,10 +2942,10 @@ namespace Barotrauma CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter => { + AddToCrew(newCharacter); newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint); newCharacter.GiveIdCardTags(spawnPoint); newCharacter.Info.StartItemsGiven = true; - PostSpawnHuman(newCharacter); }); } else if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) is { } prefab) @@ -2953,7 +2953,7 @@ namespace Barotrauma Entity.Spawner.AddCharacterToSpawnQueue(args[0].ToIdentifier(), spawnPosition, prefab.HasCharacterInfo ? new CharacterInfo(prefab.Identifier) : null); } - void PostSpawnHuman(Character newCharacter) + void AddToCrew(Character newCharacter) { newCharacter.TeamID = teamType; if (addToCrew) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index e0c836ef0..35e5901db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -218,11 +218,11 @@ namespace Barotrauma //the first character in the CustomInteract callback is always the NPC and the 2nd the character who interacted with it //but the TriggerAction can configure the 1st and 2nd entity in either order, //let's make sure we pass the NPC and the interactor in the intended order - if (e1 == npc && targets2.Contains(interactor)) + if (e1 == npc && ParentEvent.GetTargets(Target2Tag).Contains(interactor)) { Trigger(npc, interactor); } - else if (targets1.Contains(interactor) && e2 == npc) + else if (ParentEvent.GetTargets(Target1Tag).Contains(interactor) && e2 == npc) { Trigger(interactor, npc); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 37e9c078d..67c18ce0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -250,8 +250,7 @@ namespace Barotrauma } // Overrides the team change set in the base method. var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); - var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId; - if (teamId != originalTeam) + if (teamId != spawnedCharacter.TeamID) { spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 3d2e94d78..e4edf96d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -356,7 +356,7 @@ namespace Barotrauma //that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost if (teamId != originalTeam) { - spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId); + spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId, processImmediately: true); } if (element.GetAttribute("color") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 9bbedeae5..540696e3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -542,6 +542,7 @@ namespace Barotrauma { currentConfig = Config.FromElement(currentConfigDoc.Root ?? throw new NullReferenceException("Config XML element is invalid: document is null.")); #if CLIENT + MainMenuScreen.DismissedNotifications = currentConfigDoc.Root.GetAttributeIdentifierArray(nameof(MainMenuScreen.DismissedNotifications), defaultValue: Array.Empty()).ToHashSet(); ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); MultiplayerPreferences.Init( currentConfigDoc.Root.GetChildElement("player"), @@ -660,6 +661,8 @@ namespace Barotrauma } #if CLIENT + root.Add(new XAttribute(nameof(MainMenuScreen.DismissedNotifications), string.Join(',', MainMenuScreen.DismissedNotifications.Select(n => n.Value)))); + XElement serverFiltersElement = new XElement("serverfilters"); root.Add(serverFiltersElement); ServerListFilters.Instance.SaveTo(serverFiltersElement); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 46a65d9cb..8c23a48ed 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,9 +1,56 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.8.6.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed some icons still scaling incorrectly (e.g. location icons on the campaign map, device icons on the status monitor). +- Fixed device previews not appearing in the shipyard menu when you've selected a category with only swappable items but no upgrades (doesn't affect any vanilla content). + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.8.6.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed an issue that caused the scaling of item icons in the submarine editor (and potentially other places) to be incorrect. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.8.6.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed certain scripted events breaking when you switch the character (e.g. interaction icons disappearing from some NPCs). +- Fixed hostages being hostile to you and the outpost team in jailbreak missions. +- Fixed inability to communicate using the headset with characters spawned via the debug console (once again). + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.8.5.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Updated translations. +- Fixed "favorite" checkbox rendering behind the server settings button in the server lobby. +- Fixed server not allowing new characters to be hired to the reserve bench if over the maximum crew size. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.8.4.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed PvP variants of the underwater scooter being sold in campaign stores. +- Hid the new sorting and filtering options from vending machines. +- Fixes to several AI issues that sometimes made it difficult for monsters to board the sub (e.g. repeatedly moving in and out of the airlock). +- Fixes to issues that sometimes caused monsters that are indoors to attempt to attack a target through a wall/floor/ceiling even if they have a valid path to the target. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.8.4.1 ------------------------------------------------------------------------------------------------------------------------------------------------- - Fixed characters sometimes ending up in an invalid state when a player goes back to the server lobby, opts to spectate and rejoins the round: the character wouldn't go unconscious or braindead as it should, but the client would not regain control of it either. - AI characters no longer accidentally hit friendly characters with melee weapons. +- Fixed bots not reacting to enemies while following, unless being attacked. +- Fixed bots being allowed to operate items in unsafe hulls, meaning they'd attempt to get to a device in a room with e.g. monsters or fires, and then immediately flee, and repeat. +- AI fixes to make characters better at dealing with fires. +- Fixed bots getting stuck in ladders while following the player and aiming at the enemy. +- Fixed bots not facing the enemies (or sometimes glitching while trying to face both the player and the enemy) while following a player. +- Fixes to several AI issues that caused issues when bots where trying to find safety. +- Fixed bots sometimes changing targets when they should just stick to the current target, causing indecisive behavior. +- Made bots crouch by default when using ranged weapons, so that other bots (or players) can shoot past them. +- Improved AI behavior in close combat: when a bot has a valid ranged weapon in the inventory, they now try to keep the distance to the target and switch back to the ranged weapon when possible. Previously the bots just used melee, because they can't shoot the targets when they are really close.Improved AI behavior in close combat: when a bot has a valid ranged weapon in the inventory, they now try to keep the distance to the target and switch back to the ranged weapon when possible. Previously the bots just used melee, because they can't shoot the targets when they are really close. ------------------------------------------------------------------------------------------------------------------------------------------------- v1.8.4.0