diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 56cb2ac83..9ecd47a4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -19,7 +19,7 @@ namespace Barotrauma var target = _selectedAiTarget ?? _lastAiTarget; if (target != null && target.Entity != null) { - var memory = GetTargetMemory(target, false); + var memory = GetTargetMemory(target); if (memory != null) { Vector2 targetPos = memory.Location; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8139f283c..8cd302262 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -214,7 +214,6 @@ namespace Barotrauma double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; - TransformCursorPos(); bool ragdollInput = msg.ReadBoolean(); keys[(int)InputType.Ragdoll].Held = ragdollInput; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 107816307..602518293 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -850,7 +850,7 @@ namespace Barotrauma if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { //highlight the slot the treatment item is in - var rootContainer = matchingItem.GetRootContainer() ?? matchingItem; + var rootContainer = matchingItem.RootContainer ?? matchingItem; var index = Character.Controlled.Inventory.FindIndex(rootContainer); if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 913f15dca..411828bc8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -870,7 +870,7 @@ namespace Barotrauma { if (wearable.Type == WearableType.Hair) { - if (HairWithHatSprite != null) + if (HairWithHatSprite != null && !hideLimb) { DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); depthStep += step; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index e3ea672f6..908ce4426 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -9,6 +9,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -1146,6 +1147,26 @@ namespace Barotrauma }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("debugdrawlos", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.LightManager.DebugLos; + } + GameMain.LightManager.DebugLos = state; + NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow); + }); + AssignOnExecute("debugwiring", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !ConnectionPanel.DebugWiringMode; + } + ConnectionPanel.DebugWiringMode = state; + NewMessage("Wiring debug mode " + (ConnectionPanel.DebugWiringMode ? "enabled" : "disabled"), Color.Yellow); + }); + AssignRelayToServer("debugdraw", false); + AssignOnExecute("devmode", (string[] args) => { if (args.None() || !bool.TryParse(args[0], out bool state)) @@ -2814,7 +2835,26 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.ReloadCore(); })); - #warning TODO: reimplement? +#if WINDOWS + commands.Add(new Command("startdedicatedserver", "", (string[] args) => + { + Process.Start("DedicatedServer.exe"); + })); + + commands.Add(new Command("editserversettings", "", (string[] args) => + { + if (Process.GetProcessesByName("DedicatedServer").Length > 0) + { + NewMessage("Can't be edited if DedicatedServer.exe is already running", Color.Red); + } + else + { + Process.Start("notepad.exe", "serversettings.xml"); + } + })); +#endif + +#warning TODO: reimplement? /*commands.Add(new Command("ingamemodswap", "", (string[] args) => { ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index ef02adad7..1615092b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -662,34 +662,36 @@ namespace Barotrauma Identifier missionIdentifier = msg.ReadIdentifier(); int locationIndex = msg.ReadInt32(); int destinationIndex = msg.ReadInt32(); - string missionName = msg.ReadString(); - MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); - if (prefab != null) + if (Screen.Selected != GameMain.NetLobbyScreen) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), - Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); + if (prefab != null) { - IconColor = prefab.IconColor - }; - if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) - { - Location location = map.Locations[locationIndex]; - map.Discover(location, checkTalents: false); + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + { + IconColor = prefab.IconColor + }; + if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) + { + Location location = map.Locations[locationIndex]; + map.Discover(location, checkTalents: false); - LocationConnection? connection = null; - if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) - { - Location destination = map.Locations[destinationIndex]; - connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); - } - if (connection != null) - { - location.UnlockMission(prefab, connection); - } - else - { - location.UnlockMission(prefab); + LocationConnection? connection = null; + if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) + { + Location destination = map.Locations[destinationIndex]; + connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); + } + if (connection != null) + { + location.UnlockMission(prefab, connection); + } + else + { + location.UnlockMission(prefab); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index bb573b27d..41a7758b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - for (int i = 0; i < resourceClusters.Count; i++) + for (int i = 0; i < resourceAmounts.Count; i++) { var amount = msg.ReadByte(); var rotation = msg.ReadSingle(); @@ -54,7 +54,7 @@ namespace Barotrauma CalculateMissionClusterPositions(); - for(int i = 0; i < resourceClusters.Count; i++) + for(int i = 0; i < resourceAmounts.Count; i++) { var identifier = msg.ReadIdentifier(); var count = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 6930395a5..15860e012 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -88,13 +88,14 @@ namespace Barotrauma public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element) => TextManager.SpeciallyHandledCharCategories .Where(category => element.GetAttributeBool($"is{category}", category switch { - // CJK isn't supported by default + // CJK and Japanese aren't supported by default TextManager.SpeciallyHandledCharCategory.CJK => false, - + TextManager.SpeciallyHandledCharCategory.Japanese => false, + // For backwards compatibility, we assume that Cyrillic is supported by default TextManager.SpeciallyHandledCharCategory.Cyrillic => true, - _ => throw new Exception("unreachable") + _ => throw new NotImplementedException($"nameof{category} not implemented.") })) .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 9a33f0ce9..16ea7caa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -199,8 +199,8 @@ namespace Barotrauma if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { size = new Point( - subElement.GetAttributeInt("width", 0), - subElement.GetAttributeInt("height", 0)); + ParseSize(subElement, "width"), + ParseSize(subElement, "height")); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 5af8c6e02..f460ff480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -533,7 +533,7 @@ namespace Barotrauma { if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) { - if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index c9a7631cb..ee15c0c06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -1,14 +1,12 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; -using System.Text; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -20,6 +18,26 @@ namespace Barotrauma { return element.NameAsIdentifier(); } + + protected int ParseSize(XElement element, string attributeName) + { + string valueStr = element.GetAttributeString(attributeName, string.Empty); + bool relativeToWidth = valueStr.EndsWith("vw"); + bool relativeToHeight = valueStr.EndsWith("vh"); + if (relativeToWidth || relativeToHeight) + { + string floatStr = valueStr.Substring(0, valueStr.Length - 2); + if (!float.TryParse(floatStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float relativeHeight)) + { + DebugConsole.ThrowError($"Error while parsing a {nameof(GUIComponentStyle)}: {valueStr} is not a valid size."); + } + return (int)(relativeHeight / 100.0f * (relativeToWidth ? GameMain.GraphicsWidth : GameMain.GraphicsHeight)); + } + else + { + return element.GetAttributeInt(attributeName, 0); + } + } } public abstract class GUISelector where T : GUIPrefab @@ -166,7 +184,8 @@ namespace Barotrauma Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { - return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); + int rawSize = ParseSize(subElement, "size"); + return (uint)Math.Round(rawSize * GameSettings.CurrentConfig.Graphics.TextScale); } } return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 7e63ac0e7..55fc849e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -532,6 +532,7 @@ namespace Barotrauma void drawRect(Vector2 topLeft, Vector2 bottomRight) { int minWidth = GUI.IntScale(5); + if (OverflowClip) { topLeft.X = Math.Max(topLeft.X, 0.0f); } if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 98921a51b..8442731b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -159,8 +159,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - CrewArea = new Rectangle(Padding, crewAreaY, (int)Math.Max(400 * GUI.Scale, 220), crewAreaHeight); + float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; + CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 03a16c643..2e5826656 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -175,6 +175,7 @@ namespace Barotrauma { if (relativeOffset.NearlyEquals(value)) { return; } relativeOffset = value; + recalculateRect = true; RecalculateChildren(false, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 1cb5e37ab..34b935f84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -870,7 +870,7 @@ namespace Barotrauma { foreach (var minRep in priceInfo.MinReputation) { - if (campaign.GetReputation(minRep.Key) < minRep.Value) + if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value) { return minRep; } @@ -1930,7 +1930,7 @@ namespace Barotrauma "campaignstore.reputationrequired", ("[amount]", ((int)requiredReputation.Value.Value).ToString()), ("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value)); - Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ? + Color color = MathF.Round(campaign.GetReputation(requiredReputation.Value.Key)) < requiredReputation.Value.Value ? GUIStyle.Orange : GUIStyle.Green; toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index d1ea71205..64f9c9ac9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -807,8 +807,10 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); + if (GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine)) + { + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); + } RefreshSubmarineDisplay(true); } else @@ -829,7 +831,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); + GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine); RefreshSubmarineDisplay(true); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3bdf2d65d..cff4b0a2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1722,7 +1722,7 @@ namespace Barotrauma static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) { list.Content.ClearChildren(); - List allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List(); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled); var resources = prefab.GetApplicableResources(targetLevel); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 3a217bf1e..55023f747 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -23,9 +23,12 @@ namespace Barotrauma private float votingTime = 100f; private float timer; private VoteType currentVoteType; - private Color SubmarineColor => GUIStyle.Orange; + private static Color SubmarineColor => GUIStyle.Orange; private Point createdForResolution; + //timer ran out but server still hasn't notified of the result of the vote + public bool TimedOut => VoteRunning && timer - votingTime > 10.0f; + public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime) { if (starter == null || info == null) { return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 0df30ed06..8d8f7a838 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -664,7 +664,10 @@ namespace Barotrauma while (Timing.Accumulator >= Timing.Step) { Timing.TotalTime += Timing.Step; - + if (!Paused) + { + Timing.TotalTimeUnpaused += Timing.Step; + } Stopwatch sw = new Stopwatch(); sw.Start(); @@ -944,7 +947,10 @@ namespace Barotrauma PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } - if (!Paused) { Timing.Alpha = Timing.Accumulator / Timing.Step; } + if (!Paused) + { + Timing.Alpha = Timing.Accumulator / Timing.Step; + } if (performanceCounterTimer.ElapsedMilliseconds > 1000) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index baf76b999..4655e13d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -31,7 +31,7 @@ namespace Barotrauma // Item must be in a non-equipment slot if possible if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } // Item must not be contained inside an item in an equipment slot - if (item.GetRootContainer() is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; } + if (item.RootContainer is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; } return true; }, recursive: true).Distinct(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 377bba240..eddd741f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -376,6 +376,7 @@ namespace Barotrauma - (0.1f * iconRelativeWidth) // Spacing - (7 * layoutGroup.RelativeSpacing); + nameRelativeWidth = Math.Max(nameRelativeWidth, 0.25f); var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var nameBlock = new GUITextBlock( diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index e8823f5dc..89364a512 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -121,6 +121,23 @@ namespace Barotrauma { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } + protected GUIButton CreateEndRoundButton() + { + int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int buttonHeight = (int)(40 * GUI.yScale); + var rectT = HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y, buttonWidth, buttonHeight), GUI.Canvas); + rectT.Pivot = Pivot.Center; + return new GUIButton(rectT, TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + { + Pulse = true, + TextBlock = + { + Shadow = true, + AutoScaleHorizontal = true + } + }; + } + public override void Draw(SpriteBatch spriteBatch) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 4310ee700..ab23c52d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -125,38 +125,25 @@ namespace Barotrauma private void CreateButtons() { - int buttonHeight = (int) (GUI.Scale * 40), - buttonWidth = GUI.IntScale(450), - buttonCenter = buttonHeight / 2, - screenMiddle = GameMain.GraphicsWidth / 2; - - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => GameMain.Client.RequestStartRound(), - onReturnToMapScreen: () => - { - ShowCampaignUI = true; - if (CampaignUI == null) { InitCampaignUI(); } - CampaignUI.SelectTab(InteractionType.Map); - }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => GameMain.Client.RequestStartRound(), + onReturnToMapScreen: () => + { + ShowCampaignUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + }); + return true; }; - int readyButtonHeight = buttonHeight; - int readyButtonWidth = (int) (GUI.Scale * 50); - - ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), + int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int readyButtonHeight = (int)(GUI.Scale * 40); + int readyButtonCenter = readyButtonHeight / 2, + screenMiddle = GameMain.GraphicsWidth / 2; + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), @@ -206,7 +193,7 @@ namespace Barotrauma if (GameMain.Client == null) { - yield return CoroutineStatus.Failure; + yield return CoroutineStatus.Success; } if (GameMain.Client.LateCampaignJoin) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 102c9d921..d2c611e10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -148,6 +148,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; } } @@ -210,28 +213,14 @@ namespace Barotrauma { StartRound = () => { TryEndRound(); } }; - } - private void CreateEndRoundButton() - { - int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => TryEndRound(), - onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => TryEndRound(), + onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); + return true; }; } @@ -699,6 +688,11 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 53bcf4f1f..f6730cbd5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -310,9 +310,10 @@ namespace Barotrauma }; } } + var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) + if (selectedMissions.Contains(displayedMission)) { RichString reputationText = displayedMission.GetReputationRewardText(); if (!reputationText.IsNullOrEmpty()) @@ -324,7 +325,7 @@ namespace Barotrauma if (totalReward > 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && displayedMission.Completed) { var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); if (share > 0) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index e388ac515..289b08e4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -575,7 +575,7 @@ namespace Barotrauma //cancel dragging if too far away from the container of the dragged item if (DraggingItems.Any()) { - var rootContainer = DraggingItems.First().GetRootContainer(); + var rootContainer = DraggingItems.First().RootContainer; var rootInventory = DraggingItems.First().ParentInventory; if (rootContainer != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index e5b132f89..0be3b921f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -92,9 +92,6 @@ namespace Barotrauma.Items.Components rect.Height = (int)(rect.Height * (1.0f - openState)); } - //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed - //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion - bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f; if (Window.Height > 0 && Window.Width > 0) { if (IsHorizontal) @@ -117,7 +114,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -141,7 +138,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -156,11 +153,23 @@ namespace Barotrauma.Items.Components else { convexHull.Enabled = true; - convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); + SetVertices(convexHull, rect); } } + private void SetVertices(ConvexHull convexHull, Rectangle rect) + { + var verts = GetConvexHullCorners(rect); + Vector2 center = (verts[0] + verts[2]) / 2; + convexHull.SetVertices( + verts, + IsHorizontal ? + new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : + new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) }); + convexHull.MaxMergeLosVerticesDist = 35.0f; + } + partial void UpdateProjSpecific(float deltaTime) { if (shakeTimer > 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 2f23d59d2..cd0b92161 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { - if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } + if (character == null || !character.IsKeyDown(InputType.Aim) || !character.CanAim) { return; } //camera focused on some other item/device, don't draw the crosshair if (character.ViewTarget != null && (character.ViewTarget is Item viewTargetItem) && viewTargetItem.Prefab.FocusOnSelected) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 39e714c89..9b1334f3a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components { //whole text can fit in the textblock, no need to scroll needsScrolling = false; - scrollingText = DisplayText.Value; + TextBlock.Text = scrollingText = DisplayText.Value; scrollPadding = 0; scrollAmount = 0.0f; scrollIndex = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index e9406abde..745eca08b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components Step = 0.05f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => { + lastReceivedTargetForce = null; float newTargetForce = barScroll * 200.0f - 100.0f; if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 49379a2f9..3ac43ec4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components private GUIFrame selectedItemFrame; private GUIFrame selectedItemReqsFrame; - private GUITextBlock amountTextMin, amountTextMax; + private GUITextBlock amountTextMax; private GUIScrollBar amountInput; public GUIButton ActivateButton @@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components private GUIComponent outputSlot; private GUIComponent inputInventoryHolder, outputInventoryHolder; + private readonly List itemCategoryButtons = new List(); + private MapEntityCategory? selectedItemCategory; + public FabricationRecipe SelectedItem { get { return selectedItem; } @@ -77,7 +80,67 @@ namespace Barotrauma.Items.Components AutoScaleVertical = true }; - var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var innerArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), isHorizontal: true) + { + RelativeSpacing = 0.01f, + Stretch = true, + CanBeFocused = true + }; + + List itemCategories = Enum.GetValues().ToList(); + itemCategories.Remove(MapEntityCategory.None); + itemCategories.RemoveAll(c => fabricationRecipes.None(f => f.Value?.TargetItem is ItemPrefab ti && ti.Category.HasFlag(c))); + itemCategoryButtons.Clear(); + + //only create category buttons if there's more than one category in addition to "All" + if (itemCategories.Count > 2) + { + // === Item category buttons === + var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.05f, 1.0f), innerArea.RectTransform)) + { + RelativeSpacing = 0.01f + }; + + int buttonSize = Math.Min(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Height / itemCategories.Count); + + var categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform), style: "CategoryButton.All") + { + ToolTip = TextManager.Get("MapEntityCategory.All"), + OnClicked = OnClickedCategoryButton + }; + itemCategoryButtons.Add(categoryButton); + foreach (MapEntityCategory category in itemCategories) + { + categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform), + style: "CategoryButton." + category) + { + ToolTip = TextManager.Get("MapEntityCategory." + category), + UserData = category, + OnClicked = OnClickedCategoryButton + }; + itemCategoryButtons.Add(categoryButton); + } + bool OnClickedCategoryButton(GUIButton button, object userData) + { + MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null; + if (newCategory.HasValue) { itemFilterBox.Text = ""; } + selectedItemCategory = newCategory; + FilterEntities(newCategory, itemFilterBox.Text); + return true; + } + foreach (var btn in itemCategoryButtons) + { + btn.RectTransform.SizeChanged += () => + { + if (btn.Frame.sprites == null || !btn.Frame.sprites.TryGetValue(GUIComponent.ComponentState.None, out var spriteList)) { return; } + var sprite = spriteList?.First(); + if (sprite == null) { return; } + btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); + }; + } + } + + var mainFrame = new GUILayoutGroup(new RectTransform(Vector2.One, innerArea.RectTransform), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f, Stretch = true, @@ -105,10 +168,13 @@ namespace Barotrauma.Items.Components Padding = Vector4.Zero, AutoScaleVertical = true }; - itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true); + itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true) + { + OverflowClip = true + }; itemFilterBox.OnTextChanged += (textBox, text) => { - FilterEntities(text); + FilterEntities(selectedItemCategory, text); return true; }; filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height); @@ -174,7 +240,7 @@ namespace Barotrauma.Items.Components Stretch = true }; - amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center); amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider") { @@ -489,15 +555,37 @@ namespace Barotrauma.Items.Components inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); } - var requiredItemPrefab = requiredItem.FirstMatchingPrefab; - var itemIcon = requiredItemPrefab.InventoryIcon ?? requiredItemPrefab.Sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; - itemIcon.Draw( - spriteBatch, - slotRect.Center.ToVector2(), - color: requiredItemPrefab.InventoryIconColor * 0.3f, - scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); - + + var requiredItemPrefab = requiredItem.FirstMatchingPrefab; + + float iconAlpha = 0.0f; + ItemPrefab requiredItemToDisplay; + int count = requiredItem.ItemPrefabs.Count(); + if (count > 1) + { + float iconCycleSpeed = 0.5f / count; + float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed; + int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count()); + + requiredItemToDisplay = requiredItem.ItemPrefabs.Skip(iconIndex).FirstOrDefault(); + iconAlpha = Math.Min(Math.Abs(MathF.Sin(iconCycleT * MathHelper.Pi)) * 2.0f, 1.0f); + } + else + { + requiredItemToDisplay = requiredItem.ItemPrefabs.FirstOrDefault(); + iconAlpha = 1.0f; + } + if (iconAlpha > 0.0f) + { + var itemIcon = requiredItemToDisplay.InventoryIcon ?? requiredItemToDisplay.Sprite; + itemIcon.Draw( + spriteBatch, + slotRect.Center.ToVector2(), + color: requiredItemToDisplay.InventoryIconColor * 0.3f * iconAlpha, + scale: Math.Min(slotRect.Width * 0.9f / itemIcon.size.X, slotRect.Height * 0.9f / itemIcon.size.Y)); + } + if (missingCount > 1) { Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); @@ -552,7 +640,11 @@ namespace Barotrauma.Items.Components } toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; - if (!requiredItemPrefab.Description.IsNullOrEmpty()) + if (!requiredItem.OverrideDescription.IsNullOrEmpty()) + { + toolTipText += '\n' + requiredItem.OverrideDescription; + } + else if (!requiredItemPrefab.Description.IsNullOrEmpty()) { toolTipText += '\n' + requiredItemPrefab.Description; } @@ -601,22 +693,21 @@ namespace Barotrauma.Items.Components } } - private bool FilterEntities(string filter) + private bool FilterEntities(MapEntityCategory? category, string filter) { - if (string.IsNullOrWhiteSpace(filter)) + foreach (GUIComponent child in itemList.Content.Children) { - itemList.Content.Children.ForEach(c => c.Visible = true); - } - else - { - foreach (GUIComponent child in itemList.Content.Children) - { - FabricationRecipe recipe = child.UserData as FabricationRecipe; - if (recipe?.DisplayName == null) { continue; } - child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase); - } - } + FabricationRecipe recipe = child.UserData as FabricationRecipe; + if (recipe?.DisplayName == null) { continue; } + child.Visible = + (string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) && + (!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value)); + } + foreach (GUIButton btn in itemCategoryButtons) + { + btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory; + } HideEmptyItemListCategories(); return true; @@ -648,7 +739,7 @@ namespace Barotrauma.Items.Components public bool ClearFilter() { - FilterEntities(""); + FilterEntities(selectedItemCategory, ""); itemList.UpdateScrollBarSize(); itemList.BarScroll = 0.0f; itemFilterBox.Text = ""; @@ -737,6 +828,7 @@ namespace Barotrauma.Items.Components TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, + ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip") }; foreach (Skill skill in selectedItem.RequiredSkills) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 82dbf9a7a..6759ec985 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -125,18 +125,15 @@ namespace Barotrauma.Items.Components { public static MiniMapSettings Default = new MiniMapSettings ( - ignoreOutposts: false, createHullElements: true, elementColor: MiniMap.MiniMapBaseColor ); - public readonly bool IgnoreOutposts; public readonly bool CreateHullElements; public readonly Color ElementColor; - public MiniMapSettings(bool ignoreOutposts = false, bool createHullElements = false, Color? elementColor = null) + public MiniMapSettings(bool createHullElements = false, Color? elementColor = null) { - IgnoreOutposts = ignoreOutposts; CreateHullElements = createHullElements; ElementColor = elementColor ?? MiniMap.MiniMapBaseColor; } @@ -437,7 +434,11 @@ namespace Barotrauma.Items.Components prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); submarineContainer.ClearChildren(); - if (item.Submarine is null) { return; } + if (item.Submarine is null) + { + displayedSubs.Clear(); + return; + } scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; @@ -445,8 +446,8 @@ namespace Barotrauma.Items.Components ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents); - IEnumerable electrialPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); - electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents); + IEnumerable electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); + electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents); Dictionary electricChildren = new Dictionary(); @@ -536,7 +537,7 @@ namespace Barotrauma.Items.Components displayedSubs.Clear(); displayedSubs.Add(item.Submarine); - displayedSubs.AddRange(item.Submarine.DockedTo); + displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID)); subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); @@ -551,7 +552,7 @@ namespace Barotrauma.Items.Components item.Submarine is { } itemSub && ( !displayedSubs.Contains(itemSub) || // current sub not displayed - itemSub.DockedTo.Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed + itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed ) || prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed @@ -731,7 +732,7 @@ namespace Barotrauma.Items.Components if (sprite != null && ShowHullIntegrity) { Vector2 spriteSize = sprite.size; - Rectangle worldBorders = item.Submarine.GetDockedBorders(); + Rectangle worldBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false); worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); foreach (Gap gap in Gap.GapList) { @@ -915,7 +916,7 @@ namespace Barotrauma.Items.Components } - RectangleF dockedBorders = item.Submarine.GetDockedBorders(); + RectangleF dockedBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false); dockedBorders.Location += item.Submarine.WorldPosition; RectangleF parentRect = miniMapFrame.Rect; @@ -1305,7 +1306,7 @@ namespace Barotrauma.Items.Components GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - Rectangle worldBorders = sub.GetDockedBorders(); + Rectangle worldBorders = sub.GetDockedBorders(allowDifferentTeam: false); worldBorders.Location += sub.WorldPosition.ToPoint(); parentRect.Inflate(-inflate, -inflate); @@ -1526,7 +1527,7 @@ namespace Barotrauma.Items.Components Dictionary pointsOfInterestCollection = new Dictionary(); - RectangleF worldBorders = sub.GetDockedBorders(); + RectangleF worldBorders = sub.GetDockedBorders(allowDifferentTeam: false); worldBorders.Location += sub.WorldPosition; // create a container that has the same "aspect ratio" as the sub @@ -1539,7 +1540,7 @@ namespace Barotrauma.Items.Components GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null); - ImmutableHashSet connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet(); + ImmutableHashSet connectedSubs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID).ToImmutableHashSet(); ImmutableArray hullList = ImmutableArray.Empty; ImmutableDictionary> combinedHulls = ImmutableDictionary>.Empty; @@ -1686,7 +1687,7 @@ namespace Barotrauma.Items.Components bool IsPartofSub(MapEntity entity) { if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; } - return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true); + return sub.IsEntityFoundOnThisSub(entity, true); } bool IsStandaloneHull(Hull hull) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 3a780a5e8..0f61141c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components private const float NearbyObjectUpdateInterval = 1.0f; float nearbyObjectUpdateTimer; - private List connectedSubs = new List(); + private readonly List connectedSubs = new List(); private const float ConnectedSubUpdateInterval = 1.0f; float connectedSubUpdateTimer; @@ -335,9 +335,11 @@ namespace Barotrauma.Items.Components // Setup layout for nav terminal if (isConnectedToSteering || RightLayout) { + controlContainer.RectTransform.AbsoluteOffset = Point.Zero; controlContainer.RectTransform.RelativeOffset = controlBoxOffset; controlContainer.RectTransform.SetPosition(Anchor.TopRight); sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; + if (HasMineralScanner) { PreventMineralScannerOverlap(); } sonarView.RectTransform.SetPosition(Anchor.CenterLeft); sonarView.RectTransform.Resize(GUISizeCalculation); GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); @@ -431,10 +433,11 @@ namespace Barotrauma.Items.Components var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, zoomSlider.Parent.RectTransform.RelativeSize.Y), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal") { + Selected = UseMineralScanner, OnClicked = (button, data) => { - useMineralScanner = !useMineralScanner; - button.Selected = useMineralScanner; + UseMineralScanner = !UseMineralScanner; + button.Selected = UseMineralScanner; if (GameMain.Client != null) { unsentChanges = true; @@ -496,12 +499,12 @@ namespace Barotrauma.Items.Components { if (transducer.Transducer.Item.Submarine == null) { continue; } if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { continue; } - connectedSubs = transducer.Transducer.Item.Submarine?.GetConnectedSubs(); + connectedSubs.AddRange(transducer.Transducer.Item.Submarine.GetConnectedSubs()); } } else if (item.Submarine != null) { - connectedSubs = item.Submarine?.GetConnectedSubs(); + connectedSubs.AddRange(item.Submarine?.GetConnectedSubs()); } connectedSubUpdateTimer = ConnectedSubUpdateInterval; } @@ -1032,7 +1035,7 @@ namespace Barotrauma.Items.Components missionIndex++; } - if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && + if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && (item.CurrentHull == null || !DetectSubmarineWalls)) { foreach (var c in MineralClusters) @@ -1512,9 +1515,10 @@ namespace Barotrauma.Items.Components } } - foreach (Item item in Item.ItemList) + foreach (Item item in Item.SonarVisibleItems) { - if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f) + System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f); + if (item.CurrentHull == null) { float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared(); if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) @@ -1922,7 +1926,7 @@ namespace Barotrauma.Items.Components float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } - msg.WriteBoolean(useMineralScanner); + msg.WriteBoolean(UseMineralScanner); } } @@ -1934,7 +1938,7 @@ namespace Barotrauma.Items.Components float zoomT = 1.0f; bool directionalPing = useDirectionalPing; float directionT = 0.0f; - bool mineralScanner = useMineralScanner; + bool mineralScanner = UseMineralScanner; if (isActive) { zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); @@ -1965,7 +1969,7 @@ namespace Barotrauma.Items.Components pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); } useDirectionalPing = directionalModeSwitch.Selected = directionalPing; - useMineralScanner = mineralScanner; + UseMineralScanner = mineralScanner; if (mineralScannerSwitch != null) { mineralScannerSwitch.Selected = mineralScanner; @@ -1982,7 +1986,7 @@ namespace Barotrauma.Items.Components directionalModeSwitch.Selected = useDirectionalPing; if (mineralScannerSwitch != null) { - mineralScannerSwitch.Selected = useMineralScanner; + mineralScannerSwitch.Selected = UseMineralScanner; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index c5e8531f4..6a5d4a44b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -178,8 +178,9 @@ namespace Barotrauma.Items.Components var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); + int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), - TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") + ToolBox.LimitString(TextManager.Get("SteeringMaintainPos"), GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { UserData = UIHighlightAction.ElementId.MaintainPosTickBox, Enabled = autoPilot, @@ -214,7 +215,6 @@ namespace Barotrauma.Items.Components return true; } }; - int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 022972368..35ed6a50a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -21,7 +21,8 @@ namespace Barotrauma.Items.Components public float FlashTimer { get; private set; } public static Wire DraggingConnected { get; private set; } - public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character) + public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character, + out (Vector2 tooltipPos, LocalizedString text) tooltip) { if (DraggingConnected?.Item?.Removed ?? true) { @@ -64,6 +65,8 @@ namespace Barotrauma.Items.Components } } + tooltip = (Vector2.Zero, string.Empty); + //two passes: first the connector, then the wires to get the wires to render in front for (int i = 0; i < 2; i++) { @@ -97,6 +100,42 @@ namespace Barotrauma.Items.Components } } + Vector2 position = c.IsOutput ? rightPos : leftPos; + Color highlightColor = Color.Transparent; + if (ConnectionPanel.ShouldDebugDrawWiring) + { + if (c.IsPower) + { + highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red); + } + else + { + highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen); + highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange); + } + bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale); + + LocalizedString toolTipText = c.GetToolTip(); + if (mouseOn) { tooltip = (position, toolTipText); } + if (!toolTipText.IsNullOrEmpty()) + { + var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite; + glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2, + scale: 45.0f / glowSprite.size.X * panel.Scale); + } + } + + static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color) + { + if (timeSinceCreated < 1.0f) + { + float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f; + Color targetColor = Color.Lerp(defaultColor, color, pulseAmount); + return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated); + } + return defaultColor; + } + //outputs are drawn at the right side of the panel, inputs at the left if (c.IsOutput) { @@ -127,7 +166,6 @@ namespace Barotrauma.Items.Components } } - if (DraggingConnected != null) { if (mouseInRect) @@ -225,7 +263,9 @@ namespace Barotrauma.Items.Components GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; + connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); + } private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval) @@ -259,7 +299,7 @@ namespace Barotrauma.Items.Components { bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); DraggingConnected.RemoveConnection(panel.Item); - if (DraggingConnected.Connect(this, !alreadyConnected, true)) + if (DraggingConnected.TryConnect(this, !alreadyConnected, true)) { var otherConnection = DraggingConnected.OtherConnection(this); ConnectWire(DraggingConnected); @@ -307,6 +347,63 @@ namespace Barotrauma.Items.Components FlashTimer -= deltaTime; } + private (string signal, LocalizedString tooltip) lastSignalToolTip; + private (int powerValue, LocalizedString tooltip) lastPowerToolTip; + + private LocalizedString GetToolTip() + { + if (LastReceivedSignal.TimeSinceCreated < 1.0f) + { + return getSignalTooltip(LastReceivedSignal, "receivedsignal"); + } + else if (LastSentSignal.TimeSinceCreated < 1.0f) + { + return getSignalTooltip(LastSentSignal, "sentsignal"); + } + + LocalizedString getSignalTooltip(Signal signal, string textTag) + { + if (lastSignalToolTip.signal == signal.value && !lastSignalToolTip.tooltip.IsNullOrEmpty()) { return lastSignalToolTip.tooltip; } + lastSignalToolTip = (signal.value, TextManager.GetWithVariable(textTag, "[signal]", signal.value)); + return lastSignalToolTip.tooltip; + } + + if (IsPower) + { + if (item.GetComponent() is Powered powered) + { + if (IsOutput) + { + if (powered.CurrPowerConsumption < 0) + { + return getPowerTooltip(-(int)powered.CurrPowerConsumption, "reactoroutput"); + } + else if (powered is PowerTransfer || powered is PowerContainer) + { + return getPowerTooltip((int)(Grid?.Power ?? 0), "reactoroutput"); + } + } + else if (!IsOutput) + { + float powerConsumption = powered.GetCurrentPowerConsumption(this); + if (!MathUtils.NearlyEqual((int)powerConsumption, 0.0f)) + { + return getPowerTooltip((int)powerConsumption, "reactorload"); + } + } + } + } + + LocalizedString getPowerTooltip(int powerValue, string textTag) + { + if (lastPowerToolTip.powerValue == powerValue && !lastPowerToolTip.tooltip.IsNullOrEmpty()) { return lastPowerToolTip.tooltip; } + lastPowerToolTip = (powerValue, TextManager.GetWithVariable(textTag, "[kw]", powerValue.ToString())); + return lastPowerToolTip.tooltip; + } + + return null; + } + private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label) { int textX = (int)start.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 2525f4ffb..d0c3dfda4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { + public static bool DebugWiringMode; + public static double DebugWiringEnabledUntil; + public static bool ShouldDebugDrawWiring => DebugWiringMode || Timing.TotalTimeUnpaused < DebugWiringEnabledUntil; + //how long the rewiring sound plays after doing changes to the wiring const float RewireSoundDuration = 5.0f; @@ -120,12 +124,15 @@ namespace Barotrauma.Items.Components if (user != Character.Controlled || user == null) { return; } HighlightedWire = null; - Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user); - + Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user, out (Vector2 tooltipPos, LocalizedString text) tooltip); foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) { sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None); } + if (!tooltip.text.IsNullOrEmpty()) + { + GUIComponent.DrawToolTip(spriteBatch, tooltip.text, tooltip.tooltipPos); + } } private void CheckForLabelOverlap() @@ -225,7 +232,7 @@ namespace Barotrauma.Items.Components foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) { connection.ConnectWire(wire); - wire.Connect(connection, false); + wire.TryConnect(connection, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 6ec91dabf..81d3fa3ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -119,6 +118,13 @@ namespace Barotrauma.Items.Components get { return sectionExtents; } } + public readonly record struct VisualSignal( + float TimeSent, + Color Color, + int Direction); + + private VisualSignal lastReceivedSignal; + public static Wire DraggingWire { get => draggingWire; @@ -126,13 +132,11 @@ namespace Barotrauma.Items.Components public static Sprite ExtractWireSprite(ContentXElement element) { - if (defaultWireSprite == null) - { - defaultWireSprite = new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f)) + defaultWireSprite ??= + new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f)) { Depth = 0.855f }; - } Sprite overrideSprite = null; foreach (var subElement in element.Elements()) @@ -153,6 +157,35 @@ namespace Barotrauma.Items.Components if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; } } + public void RegisterSignal(Signal signal, Connection source) + { + lastReceivedSignal = new VisualSignal( + (float)Timing.TotalTimeUnpaused, + GetSignalColor(signal), + Direction: source == connections[0] ? 1 : -1); + } + + private static readonly Color[] dataSignalColors = new Color[] { Color.White, Color.LightBlue, Color.CornflowerBlue, Color.Blue, Color.BlueViolet, Color.Violet }; + + private static Color GetSignalColor(Signal signal) + { + if (signal.value == "0") + { + return Color.Red; + } + else if (signal.value == "1") + { + return Color.LightGreen; + } + else if (float.TryParse(signal.value, out float floatValue)) + { + //convert numeric values to a color (guessing the value might be somewhere in the range of 0-200) + //so a player with a keen eye can get some info out of the color of the signal + return ToolBox.GradientLerp(Math.Abs(floatValue / 200.0f), dataSignalColors); + } + return Color.LightBlue; + } + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { Draw(spriteBatch, editing, Vector2.Zero, itemDepth); @@ -166,20 +199,7 @@ namespace Barotrauma.Items.Components return; } - Vector2 drawOffset = Vector2.Zero; - Submarine sub = item.Submarine; - if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside - { - if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } - if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; } - } - - if (sub != null) - { - drawOffset = sub.DrawPosition + sub.HiddenSubPosition; - } - - drawOffset += offset; + Vector2 drawOffset = GetDrawOffset() + offset; float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); @@ -188,7 +208,9 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f); + section.Draw(spriteBatch, wireSprite, + Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, + drawOffset, depth + 0.00001f, Width * 2.0f); } } else if (item.IsSelected) @@ -270,6 +292,12 @@ namespace Barotrauma.Items.Components } } + + if (ConnectionPanel.ShouldDebugDrawWiring) + { + DebugDraw(spriteBatch, alpha: 0.2f); + } + if (!editing || !GameMain.SubEditorScreen.WiringMode) { return; } for (int i = 0; i < nodes.Count; i++) @@ -295,6 +323,102 @@ namespace Barotrauma.Items.Components } } + public void DebugDraw(SpriteBatch spriteBatch, float alpha = 1.0f) + { + if (sections.Count == 0 || Hidden) + { + return; + } + + const float PowerPulseSpeedLow = 5.0f; + const float PowerPulseSpeedHigh = 10.0f; + const float PowerHighlightScaleLow = 1.5f; + const float PowerHighlightScaleHigh = 2.5f; + + const float SignalIndicatorInterval = 15.0f; + const float SignalIndicatorSpeed = 100.0f; + + Vector2 drawOffset = GetDrawOffset(); + + Color currentHighlightColor = Color.Transparent; + float highlightScale = 0.0f; + if (connections[0] != null && connections[1] != null) + { + float voltage = Math.Max(GetVoltage(0), GetVoltage(1)); + float GetVoltage(int connectionIndex) + { + var connection1 = connections[connectionIndex]; + var connection2 = connections[1 - connectionIndex]; + if (connection1.IsOutput && connection1.Grid is { Power: > 0.01f } grid1) + { + if (connection2.Item.GetComponent() is Powered powered && + (powered.GetCurrentPowerConsumption(connection2) > 0 || powered is PowerTransfer)) + { + return grid1.Voltage; + } + } + return 0.0f; + } + if (voltage > 0.0f) + { + //pulse faster when there's overvoltage + float pulseSpeed = voltage > 1.2f ? PowerPulseSpeedHigh : PowerPulseSpeedLow; + float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * pulseSpeed) + 1.5f) / 2.5f; + voltage = Math.Min(voltage, 1.0f); + highlightScale = MathHelper.Lerp(PowerHighlightScaleLow, PowerHighlightScaleHigh, voltage); + currentHighlightColor = Color.Red * voltage * pulseAmount; + } + } + if (highlightScale > 0.0f) + { + foreach (WireSection section in sections) + { + section.Draw(spriteBatch, wireSprite, currentHighlightColor * alpha, drawOffset, 0.0f, Width * highlightScale); + } + } + + float signalDuration = (float)Timing.TotalTimeUnpaused - lastReceivedSignal.TimeSent; + if (ConnectionPanel.ShouldDebugDrawWiring && signalDuration < 1.0f) + { + //make some wires "off sync" so it's easier to differentiate signals on overlapping wires + float offset = item.ID % 2 == 1 ? SignalIndicatorInterval / 2 : 0.0f; + float signalProgress = ((float)(Timing.TotalTimeUnpaused * SignalIndicatorSpeed + offset) % SignalIndicatorInterval) * lastReceivedSignal.Direction; + foreach (WireSection section in sections) + { + for (float x = 0; x < section.Length; x += SignalIndicatorInterval) + { + Vector2 dir = (section.End - section.Start) / section.Length; + float posOnSection = x + signalProgress; + if (posOnSection < 0 || posOnSection > section.Length) { continue; } + Vector2 signalPos = section.Start + drawOffset + dir * posOnSection; + float a = 1.0f - Vector2.Distance(Screen.Selected.Cam.WorldViewCenter, signalPos) / 500.0f; + if (a < 0) { continue; } + signalPos.Y = -signalPos.Y; + GUI.DrawRectangle(spriteBatch, signalPos - Vector2.One * 2.5f, Vector2.One * 5, lastReceivedSignal.Color * a * (1.0f - signalDuration) * alpha, isFilled: true); + } + } + } + } + + private Vector2 GetDrawOffset() + { + Submarine sub = item.Submarine; + if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside + { + if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } + if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; } + } + + if (sub == null) + { + return Vector2.Zero; + } + else + { + return sub.DrawPosition + sub.HiddenSubPosition; + } + } + private void DrawHangingWire(SpriteBatch spriteBatch, Vector2 start, float depth) { float angle = (float)Math.Sin(GameMain.GameScreen.GameTime * 2.0f + item.ID) * 0.2f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 426d2c127..698db9c7c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool DebugWiring + { + get; + private set; + } + [Serialize(true, IsPropertySaveable.No)] public bool ShowDeadCharacters { @@ -111,6 +118,11 @@ namespace Barotrauma.Items.Components refEntity = item; } + if (equipper != null && equipper == Character.Controlled && DebugWiring) + { + ConnectionPanel.DebugWiringEnabledUntil = Timing.TotalTimeUnpaused + 0.5; + } + thermalEffectState += deltaTime; thermalEffectState %= 10000.0f; @@ -153,6 +165,11 @@ namespace Barotrauma.Items.Components IsActive = false; } + public override void Drop(Character dropper, bool setTransform = true) + { + Unequip(dropper); + } + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index eeeb4f627..e1f12a51b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Lights; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Collision; using Microsoft.Xna.Framework; @@ -10,6 +11,8 @@ namespace Barotrauma.Items.Components { private GUIMessageBox autodockingVerification; + private readonly ConvexHull[] convexHulls = new ConvexHull[2]; + public Vector2 DrawSize { //use the extents of the item as the draw size @@ -109,6 +112,15 @@ namespace Barotrauma.Items.Components } } + partial void RemoveConvexHulls() + { + for (int i = 0; i < convexHulls.Length; i++) + { + convexHulls[i]?.Remove(); + convexHulls[i] = null; + } + } + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool isDocked = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index a131cd4a9..1da57e028 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -9,15 +9,17 @@ namespace Barotrauma { private LightSource lightSource; - partial void UpdateProjSpecific(float growModifier) + private float particleTimer; + + partial void UpdateProjSpecific(float growModifier, float deltaTime) { if (this is DummyFireSource) { - EmitParticles(size, WorldPosition, hull, growModifier, null); + EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, null); } else { - EmitParticles(size, WorldPosition, hull, growModifier, OnChangeHull); + EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, OnChangeHull); } lightSource.Color = new Color(1.0f, 0.45f, 0.3f) * Rand.Range(0.8f, 1.0f); @@ -25,23 +27,29 @@ namespace Barotrauma if (Vector2.DistanceSquared(lightSource.Position, position) > 5.0f) { lightSource.Position = position + Vector2.UnitY * 30.0f; } } - public static void EmitParticles(Vector2 size, Vector2 worldPosition, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null) + public void EmitParticles(Vector2 size, Vector2 worldPosition, float deltaTime, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null) { - float particleCount = Rand.Range(0.0f, size.X / 50.0f); + var particlePrefab = ParticleManager.FindPrefab("flame"); + if (particlePrefab == null) { return; } - for (int i = 0; i < particleCount; i++) + float particlesPerSecond = MathHelper.Clamp(size.X / 2.0f, 10.0f, 200.0f); + + float particleInterval = 1.0f / particlesPerSecond; + particleTimer += deltaTime; + while (particleTimer > particleInterval) { + particleTimer -= particleInterval; Vector2 particlePos = new Vector2( worldPosition.X + Rand.Range(0.0f, size.X), - Rand.Range(worldPosition.Y - size.Y, worldPosition.Y + 20.0f)); + worldPosition.Y - size.Y + particlePrefab.CollisionRadius); Vector2 particleVel = new Vector2( particlePos.X - (worldPosition.X + size.X / 2.0f), Math.Max((float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier, 0.0f)); particleVel.X = MathHelper.Clamp(particleVel.X, -200.0f, 200.0f); - - var particle = GameMain.ParticleManager.CreateParticle("flame", + + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, particlePos, particleVel, 0.0f, hull); if (particle == null) { continue; } @@ -54,7 +62,7 @@ namespace Barotrauma if (Rand.Int(5) == 1) { var smokeParticle = GameMain.ParticleManager.CreateParticle("smoke", - particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull); + particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull); if (smokeParticle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index eac1396ab..5eb9bcd95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -324,13 +324,13 @@ namespace Barotrauma } - /*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); + GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); for (int i = 0; i < waveY.Length - 1; i++) { GUI.DrawLine(spriteBatch, new Vector2(drawRect.X + WaveWidth * i, -WorldSurface - waveY[i] - 10), new Vector2(drawRect.X + WaveWidth * (i + 1), -WorldSurface - waveY[i + 1] - 10), Color.Blue * 0.5f); - }*/ + } } foreach (MapEntity e in linkedTo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs index cde165b2a..a137242ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs @@ -27,7 +27,7 @@ namespace Barotrauma { foreach (var edge in cell.Edges) { - if (MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection)) + if (MathUtils.GetLineSegmentIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection)) { intersectionFound = true; particlePos = intersection; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index b4cf7304f..7faf33912 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -32,13 +31,13 @@ namespace Barotrauma.Lights public bool IsHorizontal; public bool IsAxisAligned; + public Vector2 SubmarineDrawPos; + public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull) { if (start.Pos.Y > end.Pos.Y) { - var temp = start; - start = end; - end = temp; + (end, start) = (start, end); } Start = start; @@ -87,19 +86,23 @@ namespace Barotrauma.Lights private readonly Segment[] segments = new Segment[4]; private readonly SegmentPoint[] vertices = new SegmentPoint[4]; - private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; - private readonly VectorPair[] losOffsets = new VectorPair[4]; - - private readonly bool[] backFacing; - private readonly bool[] ignoreEdge; + private readonly SegmentPoint[] losVertices = new SegmentPoint[2]; + private readonly Vector2[] losOffsets = new Vector2[2]; private readonly bool isHorizontal; + private readonly int thickness; + public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } public int PenumbraVertexCount { get; private set; } + /// + /// Overrides the maximum distance a LOS vertex can be moved to make it align with a nearby LOS segment + /// + public float? MaxMergeLosVerticesDist; + private readonly HashSet overlappingHulls = new HashSet(); public MapEntity ParentEntity { get; private set; } @@ -130,61 +133,59 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } - public ConvexHull(Vector2[] points, Color color, MapEntity parent) + public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent) { - if (shadowEffect == null) - { - shadowEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true }; - } - if (penumbraEffect == null) - { - penumbraEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + penumbraEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { TextureEnabled = true, LightingEnabled = false, Texture = TextureLoader.FromFile("Content/Lights/penumbra.png") }; - } ParentEntity = parent; ShadowVertices = new VertexPositionColor[6 * 4]; PenumbraVertices = new VertexPositionTexture[6 * 4]; - backFacing = new bool[4]; - ignoreEdge = new bool[4]; + BoundingBox = rect; - float minX = points[0].X, minY = points[0].Y, maxX = points[0].X, maxY = points[0].Y; - - for (int i = 1; i < vertices.Length; i++) - { - if (points[i].X < minX) minX = points[i].X; - if (points[i].Y < minY) minY = points[i].Y; - - if (points[i].X > maxX) maxX = points[i].X; - if (points[i].Y > minY) maxY = points[i].Y; - } - - BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); - - isHorizontal = BoundingBox.Width > BoundingBox.Height; + this.isHorizontal = isHorizontal; if (ParentEntity is Structure structure) { - System.Diagnostics.Debug.Assert(!structure.Removed); + Debug.Assert(!structure.Removed); isHorizontal = structure.IsHorizontal; } else if (ParentEntity is Item item) { - System.Diagnostics.Debug.Assert(!item.Removed); + Debug.Assert(!item.Removed); var door = item.GetComponent(); if (door != null) { isHorizontal = door.IsHorizontal; } } - SetVertices(points); - + Vector2[] verts = new Vector2[] + { + new Vector2(rect.X, rect.Bottom), + new Vector2(rect.Right, rect.Bottom), + new Vector2(rect.Right, rect.Y), + new Vector2(rect.X, rect.Y), + }; + + Vector2[] losVerts; + if (this.isHorizontal) + { + thickness = rect.Height; + losVerts = new Vector2[] { new Vector2(rect.X, rect.Center.Y), new Vector2(rect.Right, rect.Center.Y) }; + } + else + { + thickness = rect.Width; + losVerts = new Vector2[] { new Vector2(rect.Center.X, rect.Y), new Vector2(rect.Center.X, rect.Bottom) }; + } + SetVertices(verts, losVerts); Enabled = true; var chList = HullLists.Find(h => h.Submarine == parent.Submarine); @@ -196,249 +197,123 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } chList.List.Add(this); } - private void MergeOverlappingSegments(ConvexHull ch) + private void MergeLosVertices(ConvexHull ch, bool refreshOtherOverlappingHulls = true) { if (ch == this) { return; } - if (isHorizontal == ch.isHorizontal) + //merge dist in the direction parallel to the segment + //(e.g. how far up/down we can stretch a vertical segment) + float mergeDistParallel = MathHelper.Clamp(ch.thickness * 0.65f, 16, 512); + if (MaxMergeLosVerticesDist.HasValue) { - //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) - float mergeDist = 16; - float mergeDistSqr = mergeDist * mergeDist; - - Rectangle intersection = Rectangle.Intersect(BoundingBox, ch.BoundingBox); - int intersectionArea = intersection.Width * intersection.Height; - int bboxArea = BoundingBox.Width * BoundingBox.Height; - int otherBboxArea = ch.BoundingBox.Width * ch.BoundingBox.Height; - if (Math.Abs(intersectionArea - bboxArea) < mergeDistSqr) { return; } - if (Math.Abs(intersectionArea - otherBboxArea) < mergeDistSqr) { return; } - - for (int i = 0; i < segments.Length; i++) - { - for (int j = 0; j < ch.segments.Length; j++) - { - if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } - if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; } - - //the segments must be at different sides of the convex hulls to be merged - //(e.g. the right edge of a wall piece and the left edge of another one) - var segment1Center = (segments[i].Start.Pos + segments[i].End.Pos) / 2.0f; - var segment2Center = (ch.segments[j].Start.Pos + ch.segments[j].End.Pos) / 2.0f; - if (Vector2.Dot(segment1Center - BoundingBox.Center.ToVector2(), segment2Center - ch.BoundingBox.Center.ToVector2()) > 0) { continue; } - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], true); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], false); - } - } - } - } - - for (int i = 0; i < segments.Length; i++) - { - if (ignoreEdge[i]) { continue; } - if (Vector2.DistanceSquared(segments[i].Start.Pos, segments[i].End.Pos) < 1.0f) { continue; } - for (int j = 0; j < ch.segments.Length; j++) - { - if (ch.ignoreEdge[j]) { continue; } - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, ch.segments[j].End.Pos) < 1.0f) { continue; } - if (IsSegmentAInB(segments[i], ch.segments[j])) - { - ignoreEdge[i] = true; - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].Start.Pos); - } - - if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].Start.Pos); - } - } - else if (IsSegmentAInB(ch.segments[j], segments[i])) - { - ch.ignoreEdge[j] = true; - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].Start.Pos); - } - - if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].Start.Pos); - } - } - } - } - - //ignore edges that are inside some other convex hull - for (int i = 0; i < vertices.Length; i++) - { - if (ch.IsPointInside(vertices[i].Pos)) - { - if (ch.IsPointInside(vertices[(i + 1) % vertices.Length].Pos)) - { - ignoreEdge[i] = true; - overlappingHulls.Add(ch); - } - } - } - } - - private void ShiftSegmentPoint(int segmentIndex, bool end, Vector2 newPos) - { - var segment = segments[segmentIndex]; - - losOffsets[segmentIndex] ??= new VectorPair(); - bool flipped = false; - if (Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.Start.Pos) > Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.End.Pos)) - { - flipped = true; - } - if (end == !flipped) - { - losOffsets[segmentIndex].B = newPos; + mergeDistParallel = Math.Max(mergeDistParallel, MaxMergeLosVerticesDist.Value); } else { - losOffsets[segmentIndex].A = newPos; - } - } - - public static bool IsSegmentAInB(Segment a, Segment b) - { - if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) - { - return false; - } - - Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); - min.X -= 1.0f; min.Y -= 1.0f; - - if (a.Start.Pos.X < min.X) { return false; } - if (a.Start.Pos.Y < min.Y) { return false; } - if (a.End.Pos.X < min.X) { return false; } - if (a.End.Pos.Y < min.Y) { return false; } - - Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); - max.X += 1.0f; max.Y += 1.0f; - - if (a.Start.Pos.X > max.X) { return false; } - if (a.Start.Pos.Y > max.Y) { return false; } - if (a.End.Pos.X > max.X) { return false; } - if (a.End.Pos.Y > max.Y) { return false; } - - float startDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.Start.Pos); - if (startDist > 1.0f) { return false; } - float endDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.End.Pos); - if (endDist > 1.0f) { return false; } - return true; - } - - public bool IsPointInside(Vector2 point) - { - if (!BoundingBox.Contains(point)) { return false; } - - Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; - for (int i = 0; i < 4; i++) - { - Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; - Vector2 centerToVertex = center - vertices[i].Pos; - Vector2 pointToVertex = point - vertices[i].Pos; - - float dotCenter = Vector2.Dot(centerToVertex, segmentVector); - float dotPoint = Vector2.Dot(pointToVertex, segmentVector); - - if ((dotCenter > 0f && dotPoint < 0f) || (dotCenter < 0f && dotPoint > 0f)) { return false; } - } - - return true; - } - - private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch) - { - int startPointIndex = -1, endPointIndex = -1; - for (int i = 0; i < vertices.Length; i++) - { - if (vertices[i].Pos.NearlyEquals(segment1.Start.Pos)) - startPointIndex = i; - else if (vertices[i].Pos.NearlyEquals(segment1.End.Pos)) - endPointIndex = i; - } - if (startPointIndex == -1 || endPointIndex == -1) { return; } - - int startPoint2Index = -1, endPoint2Index = -1; - for (int i = 0; i < segment2.ConvexHull.vertices.Length; i++) - { - if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.Start.Pos)) - startPoint2Index = i; - else if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.End.Pos)) - endPoint2Index = i; - } - if (startPoint2Index == -1 || endPoint2Index == -1) { return; } - - if (startPointsMatch) - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.End.Pos) / 2.0f; - } - else - { - if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) < - Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos)) + Rectangle inflatedAABB = ch.BoundingBox; + inflatedAABB.Inflate(2, 2); + //if this los segment isn't touching the other's bounding box, + //don't extend the segment by more than 50% of it's length + if (!inflatedAABB.Contains(losVertices[0].Pos) && + !inflatedAABB.Contains(losVertices[1].Pos)) { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - } - else - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; + mergeDistParallel = Math.Min(mergeDistParallel, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) * 0.5f); } } + //merge dist in the direction perpendicular to the segment + //(e.g. how far right/left we can stretch a vertical segment) + //do not allow more than ~half of the thickness, because that'd make the segment go outside the convex hull + float mergeDistPerpendicular = Math.Min(mergeDistParallel, thickness * 0.35f); - overlappingHulls.Add(segment2.ConvexHull); - segment2.ConvexHull.overlappingHulls.Add(this); + Vector2 center = (losVertices[0].Pos + losVertices[1].Pos) / 2; + + bool changed = false; + for (int i = 0; i < losVertices.Length; i++) + { + Vector2 segmentDir = Vector2.Normalize(losVertices[i].Pos - center); + //check if the closest point on the other convex hull segment is close enough, disregarding any offsets + //otherwise we might end up moving the vertex too much if we stretch it to an already-offset segment + if (!isCloseEnough( + MathUtils.GetClosestPointOnLineSegment(ch.losVertices[0].Pos, ch.losVertices[1].Pos, losVertices[i].Pos), + losVertices[i].Pos)) + { + continue; + } + + //check the offset position of the segment next + Vector2 closest = MathUtils.GetClosestPointOnLineSegment( + ch.losVertices[0].Pos + ch.losOffsets[0], + ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[i].Pos); + if (!isCloseEnough(closest, losVertices[i].Pos)) { continue; } + + //find where the segments would intersect if they had infinite length + // if it's close to the closest point, let's use that instead to keep + // the direction of the segment unchanged (i.e. vertical segment stays vertical) + if (MathUtils.GetLineIntersection( + ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[0].Pos, losVertices[1].Pos, + areLinesInfinite: true, out Vector2 intersection) && + //the intersection needs to be outwards from the vertex we're checking + Vector2.Dot(segmentDir, intersection - losVertices[i].Pos) > 0 && + //the intersection needs to be close enough to the default position of the vertex and the closest point + //(we don't want to merge the segments somewhere close to infinity!) + (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistParallel * mergeDistParallel || + Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f)) + { + closest = intersection; + } + + //don't move the vertices of the segment too close to each other + if (Vector2.DistanceSquared(losVertices[1 - i].Pos + losOffsets[1 - i], closest) < mergeDistPerpendicular * mergeDistPerpendicular) + { + continue; + } + + losOffsets[i] = closest - losVertices[i].Pos; + overlappingHulls.Add(ch); + ch.overlappingHulls.Add(this); + changed = true; + + bool isCloseEnough(Vector2 closest, Vector2 vertex) + { + float dist = Vector2.Distance(closest, vertex); + if (dist < 0.001f) { return true; } + if (dist > mergeDistParallel) { return false; } + + Vector2 closestDir = (closest - vertex) / dist; + + float dot = Math.Abs(Vector2.Dot(segmentDir, closestDir)); + float distAlongAxis = dist * dot; + if (distAlongAxis > mergeDistParallel) { return false; } + + float distPerpendicular = dist * (1.0f - dot); + if (distPerpendicular > mergeDistPerpendicular) { return false; } + + return true; + } + } + if (changed && refreshOtherOverlappingHulls) + { + foreach (var overlapping in overlappingHulls) + { + overlapping.MergeLosVertices(this, refreshOtherOverlappingHulls: false); + } + } + } + + public bool LosIntersects(Vector2 pos1, Vector2 pos2) + { + return MathUtils.LineSegmentsIntersect( + losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1], + pos1, pos2); } public void Rotate(Vector2 origin, float amount) @@ -447,7 +322,7 @@ namespace Barotrauma.Lights Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); - SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); + SetVertices(vertices.Select(v => v.Pos).ToArray(), losVertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); } private void CalculateDimensions() @@ -456,11 +331,10 @@ namespace Barotrauma.Lights for (int i = 1; i < vertices.Length; i++) { - if (vertices[i].Pos.X < minX) minX = vertices[i].Pos.X; - if (vertices[i].Pos.Y < minY) minY = vertices[i].Pos.Y; - - if (vertices[i].Pos.X > maxX) maxX = vertices[i].Pos.X; - if (vertices[i].Pos.Y > minY) maxY = vertices[i].Pos.Y; + minX = Math.Min(minX, vertices[i].Pos.X); + minY = Math.Min(minY, vertices[i].Pos.Y); + maxX = Math.Max(maxX, vertices[i].Pos.X); + maxY = Math.Max(maxY, vertices[i].Pos.Y); } BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); @@ -471,21 +345,17 @@ namespace Barotrauma.Lights for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos += amount; - losVertices[i].Pos += amount; - - losOffsets[i] = null; - segments[i].Start.Pos += amount; segments[i].End.Pos += amount; } + for (int i = 0; i < losVertices.Length; i++) + { + losVertices[i].Pos += amount; + } LastVertexChangeTime = (float)Timing.TotalTime; overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) - { - ignoreEdge[i] = false; - } CalculateDimensions(); @@ -497,8 +367,8 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } } } @@ -511,23 +381,23 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { ch.overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) + for (int i = 0; i < ch.losOffsets.Length; i++) { - ch.ignoreEdge[i] = false; + ch.losOffsets[i] = Vector2.Zero; } } for (int i = 0; i < chList.List.Count; i++) { for (int j = i + 1; j < chList.List.Count; j++) { - chList.List[i].MergeOverlappingSegments(chList.List[j]); - chList.List[j].MergeOverlappingSegments(chList.List[i]); + chList.List[i].MergeLosVertices(chList.List[j]); + chList.List[j].MergeLosVertices(chList.List[i]); } } } } - public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) + public void SetVertices(Vector2[] points, Vector2[] losPoints, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); @@ -535,39 +405,24 @@ namespace Barotrauma.Lights for (int i = 0; i < 4; i++) { - vertices[i] = new SegmentPoint(points[i], this); - losVertices[i] = new SegmentPoint(points[i], this); - losOffsets[i] = null; + vertices[i] = new SegmentPoint(points[i], this); } - - for (int i = 0; i < 4; i++) + for (int i = 0; i < 2; i++) { - ignoreEdge[i] = false; + losVertices[i] = new SegmentPoint(losPoints[i], this); + losOffsets[i] = Vector2.Zero; } overlappingHulls.Clear(); - int margin = 0; - if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) - { - losVertices[0].Pos = new Vector2(points[0].X + margin, points[0].Y); - losVertices[1].Pos = new Vector2(points[1].X + margin, points[1].Y); - losVertices[2].Pos = new Vector2(points[2].X - margin, points[2].Y); - losVertices[3].Pos = new Vector2(points[3].X - margin, points[3].Y); - } - else - { - losVertices[0].Pos = new Vector2(points[0].X, points[0].Y + margin); - losVertices[1].Pos = new Vector2(points[1].X, points[1].Y - margin); - losVertices[2].Pos = new Vector2(points[2].X, points[2].Y - margin); - losVertices[3].Pos = new Vector2(points[3].X, points[3].Y + margin); - } - if (rotationMatrix.HasValue) { for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value); + } + for (int i = 0; i < losVertices.Length; i++) + { losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value); } } @@ -588,7 +443,7 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); + MergeLosVertices(ch); } } } @@ -610,31 +465,17 @@ namespace Barotrauma.Lights /// /// Returns the segments that are facing towards viewPosition /// - public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments, bool ignoreEdges) + public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments) { for (int i = 0; i < 4; i++) { - if (ignoreEdge[i] && ignoreEdges) { continue; } - - Vector2 pos1 = vertices[i].WorldPos; - Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; - - Vector2 middle = (pos1 + pos2) / 2; - - Vector2 L = viewPosition - middle; - - Vector2 N = new Vector2( - -(pos2.Y - pos1.Y), - pos2.X - pos1.X); - - if (Vector2.Dot(N, L) > 0) + if (IsSegmentFacing(vertices[i].WorldPos, vertices[(i + 1) % 4].WorldPos, viewPosition)) { visibleSegments.Add(segments[i]); } } } - public void RefreshWorldPositions() { for (int i = 0; i < 4; i++) @@ -662,34 +503,12 @@ namespace Barotrauma.Lights ShadowVertexCount = 0; - //compute facing of each edge, using N*L - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { - if (ignoreEdge[i]) - { - backFacing[i] = false; - continue; - } - - Vector2 firstVertex = losVertices[i].Pos; - Vector2 secondVertex = losVertices[(i+1) % 4].Pos; - - Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f); - - Vector2 N = new Vector2( - -(secondVertex.Y - firstVertex.Y), - secondVertex.X - firstVertex.X); - - backFacing[i] = (Vector2.Dot(N, L) < 0); - } - - ShadowVertexCount = 0; - for (int i = 0; i < 4; i++) - { - if (!backFacing[i]) { continue; } int currentIndex = i; - Vector3 vertexPos0 = new Vector3(losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos, 0.0f); - Vector3 vertexPos1 = new Vector3(losOffsets[currentIndex]?.B ?? losVertices[(currentIndex + 1) % 4].Pos, 0.0f); + int nextIndex = (currentIndex + 1) % 2; + Vector3 vertexPos0 = new Vector3(losVertices[currentIndex].Pos + losOffsets[currentIndex], 0.0f); + Vector3 vertexPos1 = new Vector3(losVertices[nextIndex].Pos + losOffsets[nextIndex], 0.0f); if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } @@ -740,9 +559,24 @@ namespace Barotrauma.Lights ShadowVertexCount += 6; } + if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) + { + Array.Reverse(ShadowVertices); + } + CalculateLosPenumbraVertices(lightSourcePos); } + private static bool IsSegmentFacing(Vector2 segmentPos1, Vector2 segmentPos2, Vector2 viewPosition) + { + Vector2 segmentMid = (segmentPos1 + segmentPos2) / 2; + Vector2 segmentDiff = segmentPos2 - segmentPos1; + Vector2 segmentNormal = new Vector2(-segmentDiff.Y, segmentDiff.X); + + Vector2 viewDirection = viewPosition - segmentMid; + return Vector2.Dot(segmentNormal, viewDirection) > 0; + } + private void CalculateLosPenumbraVertices(Vector2 lightSourcePos) { Vector3 offset = Vector3.Zero; @@ -752,73 +586,104 @@ namespace Barotrauma.Lights } PenumbraVertexCount = 0; - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { int currentIndex = i; - int prevIndex = (i + 3) % 4; - int nextIndex = (i + 1) % 4; - bool disjointed = losOffsets[i]?.A != null; - Vector2 vertexPos0 = losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos; - Vector2 vertexPos1 = losOffsets[currentIndex]?.B ?? losVertices[nextIndex].Pos; + int nextIndex = (i + 1) % 2; + Vector2 vertexPos0 = losVertices[currentIndex].Pos + losOffsets[currentIndex]; + Vector2 vertexPos1 = losVertices[nextIndex].Pos + losOffsets[nextIndex]; if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } + + Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); - if (backFacing[currentIndex] && (disjointed || (!backFacing[prevIndex]))) + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } - disjointed = losOffsets[i]?.B != null; - if (backFacing[currentIndex] && (disjointed || (!backFacing[nextIndex]))) + PenumbraVertexCount += 3; + + penumbraStart = new Vector3(vertexPos1, 0.0f); + + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos1, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } + + PenumbraVertexCount += 3; + } + } + + public void DebugDraw(SpriteBatch spriteBatch) + { + //RecalculateAll(Submarine.MainSub); + //RefreshWorldPositions(); + + DrawLine(losVertices[0].Pos, losVertices[1].Pos, Color.Gray * 0.5f, width: 3); + DrawLine(losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1], Color.LightGreen, width: 2); + DrawLine(GameMain.GameScreen.Cam.Position + Vector2.One * 1000, GameMain.GameScreen.Cam.Position - Vector2.One * 1000, Color.Magenta, width: 2); + + if (GameMain.LightManager.LightingEnabled) + { + for (int i = 0; i < vertices.Length; i++) + { + Vector2 start = vertices[i].Pos; + Vector2 end = vertices[(i + 1) % 4].Pos; + DrawLine( + start, + end, Color.Yellow * 0.5f, + width: 4); + } + } + + void DrawLine(Vector2 vertexPos0, Vector2 vertexPos1, Color color, int width) + { + if (ParentEntity != null && ParentEntity.Submarine != null) + { + vertexPos0 += ParentEntity.Submarine.DrawPosition; + vertexPos1 += ParentEntity.Submarine.DrawPosition; + } + float alpha = 1.0f; + if (LightManager.ViewTarget != null) + { + alpha = IsSegmentFacing(vertexPos0, vertexPos1, LightManager.ViewTarget.WorldPosition) ? 1.0f : 0.5f; + } + vertexPos0.Y = -vertexPos0.Y; + vertexPos1.Y = -vertexPos1.Y; + GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width); } } @@ -889,16 +754,13 @@ namespace Barotrauma.Lights { HullLists.Remove(chList); } - foreach (ConvexHull ch2 in overlappingHulls) + //create a new list because MergeLosVertices can edit overlappingHulls + foreach (ConvexHull ch2 in overlappingHulls.ToList()) { - for (int i = 0; i < 4; i++) - { - ch2.ignoreEdge[i] = false; - } ch2.overlappingHulls.Remove(this); foreach (ConvexHull ch in chList.List) { - ch.MergeOverlappingSegments(ch2); + ch.MergeLosVertices(ch2); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index c8ec7bb9d..e846bfa4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Threading; namespace Barotrauma.Lights { @@ -22,6 +23,9 @@ namespace Barotrauma.Lights /// const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; + private Thread rayCastThread; + private Queue pendingRayCasts = new Queue(); + public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -58,6 +62,8 @@ namespace Barotrauma.Lights private readonly List lights; + public bool DebugLos; + public bool LosEnabled = true; public float LosAlpha = 1f; public LosMode LosMode = LosMode.Transparent; @@ -68,6 +74,8 @@ namespace Barotrauma.Lights private readonly Texture2D visionCircle; + private readonly Texture2D gapGlowTexture; + private Vector2 losOffset; private int recalculationCount; @@ -85,8 +93,16 @@ namespace Barotrauma.Lights AmbientLight = new Color(20, 20, 20, 255); + rayCastThread = new Thread(UpdateRayCasts) + { + Name = "LightManager Raycast thread", + IsBackground = true //this should kill the thread if the game crashes + }; + rayCastThread.Start(); + visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png"); highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png"); + gapGlowTexture = Sprite.LoadTexture("Content/Lights/pointlight_rays.png"); GameMain.Instance.ResolutionChanged += () => { @@ -100,15 +116,12 @@ namespace Barotrauma.Lights LosEffect = EffectLoader.Load("Effects/losshader"); SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); - if (lightEffect == null) - { - lightEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + lightEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true, TextureEnabled = true, Texture = LightSource.LightTexture }; - } }); } @@ -176,6 +189,51 @@ namespace Barotrauma.Lights } } + private sealed class RayCastTask + { + public LightSource LightSource; + public Vector2 DrawPos; + public float Rotation; + + public RayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + LightSource = lightSource; + DrawPos = drawPos; + Rotation = rotation; + } + + public void Calculate() + { + LightSource.RayCastTask(DrawPos, Rotation); + } + } + + private static readonly object mutex = new object(); + + public void AddRayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + lock (mutex) + { + if (pendingRayCasts.Any(p => p.LightSource == lightSource)) { return; } + pendingRayCasts.Enqueue(new RayCastTask(lightSource, drawPos, rotation)); + } + } + + private void UpdateRayCasts() + { + while (true) + { + lock (mutex) + { + while (pendingRayCasts.Count > 0) + { + pendingRayCasts.Dequeue().Calculate(); + } + } + Thread.Sleep(10); + } + } + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -288,8 +346,8 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } - light.DrawSprite(spriteBatch, cam); light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); + light.DrawSprite(spriteBatch, cam); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -308,15 +366,46 @@ namespace Barotrauma.Lights } spriteBatch.End(); - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); - Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); + Vector3 glowColorHSV = ToolBox.RGBToHSV(AmbientLight); + glowColorHSV.Z = Math.Max(glowColorHSV.Z, 0.4f); + Color glowColor = ToolBox.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z); + Vector2 glowSpriteSize = new Vector2(gapGlowTexture.Width, gapGlowTexture.Height); + foreach (var gap in Gap.GapList) + { + if (gap.IsRoomToRoom || gap.Open <= 0.0f || gap.ConnectedWall == null) { continue; } + + float a = MathHelper.Lerp(0.5f, 1.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.05f, gap.GlowEffectT)); + + float scale = MathHelper.Lerp(0.5f, 2.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, gap.GlowEffectT)); + + float rot = PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.001f, gap.GlowEffectT) * MathHelper.TwoPi; + + Vector2 spriteScale = new Vector2(gap.Rect.Width, gap.Rect.Height) / glowSpriteSize; + Vector2 drawPos = new Vector2(gap.DrawPosition.X, -gap.DrawPosition.Y); + + spriteBatch.Draw(gapGlowTexture, + drawPos, + null, + glowColor * a, + rot, + glowSpriteSize / 2, + scale: Math.Max(spriteScale.X, spriteScale.Y) * scale, + SpriteEffects.None, + layerDepth: 0); + } + spriteBatch.End(); + + GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"]; + GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect); + Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect); spriteBatch.End(); graphics.BlendState = BlendState.Additive; - //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- @@ -389,6 +478,17 @@ namespace Barotrauma.Lights light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); } + if (ConnectionPanel.ShouldDebugDrawWiring) + { + foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList)) + { + if (e is Item item && item.GetComponent() is Wire wire) + { + wire.DebugDraw(spriteBatch, alpha: 0.4f); + } + } + } + lightEffect.World = transform; GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); @@ -566,7 +666,7 @@ namespace Barotrauma.Lights public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) { - if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) return; + if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; } if (ViewTarget == null) return; graphics.SetRenderTarget(LosTexture); @@ -598,23 +698,52 @@ namespace Barotrauma.Lights if (LosEnabled && LosMode != LosMode.None && ViewTarget != null) { Vector2 pos = ViewTarget.DrawPosition; + bool centeredOnHead = false; + if (ViewTarget is Character character && + character.AnimController?.GetLimb(LimbType.Head) is Limb head && + !head.IsSevered && !head.Removed) + { + pos = head.body.DrawPosition; + centeredOnHead = true; + } Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); - Matrix shadowTransform = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; - var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width*0.75f, ViewTarget.Submarine); + var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine); + + //make sure the head isn't peeking through any LOS segments, and if it is, + //center the LOS on the character's collider instead + if (centeredOnHead) + { + foreach (var ch in convexHulls) + { + Vector2 currentViewPos = pos; + Vector2 defaultViewPos = ViewTarget.DrawPosition; + if (ch.ParentEntity?.Submarine != null) + { + defaultViewPos -= ch.ParentEntity.Submarine.DrawPosition; + currentViewPos -= ch.ParentEntity.Submarine.DrawPosition; + } + //check if a line from the character's collider to the head intersects with the los segment (= head poking through it) + if (ch.LosIntersects(defaultViewPos, currentViewPos)) + { + pos = ViewTarget.DrawPosition; + } + } + } + if (convexHulls != null) { List shadowVerts = new List(); List penumbraVerts = new List(); foreach (ConvexHull convexHull in convexHulls) { - if (!convexHull.Enabled || !convexHull.Intersects(camView)) continue; + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) relativeLightPos -= convexHull.ParentEntity.Submarine.Position; + if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } convexHull.CalculateLosVertices(relativeLightPos); @@ -647,6 +776,21 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(null); } + public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam) + { + Vector2 pos = ViewTarget?.Position ?? cam.Position; + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform); + var convexHulls = ConvexHull.GetHullsInRange(pos, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine); + Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); + foreach (ConvexHull convexHull in convexHulls) + { + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + if (convexHull.ParentEntity is Structure { CastShadow: false }) { continue; } + convexHull.DebugDraw(spriteBatch); + } + spriteBatch.End(); + } + public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 724214955..9574a3d6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -15,6 +15,7 @@ namespace Barotrauma.Lights public bool Persistent; + public Dictionary SerializableProperties { get; private set; } = new Dictionary(); [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] @@ -228,6 +229,7 @@ namespace Barotrauma.Lights //do we need to recalculate the vertices of the light volume private bool needsRecalculation; + private bool needsRecalculationWhenUpToDate; public bool NeedsRecalculation { get { return needsRecalculation; } @@ -241,12 +243,30 @@ namespace Barotrauma.Lights } } needsRecalculation = value; + if (needsRecalculation && state != LightVertexState.UpToDate) + { + //if we're currently recalculating light vertices, mark that we need to recalculate them again after it's done + needsRecalculationWhenUpToDate = true; + } } } + //when were the vertices of the light volume last calculated public float LastRecalculationTime { get; private set; } + + private enum LightVertexState + { + UpToDate, + PendingRayCasts, + PendingVertexRecalculation, + } + + private LightVertexState state; + + private Vector2 calculatedDrawPos; + private readonly Dictionary diffToSub; private DynamicVertexBuffer lightVolumeBuffer; @@ -255,7 +275,6 @@ namespace Barotrauma.Lights private int indexCount; private Vector2 translateVertices; - private float rotateVertices; private readonly LightSourceParams lightSourceParams; @@ -295,7 +314,6 @@ namespace Barotrauma.Lights if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { - rotateVertices = rotation - prevCalculatedRotation; return; } @@ -647,13 +665,19 @@ namespace Barotrauma.Lights } } - private static readonly List visibleSegments = new List(); - private static readonly List points = new List(); - private static readonly List output = new List(); - private static readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; - private List FindRaycastHits() + private static readonly object mutex = new object(); + + private readonly List visibleSegments = new List(); + private readonly List points = new List(); + private readonly List verts = new List(); + private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; + private void FindRaycastHits() { - if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } + if (!CastShadows || Range < 1.0f || Color.A < 1) + { + state = LightVertexState.PendingVertexRecalculation; + return; + } Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } @@ -666,8 +690,18 @@ namespace Barotrauma.Lights if (!chList.IsHidden.Contains(hull)) { //find convexhull segments that are close enough and facing towards the light source - hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); + lock (mutex) + { + hull.RefreshWorldPositions(); + hull.GetVisibleSegments(drawPos, visibleSegments); + foreach (var visibleSegment in visibleSegments) + { + if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null) + { + visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition; + } + } + } } } foreach (ConvexHull hull in chList.List) @@ -676,17 +710,19 @@ namespace Barotrauma.Lights } } - //add a square-shaped boundary to make sure we've got something to construct the triangles from - //even if there aren't enough hull segments around the light source + state = LightVertexState.PendingRayCasts; + GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); + } - //(might be more effective to calculate if we actually need these extra points) + public void RayCastTask(Vector2 drawPos, float rotation) + { Vector2 drawOffset = Vector2.Zero; float boundsExtended = TextureRange; if (OverrideLightTexture != null) { - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); + float cosAngle = (float)Math.Cos(rotation); + float sinAngle = -(float)Math.Sin(rotation); var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -706,6 +742,10 @@ namespace Barotrauma.Lights drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; } + //add a square-shaped boundary to make sure we've got something to construct the triangles from + //even if there aren't enough hull segments around the light source + + //(might be more effective to calculate if we actually need these extra points) Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); boundaryCorners[0] = new SegmentPoint(boundsMax, null); @@ -719,197 +759,197 @@ namespace Barotrauma.Lights visibleSegments.Add(s); } - //Generate new points at the intersections between segments - //This is necessary for the light volume to generate properly on some subs - for (int i = 0; i < visibleSegments.Count; i++) + lock (mutex) { - Vector2 p1a = visibleSegments[i].Start.WorldPos; - Vector2 p1b = visibleSegments[i].End.WorldPos; - - for (int j = i + 1; j < visibleSegments.Count; j++) + //Generate new points at the intersections between segments + //This is necessary for the light volume to generate properly on some subs + for (int i = 0; i < visibleSegments.Count; i++) { - //ignore intersections between parallel axis-aligned segments - if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && - visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) - { - continue; - } + Vector2 p1a = visibleSegments[i].Start.WorldPos; + Vector2 p1b = visibleSegments[i].End.WorldPos; - Vector2 p2a = visibleSegments[j].Start.WorldPos; - Vector2 p2b = visibleSegments[j].End.WorldPos; - - if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || - Vector2.DistanceSquared(p1a, p2b) < 5.0f || - Vector2.DistanceSquared(p1b, p2a) < 5.0f || - Vector2.DistanceSquared(p1b, p2b) < 5.0f) + for (int j = i + 1; j < visibleSegments.Count; j++) { - continue; - } - - bool intersects; - Vector2 intersection = Vector2.Zero; - if (visibleSegments[i].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); - } - else if (visibleSegments[j].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); - } - else - { - intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); - } - - if (intersects) - { - SegmentPoint start = visibleSegments[i].Start; - SegmentPoint end = visibleSegments[i].End; - SegmentPoint mid = new SegmentPoint(intersection, null); - if (visibleSegments[i].ConvexHull?.ParentEntity?.Submarine != null) - { - mid.Pos -= visibleSegments[i].ConvexHull.ParentEntity.Submarine.DrawPosition; - } - - if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || - Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + //ignore intersections between parallel axis-aligned segments + if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && + visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) { continue; } - Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) - { - IsHorizontal = visibleSegments[i].IsHorizontal, - }; + Vector2 p2a = visibleSegments[j].Start.WorldPos; + Vector2 p2b = visibleSegments[j].End.WorldPos; - Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || + Vector2.DistanceSquared(p1a, p2b) < 5.0f || + Vector2.DistanceSquared(p1b, p2a) < 5.0f || + Vector2.DistanceSquared(p1b, p2b) < 5.0f) { - IsHorizontal = visibleSegments[i].IsHorizontal - }; + continue; + } - visibleSegments[i] = seg1; - visibleSegments.Insert(i + 1, seg2); + bool intersects; + Vector2 intersection = Vector2.Zero; + if (visibleSegments[i].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); + } + else if (visibleSegments[j].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); + } + else + { + intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection); + } + + if (intersects) + { + SegmentPoint start = visibleSegments[i].Start; + SegmentPoint end = visibleSegments[i].End; + SegmentPoint mid = new SegmentPoint(intersection, null); + mid.Pos -= visibleSegments[i].SubmarineDrawPos; + + if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || + Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + { + continue; + } + + Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal, + }; + + Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal + }; + + visibleSegments[i] = seg1; + visibleSegments.Insert(i + 1, seg2); + i--; + break; + } + } + } + + points.Clear(); + //remove segments that fall out of bounds + for (int i = 0; i < visibleSegments.Count; i++) + { + Segment s = visibleSegments[i]; + if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + { + visibleSegments.RemoveAt(i); i--; - break; + } + else + { + points.Add(s.Start); + points.Add(s.End); } } - } - points.Clear(); - //remove segments that fall out of bounds - for (int i = 0; i < visibleSegments.Count; i++) - { - Segment s = visibleSegments[i]; - if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + //remove points that are very close to each other + for (int i = 0; i < points.Count; i++) { - visibleSegments.RemoveAt(i); - i--; + for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) + { + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } + } } - else + + var compareCCW = new CompareSegmentPointCW(drawPos); + try { - points.Add(s.Start); - points.Add(s.End); + points.Sort(compareCCW); } - } + catch (Exception e) + { + StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); + foreach (SegmentPoint sp in points) + { + sb.AppendLine(sp.Pos.ToString()); + } + DebugConsole.ThrowError(sb.ToString(), e); + } + + visibleSegments.Sort((s1, s2) => + MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) + .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); + + verts.Clear(); + foreach (SegmentPoint p in points) + { + Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); + Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; + + //do two slightly offset raycasts to hit the segment itself and whatever's behind it + var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + if (intersection1.index < 0) { return; } + var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); + if (intersection2.index < 0) { return; } + + Segment seg1 = visibleSegments[intersection1.index]; + Segment seg2 = visibleSegments[intersection2.index]; + + bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; + bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + + if (isPoint1 && isPoint2) + { + //hit at the current segmentpoint -> place the segmentpoint into the list + verts.Add(p.WorldPos); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + else if (intersection1.index != intersection2.index) + { + //the raycasts landed on different segments + //we definitely want to generate new geometry here + verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); + verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + //if neither of the conditions above are met, we just assume + //that the raycasts both resulted on the same segment + //and creating geometry here would be wasteful + } + } //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) + for (int i = 0; i < verts.Count - 1; i++) { - for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) + for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + if (Math.Abs(verts[i].X - verts[j].X) < 6 && + Math.Abs(verts[i].Y - verts[j].Y) < 6) { - points.RemoveAt(j); + verts.RemoveAt(j); } } } - - var compareCCW = new CompareSegmentPointCW(drawPos); - try - { - points.Sort(compareCCW); - } - catch (Exception e) - { - StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); - foreach (SegmentPoint sp in points) - { - sb.AppendLine(sp.Pos.ToString()); - } - DebugConsole.ThrowError(sb.ToString(), e); - } - - visibleSegments.Sort((s1, s2) => - MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) - .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); - - output.Clear(); - foreach (SegmentPoint p in points) - { - Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); - Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; - - //do two slightly offset raycasts to hit the segment itself and whatever's behind it - var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); - if (intersection1.index < 0) { return null; } - var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); - if (intersection2.index < 0) { return null; } - - Segment seg1 = visibleSegments[intersection1.index]; - Segment seg2 = visibleSegments[intersection2.index]; - - bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; - bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; - - if (isPoint1 && isPoint2) - { - //hit at the current segmentpoint -> place the segmentpoint into the list - output.Add(p.WorldPos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - else if (intersection1.index != intersection2.index) - { - //the raycasts landed on different segments - //we definitely want to generate new geometry here - output.Add(isPoint1 ? p.WorldPos : intersection1.pos); - output.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in convexHullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - //if neither of the conditions above are met, we just assume - //that the raycasts both resulted on the same segment - //and creating geometry here would be wasteful - } - - //remove points that are very close to each other - for (int i = 0; i < output.Count - 1; i++) - { - for (int j = Math.Min(i + 4, output.Count - 1); j > i; j--) - { - if (Math.Abs(output[i].X - output[j].X) < 6 && - Math.Abs(output[i].Y - output[j].Y) < 6) - { - output.RemoveAt(j); - } - } - } - - return output; + calculatedDrawPos = drawPos; + state = LightVertexState.PendingVertexRecalculation; } private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) @@ -954,7 +994,7 @@ namespace Barotrauma.Lights } else { - intersects = MathUtils.GetLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection); + intersects = MathUtils.GetLineSegmentIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection); } if (intersects) @@ -987,8 +1027,7 @@ namespace Barotrauma.Lights indices = new short[indexCount]; } - Vector2 drawPos = position; - if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } + Vector2 drawPos = calculatedDrawPos; float cosAngle = (float)Math.Cos(Rotation); float sinAngle = -(float)Math.Sin(Rotation); @@ -1042,7 +1081,7 @@ namespace Barotrauma.Lights //calculate normal of first segment Vector2 nDiff1 = vertex - nextVertex; - float tx = nDiff1.X; nDiff1.X = -nDiff1.Y; nDiff1.Y = tx; + nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X); nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it @@ -1050,21 +1089,23 @@ namespace Barotrauma.Lights //calculate normal of second segment Vector2 nDiff2 = prevVertex - vertex; - tx = nDiff2.X; nDiff2.X = -nDiff2.Y; nDiff2.Y = tx; - nDiff2 /= Math.Max(Math.Abs(nDiff2.X),Math.Abs(nDiff2.Y)); + nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X); + nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2; //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - Vector2 nDiff = nDiff1 * 40.0f; - if (MathUtils.GetLineIntersection(vertex + (nDiff1 * 40.0f), nextVertex + (nDiff1 * 40.0f), vertex + (nDiff2 * 40.0f), prevVertex + (nDiff2 * 40.0f), true, out Vector2 intersection)) + float blurDistance = 40.0f; + Vector2 nDiff = nDiff1 * blurDistance; + if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) { nDiff = intersection - vertex; - if (nDiff.LengthSquared() > 10000.0f) + if (nDiff.LengthSquared() > 100.0f * 100.0f) { - nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); nDiff *= 100.0f; + nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); + nDiff *= 100.0f; } } @@ -1162,7 +1203,6 @@ namespace Barotrauma.Lights } translateVertices = Vector2.Zero; - rotateVertices = 0.0f; prevCalculatedPosition = position; prevCalculatedRotation = rotation; } @@ -1340,31 +1380,41 @@ namespace Barotrauma.Lights if (NeedsRecalculation && allowRecalculation) { - recalculationCount++; - var verts = FindRaycastHits(); - if (verts == null) + if (state == LightVertexState.UpToDate) { -#if DEBUG - DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); -#endif - Enabled = false; - return; + recalculationCount++; + FindRaycastHits(); } + else if (state == LightVertexState.PendingVertexRecalculation) + { + if (verts == null) + { + #if DEBUG + DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); + #endif + Enabled = false; + return; + } - CalculateLightVertices(verts); + CalculateLightVertices(verts); - LastRecalculationTime = (float)Timing.TotalTime; - NeedsRecalculation = false; + LastRecalculationTime = (float)Timing.TotalTime; + NeedsRecalculation = needsRecalculationWhenUpToDate; + needsRecalculationWhenUpToDate = false; + + state = LightVertexState.UpToDate; + } } + if (vertexCount == 0) { return; } + Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(rotateVertices - MathHelper.ToRadians(LightSourceParams.Rotation)) * + Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; - if (vertexCount == 0) { return; } lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f; if (OverrideLightTexture != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index b150f9b64..783c6d66a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -738,8 +738,8 @@ namespace Barotrauma spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - Vector2 topLeft = rectCenter + viewOffset; - Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(Width, Height)); + Vector2 topLeft = rectCenter + viewOffset - rect.Location.ToVector2(); + Vector2 bottomRight = topLeft + new Vector2(Width, Height); Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index c229c7011..18e2bef75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -56,12 +56,12 @@ namespace Barotrauma } private static readonly List roundSounds = new List(); + private static readonly Dictionary roundSoundByPath = new Dictionary(); public static RoundSound? Load(ContentXElement element, bool stream = false) { if (GameMain.SoundManager?.Disabled ?? true) { return null; } var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound"); - if (filename is null) { string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; @@ -70,7 +70,11 @@ namespace Barotrauma return null; } - Sound? existingSound = roundSounds.Find(s => s.Filename == filename?.FullPath && s.Stream == stream && s.Sound is { Disposed: false })?.Sound; + Sound? existingSound = null; + if (roundSoundByPath.TryGetValue(filename.FullPath, out RoundSound? rs) && rs.Sound is { Disposed: false }) + { + existingSound = rs.Sound; + } if (existingSound is null) { @@ -99,7 +103,10 @@ namespace Barotrauma } RoundSound newSound = new RoundSound(element, existingSound); - + if (filename is not null && !newSound.Stream) + { + roundSoundByPath.TryAdd(filename.FullPath, newSound); + } roundSounds.Add(newSound); return newSound; } @@ -124,24 +131,14 @@ namespace Barotrauma roundSound.Sound = existingSound; } - private static void Remove(RoundSound roundSound) - { - #warning TODO: what is going on here???? - roundSound.Sound?.Dispose(); - - if (roundSounds.Contains(roundSound)) { roundSounds.Remove(roundSound); } - foreach (RoundSound otherSound in roundSounds) - { - if (otherSound.Sound == roundSound.Sound) { otherSound.Sound = null; } - } - } - public static void RemoveAllRoundSounds() { - for (int i = roundSounds.Count - 1; i >= 0; i--) + foreach (var roundSound in roundSounds) { - Remove(roundSounds[i]); + roundSound.Sound?.Dispose(); } + roundSounds.Clear(); + roundSoundByPath.Clear(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 9bdf822ca..9f1b8c657 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -55,21 +55,11 @@ namespace Barotrauma { if (!CastShadow) { return; } - if (convexHulls == null) - { - convexHulls = new List(); - } - - Vector2 halfSize = size / 2; - Vector2[] verts = new Vector2[] - { - position + new Vector2(-halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, -halfSize.Y), - position + new Vector2(-halfSize.X, -halfSize.Y), - }; - - var h = new ConvexHull(verts, Color.Black, this); + convexHulls ??= new List(); + var h = new ConvexHull( + new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), + IsHorizontal, + this); if (Math.Abs(rotation) > 0.001f) { h.Rotate(position, rotation); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 614365566..795401f2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -116,11 +116,11 @@ namespace Barotrauma foreach (MapEntity e in entitiesToRender) { - if (!e.DrawOverWater) continue; + if (!e.DrawOverWater) { continue; } if (predicate != null) { - if (!predicate(e)) continue; + if (!predicate(e)) { continue; } } e.Draw(spriteBatch, editing, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index 57bbe7a9e..27242fb85 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -1,6 +1,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -18,7 +19,7 @@ namespace Barotrauma return; } - List subsToMove = submarine.GetConnectedSubs(); + var subsToMove = submarine.GetConnectedSubs(); foreach (Submarine dockedSub in subsToMove) { if (dockedSub == submarine) { continue; } @@ -51,7 +52,6 @@ namespace Barotrauma sub.PhysicsBody.SetTransformIgnoreContacts(sub.PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(moveAmount), 0.0f); } } - if (closestSub != null && subsToMove.Contains(closestSub)) { GameMain.GameScreen.Cam.Position += moveAmount; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index cded39e96..bf77b5ccc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -67,6 +67,10 @@ namespace Barotrauma else if (ConnectedDoor != null) { sprite = iconSprites["Door"]; + if (ConnectedDoor.IsHorizontal && Ladders == null) + { + clr = Color.Yellow; + } } else if (Ladders != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2e938e4a7..bf6adf1cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3070,8 +3070,12 @@ namespace Barotrauma.Networking if (votingInterface != null) { votingInterface.Update(deltaTime); - if (!votingInterface.VoteRunning) + if (!votingInterface.VoteRunning || votingInterface.TimedOut) { + if (votingInterface.TimedOut) + { + DebugConsole.AddWarning($"Voting interface timed out."); + } votingInterface.Remove(); votingInterface = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index 56138ec83..259399b16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -53,7 +53,7 @@ namespace Barotrauma.Networking } } - private readonly ref struct LobbyDataChangedEventHandler + private readonly struct LobbyDataChangedEventHandler : IDisposable { private readonly Action action; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 2039ad51d..57c19e71c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Networking int botSpawnMode = 0, bool? useRespawnShuttle = null) { - if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; + if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; } IWriteMessage outMsg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 765d1a5d7..23b4c5b10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -253,12 +253,9 @@ namespace Barotrauma.Networking //in push-to-talk mode, InputType.Voice uses the active chat mode bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice); bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; - if (pttDown || captureTimer <= 0) - { - ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode; - } if (pttDown) { + ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode; allowEnqueue = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 28d0461ad..6c664b86d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -115,17 +115,20 @@ namespace Barotrauma.Networking float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f; bool spectating = Character.Controlled == null; float rangeMultiplier = spectating ? 2.0f : 1.0f; - WifiComponent radio = null; + WifiComponent senderRadio = null; var messageType = - !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? - ChatMessageType.Radio : ChatMessageType.Default; + !client.VoipQueue.ForceLocal && + ChatMessage.CanUseRadio(client.Character, out senderRadio) && + ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && + senderRadio.CanReceive(recipientRadio) ? + ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { - client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier); + client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier); if (distanceFactor > RangeNear && !spectating) { //noise starts increasing exponentially after 40% range diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 466b9418b..470219c9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -348,11 +348,13 @@ namespace Barotrauma switch (voteType) { case VoteType.PurchaseAndSwitchSub: - GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); + if (GameMain.GameSession.TryPurchaseSubmarine(subInfo)) + { + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); + } break; case VoteType.PurchaseSub: - GameMain.GameSession.PurchaseSubmarine(subInfo); + GameMain.GameSession.TryPurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 8af7d4e14..3f8492120 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -48,19 +49,23 @@ namespace Barotrauma.Particles private Vector2 drawPosition; private float drawRotation; + + private Vector2 colliderRadius; private Hull currentHull; private List hullGaps; private bool hasSubEmitters; - private List subEmitters = new List(); + private readonly List subEmitters = new List(); private float animState; private int animFrame; private float collisionUpdateTimer; + private bool changesSize; + public bool HighQualityCollisionDetection; public Vector4 ColorMultiplier; @@ -127,7 +132,10 @@ namespace Barotrauma.Particles position = (tracerPoints.Item1 + tracerPoints.Item2) / 2; } + RefreshColliderSize(); + sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f); + changesSize = !sizeChange.NearlyEquals(Vector2.Zero); this.position = position; prevPosition = position; @@ -256,8 +264,12 @@ namespace Barotrauma.Particles } } - size.X += sizeChange.X * deltaTime; - size.Y += sizeChange.Y * deltaTime; + if (changesSize) + { + size.X += sizeChange.X * deltaTime; + size.Y += sizeChange.Y * deltaTime; + RefreshColliderSize(); + } if (UseMiddleColor) { @@ -344,11 +356,11 @@ namespace Barotrauma.Particles { Rectangle hullRect = currentHull.WorldRect; Vector2 collisionNormal = Vector2.Zero; - if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) + if (velocity.Y < 0.0f && position.Y - colliderRadius.Y < hullRect.Y - hullRect.Height) { collisionNormal = new Vector2(0.0f, 1.0f); } - else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) + else if (velocity.Y > 0.0f && position.Y + colliderRadius.Y > hullRect.Y) { collisionNormal = new Vector2(0.0f, -1.0f); } @@ -378,11 +390,11 @@ namespace Barotrauma.Particles } collisionNormal = Vector2.Zero; - if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) + if (velocity.X < 0.0f && position.X - colliderRadius.X < hullRect.X) { collisionNormal = new Vector2(1.0f, 0.0f); } - else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right) + else if (velocity.X > 0.0f && position.X + colliderRadius.X > hullRect.Right) { collisionNormal = new Vector2(-1.0f, 0.0f); } @@ -431,6 +443,13 @@ namespace Barotrauma.Particles return UpdateResult.Normal; } + private void RefreshColliderSize() + { + if (!prefab.UseCollision) { return; } + colliderRadius = new Vector2(prefab.CollisionRadius); + if (!prefab.InvariantCollisionSize) { colliderRadius *= size; } + } + private void ApplyDrag(float dragCoefficient, float deltaTime) { Vector2 relativeVel = velocity; @@ -475,11 +494,11 @@ namespace Barotrauma.Particles { if (collisionNormal.X > 0.0f) { - position.X = Math.Max(position.X, prevHullRect.X + prefab.CollisionRadius * size.X); + position.X = Math.Max(position.X, prevHullRect.X + colliderRadius.X); } else { - position.X = Math.Min(position.X, prevHullRect.Right - prefab.CollisionRadius * size.X); + position.X = Math.Min(position.X, prevHullRect.Right - colliderRadius.X); } velocity.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); @@ -488,11 +507,11 @@ namespace Barotrauma.Particles { if (collisionNormal.Y > 0.0f) { - position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + prefab.CollisionRadius * size.Y); + position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + colliderRadius.Y); } else { - position.Y = Math.Min(position.Y, prevHullRect.Y - prefab.CollisionRadius * size.Y); + position.Y = Math.Min(position.Y, prevHullRect.Y - colliderRadius.Y); } velocity.X *= (1.0f - prefab.Friction); @@ -513,26 +532,26 @@ namespace Barotrauma.Particles if (position.Y < center.Y) { - position.Y = hullRect.Y - hullRect.Height - prefab.CollisionRadius; + position.Y = hullRect.Y - hullRect.Height - colliderRadius.Y; velocity.X *= (1.0f - prefab.Friction); velocity.Y = -velocity.Y * prefab.Restitution; } else if (position.Y > center.Y) { - position.Y = hullRect.Y + prefab.CollisionRadius; + position.Y = hullRect.Y + colliderRadius.Y; velocity.X *= (1.0f - prefab.Friction); velocity.Y = -velocity.Y * prefab.Restitution; } if (position.X < center.X) { - position.X = hullRect.X - prefab.CollisionRadius; + position.X = hullRect.X - colliderRadius.X; velocity.X = -velocity.X * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); } else if (position.X > center.X) { - position.X = hullRect.X + hullRect.Width + prefab.CollisionRadius; + position.X = hullRect.X + hullRect.Width + colliderRadius.X; velocity.X = -velocity.X * prefab.Restitution; velocity.Y *= (1.0f - prefab.Friction); } @@ -559,11 +578,12 @@ namespace Barotrauma.Particles Color currColor = new Color(color.ToVector4() * ColorMultiplier); - if (prefab.Sprites[spriteIndex] is SpriteSheet) + Vector2 drawPos = new Vector2(drawPosition.X, -drawPosition.Y); + if (prefab.Sprites[spriteIndex] is SpriteSheet sheet) { - ((SpriteSheet)prefab.Sprites[spriteIndex]).Draw( + sheet.Draw( spriteBatch, animFrame, - new Vector2(drawPosition.X, -drawPosition.Y), + drawPos, currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); @@ -571,11 +591,23 @@ namespace Barotrauma.Particles else { prefab.Sprites[spriteIndex].Draw(spriteBatch, - new Vector2(drawPosition.X, -drawPosition.Y), + drawPos, currColor * (currColor.A / 255.0f), prefab.Sprites[spriteIndex].Origin, drawRotation, drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); } + + /*if (GameMain.DebugDraw && prefab.UseCollision) + { + GUI.DrawLine(spriteBatch, + drawPos - Vector2.UnitX * colliderRadius.X, + drawPos + Vector2.UnitX * colliderRadius.X, + Color.Gray); + GUI.DrawLine(spriteBatch, + drawPos - Vector2.UnitY * colliderRadius.Y, + drawPos + Vector2.UnitY * colliderRadius.Y, + Color.Gray); + }*/ } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 0b7aa21af..23fb1ea3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -135,6 +135,9 @@ namespace Barotrauma.Particles [Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] public float CollisionRadius { get; private set; } + [Editable, Serialize(false, IsPropertySaveable.No, description: "If enabled, the size (or changes in size) of the particle doesn't affect the size of the collider.")] + public bool InvariantCollisionSize { get; private set; } + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle collide with the walls of the submarine and the level.")] public bool UseCollision { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 5745b0152..f418b577c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -15,7 +15,7 @@ namespace Barotrauma private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetFinal; - private readonly Effect damageEffect; + public readonly Effect DamageEffect; private readonly Texture2D damageStencil; private readonly Texture2D distortTexture; @@ -39,7 +39,7 @@ namespace Barotrauma }; //var blurEffect = LoadEffect("Effects/blurshader"); - damageEffect = EffectLoader.Load("Effects/damageshader"); + DamageEffect = EffectLoader.Load("Effects/damageshader"); PostProcessEffect = EffectLoader.Load("Effects/postprocess"); GradientEffect = EffectLoader.Load("Effects/gradientshader"); GrainEffect = EffectLoader.Load("Effects/grainshader"); @@ -47,9 +47,9 @@ namespace Barotrauma BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); - damageEffect.Parameters["xStencil"].SetValue(damageStencil); - damageEffect.Parameters["aMultiplier"].SetValue(50.0f); - damageEffect.Parameters["cMultiplier"].SetValue(200.0f); + DamageEffect.Parameters["xStencil"].SetValue(damageStencil); + DamageEffect.Parameters["aMultiplier"].SetValue(50.0f); + DamageEffect.Parameters["cMultiplier"].SetValue(200.0f); distortTexture = TextureLoader.FromFile("Content/Effects/distortnormals.png"); PostProcessEffect.Parameters["xDistortTexture"].SetValue(distortTexture); @@ -345,12 +345,13 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); sw.Restart(); + DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"]; spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, - damageEffect, + DamageEffect, cam.Transform); - Submarine.DrawDamageable(spriteBatch, damageEffect, false); + Submarine.DrawDamageable(spriteBatch, DamageEffect, false); spriteBatch.End(); sw.Stop(); @@ -377,7 +378,7 @@ namespace Barotrauma { graphics.DepthStencilState = DepthStencilState.None; graphics.SamplerStates[0] = SamplerState.LinearWrap; - graphics.BlendState = Lights.CustomBlendStates.Multiplicative; + graphics.BlendState = CustomBlendStates.Multiplicative; Quad.UseBasicEffect(GameMain.LightManager.LightMap); Quad.Render(); } @@ -408,6 +409,7 @@ namespace Barotrauma { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; + GameMain.LightManager.LosEffect.Parameters["blurDistance"].SetValue(0.005f); GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground); GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture); GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); @@ -433,8 +435,11 @@ namespace Barotrauma graphics.BlendState = BlendState.NonPremultiplied; graphics.SamplerStates[0] = SamplerState.PointClamp; + graphics.SamplerStates[1] = SamplerState.PointClamp; GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); Quad.Render(); + graphics.SamplerStates[0] = SamplerState.LinearWrap; + graphics.SamplerStates[1] = SamplerState.LinearWrap; } if (Character.Controlled is { } character) @@ -518,6 +523,11 @@ namespace Barotrauma spriteBatch.End(); } + if (GameMain.LightManager.DebugLos) + { + GameMain.LightManager.DebugDrawLos(spriteBatch, cam); + } + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); sw.Restart(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index e812023ea..959881cc8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -59,7 +59,6 @@ namespace Barotrauma private GUIImage playstyleBanner; private GUITextBlock playstyleDescription; - private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/"; private readonly GUIComponent remoteContentContainer; private XDocument remoteContentDoc; @@ -82,6 +81,7 @@ namespace Barotrauma { GameMain.Instance.ResolutionChanged += () => { + SetMenuTabPositioning(); CreateHostServerFields(); CreateCampaignSetupUI(); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); @@ -426,31 +426,33 @@ namespace Barotrauma var relativeSize = new Vector2(0.6f, 0.65f); var minSize = new Point(600, 400); var maxSize = new Point(2000, 1500); - var anchor = Anchor.CenterRight; - var pivot = Pivot.CenterRight; - Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); - - menuTabs = new Dictionary(); + var anchor = Anchor.Center; + var pivot = Pivot.Center; + Vector2 relativeOffset = new Vector2(0.05f, 0.0f); - menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, - style: null); - menuTabs[Tab.Settings].CanBeFocused = false; - - menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs = new Dictionary + { + [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }, + style: null) + { + CanBeFocused = false + }, + [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }), + [Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }) + }; CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) - { RelativeOffset = relativeSpacing }); + { RelativeOffset = relativeOffset }); CreateHostServerFields(); //---------------------------------------------------------------------- - menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }); CreateTutorialTab(); this.game = game; @@ -471,6 +473,20 @@ namespace Barotrauma SelectTab(Tab.Empty); return true; }; + + SetMenuTabPositioning(); + } + + private void SetMenuTabPositioning() + { + foreach (GUIFrame menuTab in menuTabs.Values) + { + var anchor = GUI.IsUltrawide ? Anchor.Center : Anchor.CenterRight; + var pivot = GUI.IsUltrawide ? Pivot.Center : Pivot.CenterRight; + Vector2 relativeOffset = GUI.IsUltrawide ? Vector2.Zero : new Vector2(0.05f, 0.0f); + menuTab.RectTransform.SetPosition(anchor, pivot); + menuTab.RectTransform.RelativeOffset = relativeOffset; + } } private void CreateTutorialTab() @@ -1508,10 +1524,11 @@ namespace Barotrauma private void FetchRemoteContent() { - if (string.IsNullOrEmpty(RemoteContentUrl)) { return; } + string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl; + if (string.IsNullOrEmpty(remoteContentUrl)) { return; } try { - var client = new RestClient(RemoteContentUrl); + var client = new RestClient(remoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), RemoteContentReceived); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 74ad6abde..886f4311f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -751,12 +751,20 @@ namespace Barotrauma }; ServerMessage.OnTextChanged += (textBox, text) => { - Vector2 textSize = textBox.Font.MeasureString(textBox.WrappedText); - textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); - serverMessageContainer.UpdateScrollBarSize(); serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + RefreshServerInfoSize(); return true; }; + ServerMessage.RectTransform.SizeChanged += RefreshServerInfoSize; + + void RefreshServerInfoSize() + { + serverMessageHint.Visible = !ServerMessage.Selected && !ServerMessage.Readonly && string.IsNullOrWhiteSpace(ServerMessage.Text); + Vector2 textSize = ServerMessage.Font.MeasureString(ServerMessage.WrappedText); + ServerMessage.RectTransform.NonScaledSize = new Point(ServerMessage.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); + serverMessageContainer.UpdateScrollBarSize(); + } + ServerMessage.OnEnterPressed += (textBox, text) => { string str = textBox.Text; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 5d1831408..a47aa5922 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -812,7 +812,7 @@ namespace Barotrauma private bool SortList(GUIButton button, object obj) { - if (!(obj is ColumnLabel sortBy)) { return false; } + if (obj is not ColumnLabel sortBy) { return false; } SortList(sortBy, toggle: true); return true; } @@ -848,8 +848,7 @@ namespace Barotrauma { if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } - int comparison = sortedAscending ? 1 : -1; - return CompareServer(sortBy, s1, s2) * comparison; + return CompareServer(sortBy, s1, s2, sortedAscending); }); } @@ -857,22 +856,31 @@ namespace Barotrauma { var children = serverList.Content.RectTransform.Children.Reverse().ToList(); - int comparison = sortedAscending ? 1 : -1; foreach (var child in children) { if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } - if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0) + if (CompareServer(sortedBy, serverInfo, serverInfo2, sortedAscending) >= 0) { var index = serverList.Content.RectTransform.GetChildIndex(child); - component.RectTransform.RepositionChildInHierarchy(index + 1); + component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1)); return; } } component.RectTransform.SetAsFirstChild(); } - private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2) + private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2, bool ascending) { + //always put servers with unknown ping at the bottom (unless we're specifically sorting by ping) + //servers without a ping are often unreachable/spam + bool s1HasPing = s1.Ping.IsSome(); + bool s2HasPing = s2.Ping.IsSome(); + if (s1HasPing != s2HasPing) + { + return s1HasPing ? -1 : 1; + } + + int comparison = ascending ? 1 : -1; switch (sortBy) { case ColumnLabel.ServerListCompatible: @@ -880,18 +888,18 @@ namespace Barotrauma bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); if (s1Compatible == s2Compatible) { return 0; } - return s1Compatible ? -1 : 1; + return (s1Compatible ? -1 : 1) * comparison; case ColumnLabel.ServerListHasPassword: if (s1.HasPassword == s2.HasPassword) { return 0; } - return s1.HasPassword ? 1 : -1; + return (s1.HasPassword ? 1 : -1) * comparison; case ColumnLabel.ServerListName: // I think we actually want culture-specific sorting here? - return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture); + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * comparison; case ColumnLabel.ServerListRoundStarted: if (s1.GameStarted == s2.GameStarted) { return 0; } - return s1.GameStarted ? 1 : -1; + return (s1.GameStarted ? 1 : -1) * comparison; case ColumnLabel.ServerListPlayers: - return s2.PlayerCount.CompareTo(s1.PlayerCount); + return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison; case ColumnLabel.ServerListPing: return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch { @@ -899,7 +907,7 @@ namespace Barotrauma (true, true) => s2Ping.CompareTo(s1Ping), (false, true) => 1, (true, false) => -1 - }; + } * comparison; default: return 0; } @@ -1504,9 +1512,41 @@ namespace Barotrauma private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) { + const int MaxAllowedPlayers = 1000; + const int MaxAllowedSimilarServers = 10; + const float MinSimilarityPercentage = 0.8f; + + if (string.IsNullOrWhiteSpace(serverInfo.ServerName)) { return; } if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; } if (serverInfo.PlayerCount < 0) { return; } if (serverInfo.MaxPlayers <= 0) { return; } + //no way a legit server can have this many players + if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; } + + int similarServerCount = 0; + string serverInfoStr = getServerInfoStr(serverInfo); + foreach (var serverElement in serverList.Content.Children) + { + if (!serverElement.Visible) { continue; } + if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; } + if (ToolBox.LevenshteinDistance(serverInfoStr, getServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage)) + { + similarServerCount++; + if (similarServerCount > MaxAllowedSimilarServers) + { + DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server."); + break; + } + } + } + if (similarServerCount > MaxAllowedSimilarServers) { return; } + + static string getServerInfoStr(ServerInfo serverInfo) + { + string str = serverInfo.ServerName + serverInfo.ServerMessage + serverInfo.MaxPlayers; + if (str.Length > 200) { return str.Substring(0, 200); } + return str; + } RemoveMsgFromServerList(MsgUserData.RefreshingServerList); RemoveMsgFromServerList(MsgUserData.NoServers); @@ -1522,7 +1562,6 @@ namespace Barotrauma UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } - InsertServer(serverInfo, serverFrame); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1736,7 +1775,7 @@ namespace Barotrauma AddToFavoriteServers(serverInfo); } - SortList(sortedBy, toggle: false); + InsertServer(serverInfo, serverFrame); FilterServers(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 712f3bef7..a9b03c0ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -525,6 +525,11 @@ namespace Barotrauma GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); } WayPoint.ShowWayPoints = true; + var matchingTickBox = showEntitiesTickBoxes?.Find(tb => tb.UserData as string == "waypoint"); + if (matchingTickBox != null) + { + matchingTickBox.Selected = true; + } generateWaypointsVerification.Close(); return true; }; @@ -2847,7 +2852,7 @@ namespace Barotrauma { OnClicked = (button, o) => { - var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage) + var requiredPackages = MapEntity.mapEntityList.Select(e => e?.Prefab?.ContentPackage) .Where(cp => cp != null) .Distinct().OfType().Select(p => p.Name).ToHashSet(); var tickboxes = requiredContentPackList.Content.Children.OfType().ToArray(); @@ -5791,7 +5796,10 @@ namespace Barotrauma { item.SetTransform(dummyCharacter.SimPosition, 0.0f); item.UpdateTransform(); - item.SetTransform(item.body.SimPosition, 0.0f); + if (item.body != null) + { + item.SetTransform(item.body.SimPosition, 0.0f); + } //wires need to be updated for the last node to follow the player during rewiring Wire wire = item.GetComponent(); @@ -5904,6 +5912,11 @@ namespace Barotrauma spriteBatch.End(); } + if (GameMain.LightManager.DebugLos) + { + GameMain.LightManager.DebugDrawLos(spriteBatch, cam); + } + //-------------------- HUD ----------------------------- spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 4df190a1c..44ba1c654 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -343,12 +343,11 @@ namespace Barotrauma if (property.PropertyType == typeof(string) && value == null) { value = ""; - } + } Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier(); Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); - LocalizedString displayName = - TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); + LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); if (displayName.IsNullOrEmpty()) { Editable editable = property.GetAttribute(); @@ -380,10 +379,14 @@ namespace Barotrauma } LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); - if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType) + if (entity.GetType() != property.PropertyInfo.DeclaringType) { Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); - toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description"); + var toolTipForDerivedClass = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description"); + if (!toolTipForDerivedClass.IsNullOrEmpty()) + { + toolTip = toolTipForDerivedClass; + } } if (toolTip.IsNullOrEmpty()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index bac896004..0159decbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -189,6 +189,7 @@ namespace Barotrauma.Steam ModProject modProject = new ModProject(tempPkg) { ModVersion = modVersion, + Name = title, ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion) }; modProject.Save(stagingFileListPath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index d8525089f..a04a3e1bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -9,6 +9,7 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Steamworks; using Directory = Barotrauma.IO.Directory; using ItemOrPackage = Barotrauma.Either; using Path = Barotrauma.IO.Path; @@ -157,7 +158,7 @@ namespace Barotrauma.Steam } var selectedTitle = - new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name, + new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), localPackage.Name, font: GUIStyle.LargeFont); if (workshopItem.Id != 0) { @@ -212,7 +213,7 @@ namespace Barotrauma.Steam }; Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont); - var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name); + var titleTextBox = new GUITextBox(NewItemRectT(rightTop), localPackage.Name); Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont); var descriptionTextBox @@ -320,7 +321,9 @@ namespace Barotrauma.Steam workshopItem.Id == 0 ? Steamworks.Ugc.Editor.NewCommunityFile : new Steamworks.Ugc.Editor(workshopItem.Id); - ugcEditor = ugcEditor.WithTitle(titleTextBox.Text) + ugcEditor = ugcEditor + .InLanguage(SteamUtils.SteamUILanguage ?? string.Empty) + .WithTitle(titleTextBox.Text) .WithDescription(descriptionTextBox.Text) .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value)) .WithChangeLog(changeNoteTextBox.Text) diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb index 37e87a017..4b95d883f 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb index f51787721..5cffd1d1a 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb index 7f09d5fe6..02d699442 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb index f8f1b3f6a..b1bb40f05 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 8e8b611dd..4b5dccfa1 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.13.2 + 1.0.20.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 92d9fe417..6d8354723 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.13.2 + 1.0.20.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 6f0781293..3ec964651 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture2D xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; - float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; + float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_4_0_level_9_1 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 solidColorStencil(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index 3a4242a3a..69370113c 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * inColor; - float4 stencilColor = tex2D(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * inColor; + float4 stencilColor = tex2D(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_2_0 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_2_0 solidColorStencil(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx index f761aa1bc..1e93bd9fb 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader.fx @@ -30,11 +30,18 @@ float xLosAlpha; float4 xColor; +float blurDistance; + float4 mainPS(VertexShaderOutput input) : COLOR0 { float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); - float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords); - + + float4 losColor = xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); + losColor = losColor * 0.25f; + float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( @@ -53,4 +60,4 @@ technique LosShader VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 mainPS(); } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx index a799c320a..a576264d6 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx @@ -30,10 +30,16 @@ float xLosAlpha; float4 xColor; +float blurDistance; + float4 mainPS(VertexShaderOutput input) : COLOR0 { float4 sampleColor = tex2D(TextureSampler, input.TexCoords); - float4 losColor = tex2D(LosSampler, input.TexCoords); + float4 losColor = tex2D(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + losColor += tex2D(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + losColor += tex2D(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + losColor += tex2D(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); + losColor = losColor * 0.25f; float obscureAmount = 1.0f - losColor.r; diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 5fe49bfa9..cec27c949 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.0.13.2 + 1.0.20.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index bd43491ff..f1b4c157c 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.13.2 + 1.0.20.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 083ea367d..592c084d0 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.13.2 + 1.0.20.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 5eff578d3..c597f599e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -60,6 +60,10 @@ namespace Barotrauma { distance = Math.Min(distance, Vector2.Distance(recipient.Character.ViewTarget.WorldPosition, WorldPosition)); } + if (ViewTarget != null && ViewTarget != this) + { + distance = Math.Min(distance, Vector2.Distance(comparePosition, ViewTarget.WorldPosition)); + } float priority = 1.0f - MathUtils.InverseLerp( NetConfig.HighPrioCharacterPositionUpdateDistance, @@ -155,8 +159,6 @@ namespace Barotrauma memInput.RemoveAt(memInput.Count - 1); - TransformCursorPos(); - if ((dequeuedInput == InputNetFlags.None || dequeuedInput == InputNetFlags.FacingLeft) && Math.Abs(AnimController.Collider.LinearVelocity.X) < 0.005f && Math.Abs(AnimController.Collider.LinearVelocity.Y) < 0.2f) { while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index 65a5653e8..55e94e312 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -16,11 +17,10 @@ namespace Barotrauma foreach (var kvp in spawnedResources) { msg.WriteByte((byte)kvp.Value.Count); - var rotation = resourceClusters[kvp.Key].Rotation; - msg.WriteSingle(rotation); - foreach (var r in kvp.Value) + msg.WriteSingle(kvp.Value.FirstOrDefault()?.Rotation ?? 0.0f); + foreach (var item in kvp.Value) { - r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } } @@ -28,9 +28,9 @@ namespace Barotrauma { msg.WriteIdentifier(kvp.Key); msg.WriteByte((byte)kvp.Value.Length); - foreach (var i in kvp.Value) + foreach (var item in kvp.Value) { - msg.WriteUInt16(i.ID); + msg.WriteUInt16(item.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index b037e9888..7629dd08f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -1310,6 +1309,10 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { + //disconnected clients can never purchase anything + //(can happen e.g. if someone starts a vote to buy something and then disconnects) + if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; } + Wallet wallet = GetWallet(client); if (!AllowedToManageWallets(client)) { @@ -1359,6 +1362,12 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); modeElement.Add(Bank.Save()); + + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index b611a9b7f..1685b7e4a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -18,11 +18,11 @@ namespace Barotrauma.Items.Components if (item.CanClientAccess(c)) { + lastReceivedTargetForce = null; if (Math.Abs(newTargetForce - targetForce) > 0.01f) { GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction); } - targetForce = newTargetForce; User = c.Character; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 7ba476df9..9403732da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components //already connected, no need to do anything if (Connections[i].Wires.Contains(newWire)) { continue; } - newWire.Connect(Connections[i], true, true); + newWire.TryConnect(Connections[i], true, true); Connections[i].TryAddLink(newWire); var otherConnection = newWire.OtherConnection(Connections[i]); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 3cf292b13..d10033fc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -21,15 +21,13 @@ namespace Barotrauma.Networking public override Voting Voting { get; } - private string serverName; public string ServerName { - get { return serverName; } + get { return ServerSettings.ServerName; } set { if (string.IsNullOrEmpty(value)) { return; } - - serverName = value; + ServerSettings.ServerName = value; } } @@ -133,8 +131,6 @@ namespace Barotrauma.Networking name = name.Substring(0, NetConfig.ServerNameMaxLength); } - this.serverName = name; - LastClientListUpdateID = 0; ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); @@ -1667,38 +1663,54 @@ namespace Barotrauma.Networking //characters or items spawned mid-round don't necessarily exist at the client's end yet if (!c.NeedsMidRoundSync) { - foreach (Character character in Character.CharacterList) + Character clientCharacter = c.Character; + foreach (Character otherCharacter in Character.CharacterList) { - if (!character.Enabled) { continue; } + if (!otherCharacter.Enabled) { continue; } if (c.SpectatePos == null) { - float distSqr = Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition); - if (c.Character.ViewTarget != null) + //not spectating -> + // check if the client's character, or the entity they're viewing, + // is close enough to the other character or the entity the other character is viewing + float distSqr = GetShortestDistance(clientCharacter.WorldPosition, otherCharacter); + if (clientCharacter.ViewTarget != null && clientCharacter.ViewTarget != clientCharacter) { - distSqr = Math.Min(distSqr, Vector2.DistanceSquared(character.WorldPosition, c.Character.ViewTarget.WorldPosition)); + distSqr = Math.Min(distSqr, GetShortestDistance(clientCharacter.ViewTarget.WorldPosition, otherCharacter)); } - if (distSqr >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } + if (distSqr >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; } } - else + else if (otherCharacter != clientCharacter) { - if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= MathUtils.Pow2(character.Params.DisableDistance)) - { - continue; - } + //spectating -> + // check if the position the client is viewing + // is close enough to the other character or the entity the other character is viewing + if (GetShortestDistance(c.SpectatePos.Value, otherCharacter) >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; } } - float updateInterval = character.GetPositionUpdateInterval(c); - c.PositionUpdateLastSent.TryGetValue(character, out float lastSent); + static float GetShortestDistance(Vector2 viewPos, Character targetCharacter) + { + float distSqr = Vector2.DistanceSquared(viewPos, targetCharacter.WorldPosition); + if (targetCharacter.ViewTarget != null && targetCharacter.ViewTarget != targetCharacter) + { + //if the character is viewing something (far-away turret?), + //we might want to send updates about it to the spectating client even though they're far away from the actual character + distSqr = Math.Min(distSqr, Vector2.DistanceSquared(viewPos, targetCharacter.ViewTarget.WorldPosition)); + } + return distSqr; + } + + float updateInterval = otherCharacter.GetPositionUpdateInterval(c); + c.PositionUpdateLastSent.TryGetValue(otherCharacter, out float lastSent); if (lastSent > NetTime.Now) { //sent in the future -> can't be right, remove - c.PositionUpdateLastSent.Remove(character); + c.PositionUpdateLastSent.Remove(otherCharacter); } else { if (lastSent > NetTime.Now - updateInterval) { continue; } } - if (!c.PendingPositionUpdates.Contains(character)) { c.PendingPositionUpdates.Enqueue(character); } + if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); } } foreach (Submarine sub in Submarine.Loaded) @@ -3094,7 +3106,7 @@ namespace Barotrauma.Networking default: if (command != "") { - if (command.ToLower() == serverName.ToLower()) + if (command.ToLower() == ServerName.ToLower()) { //a private message to the host if (OwnerConnection != null) @@ -3149,7 +3161,7 @@ namespace Barotrauma.Networking //msg sent by the server if (senderCharacter == null) { - senderName = serverName; + senderName = ServerName; } else //msg sent by an AI character { @@ -3183,7 +3195,7 @@ namespace Barotrauma.Networking //msg sent by the server if (senderCharacter == null) { - senderName = serverName; + senderName = ServerName; } else //sent by an AI character, not allowed when the game is not running { @@ -3406,33 +3418,35 @@ namespace Barotrauma.Networking } } - public void SwitchSubmarine() + public bool TrySwitchSubmarine() { - if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; } + if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return false; } SubmarineInfo targetSubmarine = subVote.Sub; VoteType voteType = Voting.ActiveVote.VoteType; Client starter = Voting.ActiveVote.VoteStarter; + bool purchaseFailed = false; switch (voteType) { case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: // Pay for submarine - GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); + purchaseFailed = !GameMain.GameSession.TryPurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: break; default: - return; + return false; } - if (voteType != VoteType.PurchaseSub) + if (voteType != VoteType.PurchaseSub && !purchaseFailed) { GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); } - Voting.StopSubmarineVote(true); + Voting.StopSubmarineVote(passed: !purchaseFailed); + return !purchaseFailed; } public void UpdateClientPermissions(Client client) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 55a116898..185b5aad5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -370,6 +370,8 @@ namespace Barotrauma.Networking "192-255", "384-591", "1024-1279", + "4352-4607", //Hangul Jamo + "44032-55215", //Hangul Syllables "19968-21327","21329-40959","13312-19903","131072-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK }; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 4af593e86..c6520b8fa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -42,7 +42,11 @@ namespace Barotrauma { if (passed) { - GameMain.Server?.SwitchSubmarine(); + if (GameMain.Server != null && !GameMain.Server.TrySwitchSubmarine()) + { + passed = false; + State = VoteState.Failed; + } } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs index faf50c98a..e9cb1215d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs @@ -71,6 +71,15 @@ namespace Barotrauma StrikesResetInterval = 60, StrikeThreshold = 6; + private const int MinPacketLimitMultipler = 1; + + private static int GetMaxPacketLimit(ServerSettings settings) + => (int)MathF.Ceiling( + settings.MaxPacketAmount * + MathF.Max( + settings.TickRate / (float)ServerSettings.DefaultTickRate, + MinPacketLimitMultipler)); // Prevent the rate limit multiplier from being less than 1. + /// /// Called when the server receives a packet to start logging how much time it takes to process. /// @@ -122,11 +131,7 @@ namespace Barotrauma private void StartFor(Client client) { - if (!clients.ContainsKey(client)) - { - clients.Add(client, new OffenseData()); - } - + clients.TryAdd(client, new OffenseData()); clients[client].Stopwatch.Start(); } @@ -149,7 +154,7 @@ namespace Barotrauma if (GameMain.Server?.ServerSettings is not { } settings) { return; } // client is sending too many packets, kick them - if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin) + if (data.PacketCount > GetMaxPacketLimit(settings) && settings.MaxPacketAmount > ServerSettings.PacketLimitMin) { AttemptKickClient(client, TextManager.Get("PacketLimitKicked")); clients.Remove(client); @@ -216,7 +221,7 @@ namespace Barotrauma { if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings) { - if (data.PacketCount > settings.MaxPacketAmount * 0.9f) + if (data.PacketCount > GetMaxPacketLimit(settings) * 0.9f) { GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index c42ec7463..4c66ef099 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.0.13.2 + 1.0.20.1 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 e125d449d..a5069b8f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -95,10 +95,7 @@ namespace Barotrauma { get { - if (visibleHulls == null) - { - visibleHulls = Character.GetVisibleHulls(); - } + visibleHulls ??= Character.GetVisibleHulls(); return visibleHulls; } private set @@ -425,14 +422,9 @@ namespace Barotrauma var door = gap.ConnectedDoor; if (door != null) { - if (!door.CanBeTraversed) + if (!pathSteering.CanAccessDoor(door)) { - if (!door.HasAccess(Character)) - { - if (!canAttackDoors) { continue; } - // Treat doors that don't have access to like they were farther, because it will take time to break them. - multiplier = 5; - } + continue; } } else @@ -473,7 +465,7 @@ namespace Barotrauma Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); - if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished) + if (Character.CurrentHull == null || (isClose && !isClosedDoor) || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished) { // Very close to the target, outside, or at the end of the path -> try to steer through the gap Character.ReleaseSecondaryItem(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 7ce6c0f4c..caa1b2919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -369,7 +369,13 @@ namespace Barotrauma } else if (targetCharacter.AIController is EnemyAIController enemy) { - if (targetCharacter.IsHusk && AIParams.HasTag("husk")) + if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet"))) + { + // Pets see other pets as pets by default. + // Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below. + targetingTag = "pet"; + } + else if (targetCharacter.IsHusk && AIParams.HasTag("husk")) { targetingTag = "husk"; } @@ -695,6 +701,9 @@ namespace Barotrauma // Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group. if (Character.IsSameSpeciesOrGroup(c)) { return false; } if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; } + //don't try to attack targets in a sub that belongs to a different team + //(for example, targets in an outpost if we're in the main sub) + if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; } if (c.IsPlayer || Character.IsOnFriendlyTeam(c)) { return a.Damage >= selectedTargetingParams.Threshold; @@ -894,7 +903,7 @@ namespace Barotrauma _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) { // Keep heading to the last known position of the target - var memory = GetTargetMemory(target, false); + var memory = GetTargetMemory(target); if (memory != null) { var location = memory.Location; @@ -981,7 +990,7 @@ namespace Barotrauma } else { - PathSteering.SetPath(path); + PathSteering.SetPath(patrolTarget.SimPosition, path); patrolTimerMargin = 0; newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f); searchingNewHull = false; @@ -1088,13 +1097,13 @@ namespace Barotrauma Character owner = GetOwner(item); if (owner != null) { - if (Character.IsFriendly(owner)) + if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { ResetAITarget(); State = AIState.Idle; return; } - else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) + else { SelectedAiTarget = owner.AiTarget; } @@ -2186,7 +2195,7 @@ namespace Barotrauma } } - AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true); + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown @@ -2531,8 +2540,10 @@ namespace Barotrauma if (Math.Abs(limbDiff.X) < itemBodyExtent && Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor) { + Vector2 velocity = limbDiff; + if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); } item.body.LinearVelocity *= 0.9f; - item.body.LinearVelocity -= limbDiff * 0.25f; + item.body.LinearVelocity -= velocity * 0.25f; bool wasBroken = item.Condition <= 0.0f; item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); @@ -2924,7 +2935,8 @@ namespace Barotrauma } } } - if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0) + //no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health) + if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet) { valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f); } @@ -3021,7 +3033,7 @@ namespace Barotrauma //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and target whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); + AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget); if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) { float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y; @@ -3090,12 +3102,20 @@ namespace Barotrauma if (aiTarget.Entity is Item i) { Character owner = GetOwner(i); - // Don't target items that we own. - // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) if (owner == Character) { continue; } - if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) + if (owner != null) { - continue; + if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; } + if (Character.IsFriendly(owner)) + { + // Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game). + continue; + } + if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) + { + // ignore if owner is tagged to be explicitly ignored (Feign Death) + continue; + } } } if (targetCharacter != null) @@ -3418,7 +3438,7 @@ namespace Barotrauma return false; } - private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound) + private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false) { if (!targetMemories.TryGetValue(target, out AITargetMemory memory)) { @@ -3428,9 +3448,8 @@ namespace Barotrauma targetMemories.Add(target, memory); } } - if (addIfNotFound) + if (keepAlive) { - // Keep the memory alive. memory.Priority = Math.Max(memory.Priority, minPriority); } return memory; @@ -3446,7 +3465,7 @@ namespace Barotrauma } else if (CanPerceive(_selectedAiTarget, checkVisibility: false)) { - var memory = GetTargetMemory(_selectedAiTarget, false); + var memory = GetTargetMemory(_selectedAiTarget); if (memory != null) { memory.Location = _selectedAiTarget.WorldPosition; @@ -3504,10 +3523,10 @@ namespace Barotrauma private readonly float stateResetCooldown = 10; private float stateResetTimer; private bool isStateChanged; - private readonly Dictionary activeTriggers = new Dictionary(); - private readonly HashSet inactiveTriggers = new HashSet(); + private readonly Dictionary activeTriggers = new Dictionary(); + private readonly HashSet inactiveTriggers = new HashSet(); - public void LaunchTrigger(AITrigger trigger) + public void LaunchTrigger(StatusEffect.AITrigger trigger) { if (trigger.IsTriggered) { return; } if (activeTriggers.ContainsKey(trigger)) { return; } @@ -3527,7 +3546,7 @@ namespace Barotrauma { foreach (var triggerObject in activeTriggers) { - AITrigger trigger = triggerObject.Key; + StatusEffect.AITrigger trigger = triggerObject.Key; if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) @@ -3537,7 +3556,7 @@ namespace Barotrauma inactiveTriggers.Add(trigger); } } - foreach (AITrigger trigger in inactiveTriggers) + foreach (StatusEffect.AITrigger trigger in inactiveTriggers) { activeTriggers.Remove(trigger); } @@ -3643,7 +3662,11 @@ namespace Barotrauma { isStateChanged = true; SetStateResetTimer(); - ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman); + if (!Character.IsPet || !target.IsHuman) + { + //don't turn pets hostile to all humans when attacked by one + ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman); + } if (target.IsHuman) { priority = GetTargetParams("human")?.Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 320cac793..890772d58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -42,6 +42,8 @@ namespace Barotrauma public readonly HashSet UnsafeHulls = new HashSet(); public readonly List IgnoredItems = new List(); + private readonly HashSet dirtyHullSafetyCalculations = new HashSet(); + private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; private bool wasConscious; @@ -436,6 +438,7 @@ namespace Barotrauma foreach (Hull h in VisibleHulls) { PropagateHullSafety(Character, h); + dirtyHullSafetyCalculations.Remove(h); } } else @@ -443,9 +446,15 @@ namespace Barotrauma foreach (Hull h in VisibleHulls) { RefreshHullSafety(h); + dirtyHullSafetyCalculations.Remove(h); } } + foreach (Hull h in dirtyHullSafetyCalculations) + { + RefreshHullSafety(h); + } } + dirtyHullSafetyCalculations.Clear(); if (reportProblemsTimer <= 0.0f) { if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) @@ -615,7 +624,7 @@ namespace Barotrauma ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || Character.CurrentHull.IsWetRoom; - bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; + bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true }; bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) { @@ -900,7 +909,7 @@ namespace Barotrauma var container = i.GetComponent(); if (container == null) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } - var rootContainer = container.Item.GetRootContainer() ?? container.Item; + var rootContainer = container.Item.RootContainer ?? container.Item; if (rootContainer.GetComponent() != null || rootContainer.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { @@ -1145,7 +1154,7 @@ namespace Barotrauma string msgId = "DialogLowOxygen"; Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - if (Character.Bleeding > 2.0f && !Character.IsMedic) + if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic) { string msgId = "DialogBleeding"; Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); @@ -1658,7 +1667,7 @@ namespace Barotrauma /// public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true, - predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes)); + predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes)); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. @@ -1891,7 +1900,7 @@ namespace Barotrauma private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f); /// - /// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads. + /// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats. /// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders. /// There's now a cached value per each hull, which should prevent too frequent calculations. /// @@ -1900,9 +1909,13 @@ namespace Barotrauma DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull)); } + public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull); + private void RefreshHullSafety(Hull hull) { - if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD) + var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls; + float hullSafety = GetHullSafety(hull, Character, visibleHulls); + if (hullSafety > HULL_SAFETY_THRESHOLD) { UnsafeHulls.Remove(hull); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 0540b4243..5f26687da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -22,7 +22,10 @@ namespace Barotrauma private readonly Character character; - private Vector2 currentTarget; + /// + /// In sim units. + /// + private Vector2 currentTargetPos; private float findPathTimer; @@ -40,11 +43,6 @@ namespace Barotrauma get { return pathFinder; } } - public Vector2 CurrentTarget - { - get { return currentTarget; } - } - public bool IsPathDirty { get; @@ -54,9 +52,9 @@ namespace Barotrauma /// /// Returns true if any node in the path is in stairs /// - public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); + public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); - public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character); + public bool IsCurrentNodeLadder => GetCurrentLadder() != null; public bool IsNextNodeLadder => GetNextLadder() != null; @@ -64,14 +62,9 @@ namespace Barotrauma { get { - if (currentPath == null) { return false; } - if (currentPath.CurrentNode == null) { return false; } - if (currentPath.NextNode == null) { return false; } - var currentLadder = currentPath.CurrentNode.Ladders; + var currentLadder = GetCurrentLadder(); if (currentLadder == null) { return false; } - if (!currentLadder.Item.IsInteractable(character)) { return false; } - var nextLadder = GetNextLadder(); - return nextLadder != null && nextLadder == currentLadder; + return currentLadder == GetNextLadder(); } } @@ -107,13 +100,10 @@ namespace Barotrauma findPathTimer -= step; } - public void SetPath(SteeringPath path) + public void SetPath(Vector2 targetPos, SteeringPath path) { + currentTargetPos = targetPos; currentPath = path; - if (path.Nodes.Any()) - { - currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; - } findPathTimer = Math.Min(findPathTimer, 1.0f); IsPathDirty = false; } @@ -136,46 +126,17 @@ namespace Barotrauma steering += addition; } - /// - /// Seeks the ladder from the next and next + 1 nodes. - /// - public Ladder GetNextLadder() - { - if (currentPath == null) { return null; } - if (currentPath.NextNode == null) { return null; } - if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)) - { - return currentPath.NextNode.Ladders; - } - else - { - int index = currentPath.CurrentIndex + 2; - if (currentPath.Nodes.Count > index) - { - var node = currentPath.Nodes[index]; - if (node == null) { return null; } - if (node.Ladders != null && node.Ladders.Item.IsInteractable(character)) - { - return node.Ladders; - } - //if the next node is a hatch, check if the node after that is a ladder - else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal) - { - index++; - if (currentPath.Nodes.Count > index) - { - node = currentPath.Nodes[index]; - if (node == null) { return null; } - if (node.Ladders != null && node.Ladders.Item.IsInteractable(character)) - { - return node.Ladders; - } - } - } + public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode); - } - return null; + public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode); + + private Ladder GetLadder(WayPoint wp) + { + if (wp?.Ladders?.Item is Item item && item.IsInteractable(character)) + { + return wp.Ladders; } + return null; } private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) @@ -183,19 +144,10 @@ namespace Barotrauma bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null; if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) { - Vector2 targetDiff = target - currentTarget; - if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null) - { - //target in a different sub than where the character is now - //take that into account when calculating if the target has moved - Submarine currentPathSub = currentPath?.CurrentNode?.Submarine; - if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; } - if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null) - { - Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; - targetDiff += subDiff; - } - } + // If the target has moved, we need a new path. + // Different subs are already taken into account before setting the target. + // Triggers when either the target or we have changed subs, but only once (until the new path has been accepted). + Vector2 targetDiff = target - currentTargetPos; if (targetDiff.LengthSquared() > 1) { needsNewPath = true; @@ -205,14 +157,14 @@ namespace Barotrauma if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; - if (!needsNewPath && findPathTimer < -1) + if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp) { - if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0) + if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0) { // Not moving -> need a new path. needsNewPath = true; } - if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null) + if (character.Submarine == null && wp.CurrentHull != null) { // Current node inside, while we are outside // -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile) @@ -226,7 +178,7 @@ namespace Barotrauma if (findPathTimer < 0) { SkipCurrentPathNodes(); - currentTarget = target; + currentTargetPos = target; Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; @@ -252,6 +204,14 @@ namespace Barotrauma useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); } } + if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode)) + { + // If we are set to disregard the new path, ensure that we can actually see the current node of the old path, + // because it's possible that there's e.g. a closed door between us and the current node, + // and in that case we'd want to use the new path instead of the old. + // There's visibility checks in the pathfinder calls, so the new path should always be ok. + useNewPath = true; + } bool IsIdenticalPath() { @@ -330,6 +290,7 @@ namespace Barotrauma //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) { + // TODO: might cause some edge cases -> do we need this? diff.Y = 0.0f; } if (diff == Vector2.Zero) { return Vector2.Zero; } @@ -346,12 +307,12 @@ namespace Barotrauma } if (currentPath.Finished) { - Vector2 pos2 = host.SimPosition; + Vector2 hostPosition = host.SimPosition; if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null) { - pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; + hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition; } - return currentTarget - pos2; + return currentTargetPos - hostPosition; } bool doorsChecked = false; checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime()); @@ -371,14 +332,46 @@ namespace Barotrauma bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; - Ladder currentLadder = currentPath.CurrentNode.Ladders; - if (currentLadder != null && !currentLadder.Item.IsInteractable(character)) - { - currentLadder = null; - } + Ladder currentLadder = GetCurrentLadder(); Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; - bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1); + bool useLadders = canClimb && ladders != null; + var collider = character.AnimController.Collider; + Vector2 colliderSize = collider.GetSize(); + if (useLadders) + { + if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y)) + { + // If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders. + useLadders = false; + } + else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null) + { + Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos; + if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y)) + { + // If the next node is horizontally farther from us than vertically, we don't want to start climbing. + useLadders = false; + } + } + else if (isDiving && steering.Y < 1) + { + // When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them. + useLadders = false; + } + } + if (character.IsClimbing && !useLadders) + { + if (currentPath.IsAtEndNode && canClimb && ladders != null) + { + // Don't release the ladders when ending a path in ladders. + useLadders = true; + } + else + { + character.StopClimbing(); + } + } if (useLadders && character.SelectedSecondaryItem != ladders.Item) { if (character.CanInteractWith(ladders.Item)) @@ -398,40 +391,28 @@ namespace Barotrauma } } } - var collider = character.AnimController.Collider; - if (character.IsClimbing && !useLadders) - { - character.StopClimbing(); - } if (character.IsClimbing && useLadders) { - if (currentLadder == null && nextLadder != null) + if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item) { // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. NextNode(!doorsChecked); } else { - bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; - if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) + bool nextLadderSameAsCurrent = currentLadder == nextLadder; + if (currentLadder != null && nextLadder != null) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } //at the same height as the waypoint float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; - if (heightDiff < colliderSize) + float colliderHeight = collider.Height / 2 + collider.Radius; + float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); + if (heightDiff < colliderHeight * 1.25f) { - float heightFromFloor = character.AnimController.GetHeightFromFloor(); - // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. - bool isAboveFloor = heightFromFloor > -0.1f; - // If the next waypoint is horizontally far, we don't want to keep holding the ladders - if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) - { - character.StopClimbing(); - } - else if (nextLadder != null && !nextLadderSameAsCurrent) + if (nextLadder != null && !nextLadderSameAsCurrent) { // Try to change the ladder (hatches between two submarines) if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) @@ -442,12 +423,36 @@ namespace Barotrauma } } } - if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10) + bool isAboveFloor; + if (diff.Y < 0) { - NextNode(!doorsChecked); + // When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders. + float colliderBottom = character.AnimController.Collider.SimPosition.Y; + float floorY = character.AnimController.FloorY; + isAboveFloor = colliderBottom > floorY; + } + else + { + // When climbing up, let's use the lowest collider (feet). + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative, + // when a foot is still below the platform. + float heightFromFloor = character.AnimController.GetHeightFromFloor(); + isAboveFloor = heightFromFloor > -0.1f; + } + if (isAboveFloor) + { + if (Math.Abs(diff.Y) < distanceMargin) + { + NextNode(!doorsChecked); + } + else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin))) + { + // Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far. + character.StopClimbing(); + } } } - else if (nextLadder != null) + else if (currentLadder != null && currentPath.NextNode != null) { if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) { @@ -466,7 +471,6 @@ namespace Barotrauma if (door == null || door.CanBeTraversed) { float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - Vector2 colliderSize = collider.GetSize(); float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f); float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); @@ -485,7 +489,6 @@ namespace Barotrauma { // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); - Vector2 colliderSize = collider.GetSize(); Vector2 velocity = collider.LinearVelocity; // If the character is very short, it would fail to use the waypoint nodes because they are always too high. // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small. @@ -512,9 +515,12 @@ namespace Barotrauma } } float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed)) + if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow) { - NextNode(!doorsChecked); + if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null)) + { + NextNode(!doorsChecked); + } } } if (currentPath.CurrentNode == null) @@ -533,9 +539,9 @@ namespace Barotrauma currentPath.SkipToNextNode(); } - private bool CanAccessDoor(Door door, Func buttonFilter = null) + public bool CanAccessDoor(Door door, Func buttonFilter = null) { - if (door.IsBroken) { return true; } + if (door.CanBeTraversed) { return true; } if (door.IsClosed) { if (!door.Item.IsInteractable(character)) { return false; } @@ -631,10 +637,12 @@ namespace Barotrauma { //the node we're heading towards is the last one in the path, and at a door //the door needs to be open for the character to reach the node - if (currentWaypoint.ConnectedDoor.LinkedGap != null) + if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap) { - // Keep the airlock doors closed, but not in ruins/wrecks - if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck) + if (currentWaypoint.Submarine == null || + currentWaypoint.Submarine.Info is { IsPlayer: false } || + !linkedGap.IsRoomToRoom || + (linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false })) { shouldBeOpen = true; door = currentWaypoint.ConnectedDoor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 2dcdc2e10..ca5aa891e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -213,7 +213,7 @@ namespace Barotrauma { foreach (Voronoi2.GraphEdge edge in cell.Edges) { - if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) + if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection); float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index dc9946531..dd3b79d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -506,6 +506,8 @@ namespace Barotrauma } } + public virtual void SpeakAfterOrderReceived() { } + protected static bool CanEquip(Character character, Item item, bool allowWearing) { if (item == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index c7bbf9574..7d82e8377 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -14,6 +14,10 @@ namespace Barotrauma public readonly List prioritizedItems = new List(); + public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier(); + + protected override int MaxTargets => 100; + public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -81,8 +85,8 @@ namespace Barotrauma public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => allowUnloading && + container.HasTag(AllowCleanupTag) && container.HasAccess(character) && - container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.GetComponent() != null && IsItemInsideValidSubmarine(container, character) && @@ -91,7 +95,6 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } - if (!item.HasAccess(character)) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { @@ -102,6 +105,7 @@ namespace Barotrauma } if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } } + if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } var wire = item.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index c0ae5381b..a1b25992a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -995,10 +995,18 @@ namespace Barotrauma } } } - if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) + + //prefer using handcuffs already on the enemy's inventory + if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable matchingItems)) + { + HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems); + } + + if (matchingItems.Any() && + !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands) { var handCuffs = matchingItems.First(); - if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) + if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true)) { #if DEBUG DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index c39168a42..244b66a89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -198,7 +198,7 @@ namespace Barotrauma TargetName = container.Item.Name, AbortCondition = obj => container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) || - (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || + (container.Item.RootContainer?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder(), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index aa10e388d..3cb129c96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -30,7 +30,8 @@ namespace Barotrauma public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); - protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); + protected override bool CheckObjectiveSpecific() => + targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head); public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -51,7 +52,7 @@ namespace Barotrauma TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); } if (targetItem == null || - !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && + !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) { TryAddSubObjective(ref getDivingGear, () => @@ -65,7 +66,7 @@ namespace Barotrauma AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowToFindDivingGear = false, AllowDangerousPressure = true, - EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes, + EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head, Wear = true }; }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 97e56d4cd..a35ec388d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -406,7 +406,7 @@ namespace Barotrauma if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock")) { // Don't allow to go outside if not already outside. - var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); if (path.Unreachable) { hullSafety = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index a8968f731..aaee84fa1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -376,7 +376,7 @@ namespace Barotrauma // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. // This is relatively expensive, so let's do this only when it significantly improves the behavior. // Only allow one path find call per frame. - CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective); + CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder); } bool checkPath = CheckPathForEachItem; // Reset if the character has switched subs. @@ -536,7 +536,7 @@ namespace Barotrauma { if (itemCandidates.FirstOrDefault() is { } itemCandidate) { - var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); if (path.Unreachable) { // Remove the invalid candidates and continue on the next frame. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 521053319..dfc1966bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -26,7 +26,8 @@ namespace Barotrauma public Func PriorityGetter; - public bool IsFollowOrderObjective; + public bool IsFollowOrder; + public bool IsWaitOrder; public bool Mimic; public bool SpeakIfFails { get; set; } = true; @@ -59,7 +60,7 @@ namespace Barotrauma { get { - if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null)) + if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null)) { // Keep close when the target is going inside/outside return minDistance; @@ -220,15 +221,45 @@ namespace Barotrauma } } Hull targetHull = GetTargetHull(); - if (!IsFollowOrderObjective) + if (!IsFollowOrder) { - // Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls. - bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls - && PathSteering != null && PathSteering.CurrentPath != null - && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); - if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull)) + bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull); + if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls) { - Abandon = true; + if (HumanAIController.UnsafeHulls.Contains(targetHull)) + { + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(targetHull); + } + else if (PathSteering?.CurrentPath != null) + { + foreach (WayPoint wp in PathSteering.CurrentPath.Nodes) + { + if (wp.CurrentHull == null) { continue; } + if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull)) + { + isUnreachable = true; + HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull); + } + } + } + } + if (isUnreachable) + { + SteeringManager.Reset(); + if (PathSteering?.CurrentPath != null) + { + PathSteering.CurrentPath.Unreachable = true; + } + if (repeat) + { + SpeakCannotReach(); + } + else + { + Abandon = true; + } + return; } } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; @@ -250,6 +281,7 @@ namespace Barotrauma if (repeat) { SpeakCannotReach(); + return; } else { @@ -262,310 +294,285 @@ namespace Barotrauma waitUntilPathUnreachable = pathWaitingTime; } } - if (!Abandon) + if (Abandon) { return; } + if (getDivingGearIfNeeded) { - if (getDivingGearIfNeeded) + Character followTarget = Target as Character; + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; + bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool tryToGetDivingSuit = needsDivingSuit; + if (Mimic && !character.IsImmuneToPressure) { - Character followTarget = Target as Character; - bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; - bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - bool tryToGetDivingSuit = needsDivingSuit; - if (Mimic && !character.IsImmuneToPressure) + if (HumanAIController.HasDivingSuit(followTarget)) { - if (HumanAIController.HasDivingSuit(followTarget)) + tryToGetDivingGear = true; + tryToGetDivingSuit = true; + } + else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) + { + tryToGetDivingGear = true; + } + } + bool needsEquipment = false; + float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); + if (tryToGetDivingSuit) + { + needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); + } + else if (tryToGetDivingGear) + { + needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); + } + if (character.LockHands) + { + cantFindDivingGear = true; + } + if (cantFindDivingGear && needsDivingSuit) + { + // Don't try to reach the target without a suit because it's lethal. + Abandon = true; + return; + } + if (needsEquipment && !cantFindDivingGear) + { + SteeringManager.Reset(); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), + onAbandon: () => { - tryToGetDivingGear = true; - tryToGetDivingSuit = true; - } - else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) - { - tryToGetDivingGear = true; - } - } - bool needsEquipment = false; - float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); - if (tryToGetDivingSuit) - { - needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); - } - else if (tryToGetDivingGear) - { - needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); - } - if (character.LockHands) - { cantFindDivingGear = true; - } - if (cantFindDivingGear && needsDivingSuit) - { - // Don't try to reach the target without a suit because it's lethal. - Abandon = true; - return; - } - if (needsEquipment && !cantFindDivingGear) - { - SteeringManager.Reset(); - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), - onAbandon: () => - { - cantFindDivingGear = true; - if (needsDivingSuit) - { - // Shouldn't try to reach the target without a suit, because it's lethal. - Abandon = true; - } - else - { - // Try again without requiring the diving suit - RemoveSubObjective(ref findDivingGear); - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => - { - Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); - RemoveSubObjective(ref findDivingGear); - }, - onCompleted: () => - { - RemoveSubObjective(ref findDivingGear); - }); - } - }, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - return; - } - } - if (repeat) - { - if (IsCloseEnough) - { - if (requiredCondition == null || requiredCondition()) + if (needsDivingSuit) { - if (character.CanSeeTarget(Target)) - { - OnCompleted(); - return; - } - } - } - } - float maxGapDistance = 500; - Character targetCharacter = Target as Character; - if (character.AnimController.InWater) - { - if (character.CurrentHull == null || - IsFollowOrderObjective && - targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) && - Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance) - { - if (seekGapsTimer > 0) - { - seekGapsTimer -= deltaTime; + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; } else { - bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null; - bool isEitherOneInside = isInside || Target.Submarine != null; - if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath())) - { - SeekGaps(maxGapDistance); - seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); - if (TargetGap != null) + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => { - // Check that nothing is blocking the way - Vector2 rayStart = character.SimPosition; - Vector2 rayEnd = TargetGap.SimPosition; - if (TargetGap.Submarine != null && character.Submarine == null) - { - rayStart -= TargetGap.Submarine.SimPosition; - } - else if (TargetGap.Submarine == null && character.Submarine != null) - { - rayEnd -= character.Submarine.SimPosition; - } - var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); - if (closestBody != null) - { - TargetGap = null; - } - } + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); } - else - { - TargetGap = null; - } - } + }, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); + return; + } + } + if (repeat && IsCloseEnough) + { + if (requiredCondition == null || requiredCondition()) + { + if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder)) + { + OnCompleted(); + return; + } + } + } + float maxGapDistance = 500; + Character targetCharacter = Target as Character; + if (character.AnimController.InWater) + { + if (character.CurrentHull == null || + IsFollowOrder && + targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) && + Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance) + { + if (seekGapsTimer > 0) + { + seekGapsTimer -= deltaTime; } else { - TargetGap = null; - } - if (TargetGap != null) - { - if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime)) + bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null; + bool isEitherOneInside = isInside || Target.Submarine != null; + if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath())) { - SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); - return; + SeekGaps(maxGapDistance); + seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); + if (TargetGap != null) + { + // Check that nothing is blocking the way + Vector2 rayStart = character.SimPosition; + Vector2 rayEnd = TargetGap.SimPosition; + if (TargetGap.Submarine != null && character.Submarine == null) + { + rayStart -= TargetGap.Submarine.SimPosition; + } + else if (TargetGap.Submarine == null && character.Submarine != null) + { + rayEnd -= character.Submarine.SimPosition; + } + var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); + if (closestBody != null) + { + TargetGap = null; + } + } } else { TargetGap = null; } } - if (checkScooterTimer <= 0) - { - useScooter = false; - checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); - Identifier scooterTag = "scooter".ToIdentifier(); - Identifier batteryTag = "mobilebattery".ToIdentifier(); - Item scooter = null; - bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); - if (!shouldUseScooter) - { - float threshold = 500; - if (isInside) - { - Vector2 diff = Target.WorldPosition - character.WorldPosition; - shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150; - } - else - { - shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; - } - } - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) - { - // Currently equipped scooter - scooter = equippedScooters.FirstOrDefault(); - } - else if (shouldUseScooter) - { - var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); - var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); - bool handsFull = - (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) || - (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem)); - if (!handsFull) - { - bool hasBattery = false; - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) - { - // Non-equipped scooter with a battery - scooter = nonEquippedScooters.FirstOrDefault(); - hasBattery = true; - } - else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) - { - // Non-equipped scooter without a battery - scooter = _nonEquippedScooters.FirstOrDefault(); - // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. - hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); - } - if (scooter != null && hasBattery) - { - // Equip only if we have a battery available - HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); - } - } - } - if (scooter != null && character.HasEquippedItem(scooter)) - { - if (shouldUseScooter) - { - useScooter = true; - // Check the battery - if (scooter.ContainedItems.None(i => i.Condition > 0)) - { - // Try to switch batteries - if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) - { - scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot)); - if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) - { - useScooter = false; - } - } - else - { - useScooter = false; - } - } - } - if (!useScooter) - { - // Unequip - character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot); - } - } - } - else - { - checkScooterTimer -= deltaTime; - } } else { TargetGap = null; - useScooter = false; - checkScooterTimer = 0; } - if (SteeringManager == PathSteering) + if (TargetGap != null) { - Vector2 targetPos = character.GetRelativeSimPosition(Target); - Func nodeFilter = null; - if (isInside && !AllowGoingOutside) + if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrder ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime)) { - nodeFilter = n => n.Waypoint.CurrentHull != null; - } - else if (!isInside) - { - if (HumanAIController.UseOutsideWaypoints) - { - nodeFilter = n => n.Waypoint.Submarine == null; - } - else - { - nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null; - } - } - if (!isInside && !UsePathingOutside) - { - character.ReleaseSecondaryItem(); - PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10); - if (character.AnimController.InWater) - { - SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); - } + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); + return; } else { - PathSteering.SteeringSeek(targetPos, weight: 1, - startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), - endNodeFilter: endNodeFilter, - nodeFilter: nodeFilter, - checkVisiblity: Target is Item || Target is Character); + TargetGap = null; } - if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) + } + if (checkScooterTimer <= 0) + { + useScooter = false; + checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); + Identifier scooterTag = "scooter".ToIdentifier(); + Identifier batteryTag = "mobilebattery".ToIdentifier(); + Item scooter = null; + bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); + if (!shouldUseScooter) { - if (useScooter) + float threshold = 500; + if (isInside) { - UseScooter(Target.WorldPosition); + Vector2 diff = Target.WorldPosition - character.WorldPosition; + shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150; } else { - character.ReleaseSecondaryItem(); - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); - if (character.AnimController.InWater) + shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; + } + } + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) + { + // Currently equipped scooter + scooter = equippedScooters.FirstOrDefault(); + } + else if (shouldUseScooter) + { + var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); + var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); + bool handsFull = + (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) || + (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem)); + if (!handsFull) + { + bool hasBattery = false; + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) { - SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2); + // Non-equipped scooter with a battery + scooter = nonEquippedScooters.FirstOrDefault(); + hasBattery = true; + } + else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + { + // Non-equipped scooter without a battery + scooter = _nonEquippedScooters.FirstOrDefault(); + // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. + hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); + } + if (scooter != null && hasBattery) + { + // Equip only if we have a battery available + HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); } } } - else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null) + if (scooter != null && character.HasEquippedItem(scooter)) { - UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition); + if (shouldUseScooter) + { + useScooter = true; + // Check the battery + if (scooter.ContainedItems.None(i => i.Condition > 0)) + { + // Try to switch batteries + if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) + { + scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot)); + if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) + { + useScooter = false; + } + } + else + { + useScooter = false; + } + } + } + if (!useScooter) + { + // Unequip + character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot); + } } } else + { + checkScooterTimer -= deltaTime; + } + } + else + { + TargetGap = null; + useScooter = false; + checkScooterTimer = 0; + } + if (SteeringManager == PathSteering) + { + Vector2 targetPos = character.GetRelativeSimPosition(Target); + Func nodeFilter = null; + if (isInside && !AllowGoingOutside) + { + nodeFilter = n => n.Waypoint.CurrentHull != null; + } + else if (!isInside) + { + if (HumanAIController.UseOutsideWaypoints) + { + nodeFilter = n => n.Waypoint.Submarine == null; + } + else + { + nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null; + } + } + if (!isInside && !UsePathingOutside) + { + character.ReleaseSecondaryItem(); + PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10); + if (character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); + } + } + else + { + PathSteering.SteeringSeek(targetPos, weight: 1, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), + endNodeFilter: endNodeFilter, + nodeFilter: nodeFilter, + checkVisiblity: Target is Item || Target is Character); + } + if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { if (useScooter) { @@ -574,13 +581,33 @@ namespace Barotrauma else { character.ReleaseSecondaryItem(); - SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); if (character.AnimController.InWater) { - SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2); } } } + else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null) + { + UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition); + } + } + else + { + if (useScooter) + { + UseScooter(Target.WorldPosition); + } + else + { + character.ReleaseSecondaryItem(); + SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); + if (character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); + } + } } void UseScooter(Vector2 targetWorldPos) @@ -595,7 +622,7 @@ namespace Barotrauma } Vector2 diff = character.CursorPosition - character.Position; Vector2 dir = Vector2.Normalize(diff); - if (character.CurrentHull == null && IsFollowOrderObjective) + if (character.CurrentHull == null && IsFollowOrder) { float sqrDist = diff.LengthSquared(); if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) @@ -674,7 +701,7 @@ namespace Barotrauma { if (gap.Open < 1) { continue; } if (gap.Submarine == null) { continue; } - if (!IsFollowOrderObjective) + if (!IsFollowOrder) { if (gap.FlowTargetHull == null) { continue; } if (gap.Submarine != Target.Submarine) { continue; } @@ -792,8 +819,10 @@ namespace Barotrauma // Release ladders when ordered to wait at a spawnpoint. // This is a special case specifically meant for NPCs that spawn in outposts with a wait order. // Otherwise they might keep holding to the ladders when the target is just next to it. - // Releasing too early should be handled inside the IsCloseEnough property. - character.ReleaseSecondaryItem(); + if (character.IsClimbing && character.AnimController.IsAboveFloor) + { + character.StopClimbing(); + } } base.OnCompleted(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 0cb63b791..bb0c590c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -168,7 +168,7 @@ namespace Barotrauma CleanupItems(deltaTime); } - if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null) + if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull)) { TargetHull = character.CurrentHull; } @@ -178,7 +178,7 @@ namespace Barotrauma IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull)) + if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull)) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -258,7 +258,8 @@ namespace Barotrauma currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID; bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node => + Vector2 targetPos = character.GetRelativeSimPosition(currentTarget); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } // Check that there is no unsafe hulls on the way to the target @@ -278,7 +279,7 @@ namespace Barotrauma return; } character.AIController.SelectTarget(currentTarget.AiTarget); - PathSteering.SetPath(path); + PathSteering.SetPath(targetPos, path); SetTargetTimerNormal(); searchingNewHull = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index c3ec57136..118849867 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -98,7 +98,7 @@ namespace Barotrauma { foreach (var item in itemContainer.ContainableItems) { - if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished) + if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished) { return CheckStatus.Finished; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 49ab6d61f..8edac36a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -48,6 +48,8 @@ namespace Barotrauma protected virtual bool ResetWhenClearingIgnoreList => true; protected virtual bool ForceOrderPriority => true; + protected virtual int MaxTargets => int.MaxValue; + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } public override void Update(float deltaTime) @@ -188,6 +190,10 @@ namespace Barotrauma if (!ignoreList.Contains(target)) { Targets.Add(target); + if (Targets.Count > MaxTargets) + { + break; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 2c83b3ef7..af05c7095 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -427,7 +427,7 @@ namespace Barotrauma ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, - IsFollowOrderObjective = true, + IsFollowOrder = true, Mimic = character.IsOnPlayerTeam, DialogueIdentifier = "dialogcannotreachplace".ToIdentifier() }; @@ -435,7 +435,11 @@ namespace Barotrauma case "wait": newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { - AllowGoingOutside = true + AllowGoingOutside = true, + IsWaitOrder = true, + DebugLogWhenFails = false, + SpeakIfFails = false, + CloseEnough = 100 }; break; case "return": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 6153ee028..97ce2855f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -13,6 +13,8 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => true; public override bool AllowInAnySub => true; + private readonly HashSet charactersWithMinorInjuries = new HashSet(); + private const float vitalityThreshold = 75; private const float vitalityThresholdForOrders = 90; public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target) @@ -23,17 +25,34 @@ namespace Barotrauma } else { - // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. - // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. - // On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled. - return character == target || manager.HasOrder() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold; + return character == target || manager.HasOrder() ? vitalityThresholdForOrders : vitalityThreshold; } } - - public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + + public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Filter(Character target) => IsValidTarget(target, character); + protected override bool Filter(Character target) + { + if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; } + if (GetTreatableAfflictions(target).Any()) + { + return true; + } + else + { + //the target might be at a low enough health to be considered a valid target, + //but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them + // -> make the bot speak to make it clear the bot intentionally ignores very minor injuries + if (!charactersWithMinorInjuries.Contains(character)) + { + character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, + null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f); + charactersWithMinorInjuries.Add(character); + } + return false; + } + } protected override IEnumerable GetList() => Character.CharacterList; @@ -68,7 +87,7 @@ namespace Barotrauma float vitality = 100; vitality -= character.Bleeding * 2; vitality += Math.Min(character.Oxygen, 0); - foreach (Affliction affliction in GetTreatableAfflictions(character)) + foreach (Affliction affliction in GetTreatableAfflictions(character, ignoreTreatmentThreshold: true)) { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; @@ -84,12 +103,19 @@ namespace Barotrauma return Math.Clamp(vitality, 0, 100); } - public static IEnumerable GetTreatableAfflictions(Character character) + public static IEnumerable GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false) { var allAfflictions = character.CharacterHealth.GetAllAfflictions(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (affliction.Prefab.IsBuff) { continue; } + if (!ignoreTreatmentThreshold) + { + //other afflictions of the same type increase the "treatability" + // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs + float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction); + if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + } if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; } if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; @@ -102,7 +128,7 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Character target) => HumanAIController.RemoveTargets(character, target); - public static bool IsValidTarget(Character target, Character character) + public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true) { if (target == null || target.IsDead || target.Removed) { return false; } if (target.IsInstigator) { return false; } @@ -112,7 +138,7 @@ namespace Barotrauma { if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { - return false; + return false; } if (!humanAI.ObjectiveManager.HasOrder()) { @@ -127,6 +153,10 @@ namespace Barotrauma return false; } } + if (requireTreatableAfflictions && GetTreatableAfflictions(target).None()) + { + return false; + } } else { @@ -159,5 +189,11 @@ namespace Barotrauma } return character.GetDamageDoneByAttacker(target) <= 0; } + + public override void Reset() + { + base.Reset(); + charactersWithMinorInjuries.Clear(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 48ad56141..ed01119ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -51,7 +51,7 @@ namespace Barotrauma return; } - if (!IsRemotePlayer && !(AIController is HumanAIController)) + if (!IsRemotePlayer && AIController is not HumanAIController) { float characterDistSqr = GetDistanceSqrToClosestPlayer(); if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f)) @@ -63,6 +63,10 @@ namespace Barotrauma AnimController.SimplePhysicsEnabled = false; } } + else + { + AnimController.SimplePhysicsEnabled = false; + } if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } if (Controlled == this) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index dfa3fdba4..6682b2d50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1,10 +1,10 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.Networking; namespace Barotrauma { @@ -1544,6 +1544,10 @@ namespace Barotrauma target.AnimController.ResetPullJoints(); } + bool targetPoseControlled = + target.SelectedItem?.GetComponent() is { ControlCharacterPose: true } || + target.SelectedSecondaryItem?.GetComponent() is { ControlCharacterPose: true }; + if (IsClimbing) { //cannot drag up ladders if the character is conscious @@ -1719,13 +1723,12 @@ namespace Barotrauma targetForce = 5000.0f; } - targetLimb.PullJointEnabled = true; - targetLimb.PullJointMaxForce = targetForce; - targetLimb.PullJointWorldAnchorB = targetAnchor; - targetLimb.Disabled = true; - - if (diff.LengthSquared() > 0.1f) + if (!targetPoseControlled) { + targetLimb.PullJointEnabled = true; + targetLimb.PullJointMaxForce = targetForce; + targetLimb.PullJointWorldAnchorB = targetAnchor; + targetLimb.Disabled = true; target.AnimController.movement = -diff; } } @@ -1751,7 +1754,7 @@ namespace Barotrauma target.AnimController.IgnorePlatforms = IgnorePlatforms; target.AnimController.TargetMovement = TargetMovement; } - else if (target is AICharacter && target != Character.Controlled) + else if (target is AICharacter && target != Character.Controlled && !targetPoseControlled) { if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 5592b5dd1..cc28d59c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1197,7 +1197,7 @@ namespace Barotrauma { inWater = false; headInWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); } //ragdoll isn't in any room -> it's in the water else if (currentHull == null) @@ -1209,7 +1209,7 @@ namespace Barotrauma { headInWater = false; inWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); @@ -1554,15 +1554,24 @@ namespace Barotrauma lastFloorCheckPos = Vector2.Zero; } - private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) + // Force check floor y at least once a second so that we'll drop through gaps that we are standing upon. + private const float FloorYStaleTime = 1; + private float floorYCheckTimer; + private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false) { + floorYCheckTimer -= deltaTime; PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; - if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms) + if (floorYCheckTimer < 0 || + lastFloorCheckIgnoreStairs != ignoreStairs || + lastFloorCheckIgnorePlatforms != IgnorePlatforms || + Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f) { floorY = GetFloorY(refBody.SimPosition, ignoreStairs); lastFloorCheckPos = refBody.SimPosition; lastFloorCheckIgnoreStairs = ignoreStairs; lastFloorCheckIgnorePlatforms = IgnorePlatforms; + // Add some randomness to prevent all stationary characters doing the checks at the same frame. + floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f); } } @@ -1846,6 +1855,7 @@ namespace Barotrauma private bool collisionsDisabled; + private double lastObstacleRayCastTime; protected void CheckDistFromCollider() { @@ -1853,15 +1863,28 @@ namespace Barotrauma allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; + float obstacleCheckDist = 0.3f; + Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; float distSqrd = diff.LengthSquared(); - if (distSqrd > resetDist * resetDist) + bool shouldReset = distSqrd > resetDist * resetDist; + if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist) + { + if (Timing.TotalTime > lastObstacleRayCastTime + 1 && + Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null) + { + shouldReset = true; + lastObstacleRayCastTime = Timing.TotalTime; + } + } + + if (shouldReset) { //ragdoll way too far, reset position SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true); } - if (distSqrd > allowedDist * allowedDist) + else if (distSqrd > allowedDist * allowedDist) { //ragdoll too far from the collider, disable collisions until it's close enough //(in case the ragdoll has gotten stuck somewhere) @@ -1883,7 +1906,7 @@ namespace Barotrauma collisionsDisabled = false; //force collision categories to be updated prevCollisionCategory = Category.None; - } + } } partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d7c1d099e..6042aecee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1065,7 +1065,7 @@ namespace Barotrauma { get { - return SelectedItem == null || (SelectedItem.GetComponent()?.AllowAiming ?? false); + return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled; } } @@ -1128,6 +1128,7 @@ namespace Barotrauma public HashSet MarkedAsLooted = new(); public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; + public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer; public float AITurretPriority { @@ -2703,27 +2704,6 @@ namespace Barotrauma CustomInteractHUDText = hudText; } - private void TransformCursorPos() - { - if (Submarine == null) - { - //character is outside but cursor position inside - if (cursorPosition.Y > Level.Loaded.Size.Y) - { - var sub = Submarine.FindContaining(cursorPosition); - if (sub != null) cursorPosition += sub.Position; - } - } - else - { - //character is inside but cursor position is outside - if (cursorPosition.Y < Level.Loaded.Size.Y) - { - cursorPosition -= Submarine.Position; - } - } - } - public void SelectCharacter(Character character) { if (character == null || character == this) { return; } @@ -4316,7 +4296,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (LastAttacker == null || !LastAttacker.IsPlayer) { @@ -4374,6 +4354,10 @@ namespace Barotrauma { statusEffect.Apply(actionType, deltaTime, this, this); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null) + { + statusEffect.Apply(actionType, deltaTime, this, CurrentHull); + } } if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { @@ -4650,6 +4634,13 @@ namespace Barotrauma CharacterList.Remove(this); + foreach (var attachedProjectile in AttachedProjectiles.ToList()) + { + attachedProjectile.Unstick(); + } + Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); + Latchers.Clear(); + if (Inventory != null) { foreach (Item item in Inventory.AllItems) @@ -4736,6 +4727,8 @@ namespace Barotrauma } #if SERVER newItem.GetComponent()?.SyncHistory(); + if (newItem.GetComponent() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); } + if (newItem.GetComponent() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); } #endif int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 }); if (!slotIndices.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 99b34312b..ffbf96858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -386,6 +386,8 @@ namespace Barotrauma { foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects) { + if (Strength <= periodicEffect.MinStrength) { continue; } + if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; } PeriodicEffectTimers[periodicEffect] -= deltaTime; if (PeriodicEffectTimers[periodicEffect] <= 0.0f) { @@ -495,6 +497,13 @@ namespace Barotrauma /// public void SetStrength(float strength) { + if (!MathUtils.IsValid(strength)) + { +#if DEBUG + DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + return; + } _nonClampedStrength = strength; _strength = _nonClampedStrength; activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index d3bd97c3c..3b65cc320 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -730,13 +730,13 @@ namespace Barotrauma /// /// How high the strength has to be for the affliction icon to be shown with a health scanner /// - public readonly float ShowInHealthScannerThreshold = 0.05f; + public readonly float ShowInHealthScannerThreshold; /// /// How strong the affliction needs to be before bots attempt to treat it. /// Also effects when the affliction is shown in the suitable treatments list. /// - public readonly float TreatmentThreshold = 5.0f; + public readonly float TreatmentThreshold; /// /// Bots will not try to treat the affliction if the character has any of these afflictions @@ -847,7 +847,7 @@ namespace Barotrauma { foreach (var itemPrefab in ItemPrefab.Prefabs) { - float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType)); + float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType); if (!MathUtils.NearlyEqual(suitability, 0.0f)) { yield return new KeyValuePair(itemPrefab.Identifier, suitability); @@ -915,7 +915,7 @@ namespace Barotrauma ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold), Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); - TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f)); + TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f)); DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 250337640..95dad7ad3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -417,9 +418,13 @@ namespace Barotrauma return strength; } - public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true) + public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false) { - if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; } + if (Character.GodMode) { return; } + if (!ignoreUnkillability) + { + if (!affliction.Prefab.IsBuff && Unkillable) { return; } + } if (affliction.Prefab.LimbSpecific) { if (targetLimb == null) @@ -449,11 +454,7 @@ namespace Barotrauma var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - - resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); - if (resistance > 1f) { resistance = 1f; } - - return resistance; + return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); } public float GetStatValue(StatTypes statType) @@ -974,7 +975,7 @@ namespace Barotrauma #endif } - private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth) + private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth) { float multiplier = 1.0f; if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier)) @@ -1116,7 +1117,11 @@ namespace Barotrauma strength = GetPredictedStrength(affliction, predictFutureDuration, limb); } - if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } + //other afflictions of the same type increase the "treatability" + // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs + float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false); + if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) @@ -1133,13 +1138,20 @@ namespace Barotrauma foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { + float suitability = treatment.Value * strength; + if (treatment.Value > strength) + { + //avoid using very effective meds on small injuries + float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f); + suitability /= overtreatmentFactor; + } if (!treatmentSuitability.ContainsKey(treatment.Key)) { - treatmentSuitability[treatment.Key] = treatment.Value * strength; + treatmentSuitability[treatment.Key] = suitability; } else { - treatmentSuitability[treatment.Key] += treatment.Value * strength; + treatmentSuitability[treatment.Key] += suitability; } minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability); maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); @@ -1155,6 +1167,28 @@ namespace Barotrauma } } + /// + /// Returns the total strength of instances of the same affliction on all the characters limbs, + /// with a smaller weight given to the other afflictions on other limbs + /// + /// Multiplier on the strengths of the afflictions on other limbs. + /// Should the strength of the provided affliction be included too? + public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true) + { + float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0; + if (affliction.Prefab.LimbSpecific) + { + foreach (Affliction otherAffliction in afflictions.Keys) + { + if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction) + { + totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier; + } + } + } + return totalAfflictionStrength; + } + private readonly HashSet afflictionTags = new HashSet(); public IEnumerable GetActiveAfflictionTags() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e7d13cf51..d5c10cbef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -182,7 +182,9 @@ namespace Barotrauma { humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200) { - DebugLogWhenFails = false + DebugLogWhenFails = false, + IsWaitOrder = true, + CloseEnough = 100 }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index a58651d84..5cea1b66f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1226,7 +1226,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 1c1d10a9b..9289a90d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -231,12 +231,17 @@ namespace Barotrauma protected void CreateSubParams() { SubParams.Clear(); - var health = MainElement.GetChildElement("health"); - if (health != null) + var healthElement = MainElement.GetChildElement("health"); + if (healthElement != null) { - Health = new HealthParams(health, this); - SubParams.Add(Health); + Health = new HealthParams(healthElement, this); } + else + { + DebugConsole.ThrowError($"No health parameters defined for character \"{(SpeciesName)}\"."); + Health = new HealthParams(null, this); + } + SubParams.Add(Health); // TODO: support for multiple ai elements? var ai = MainElement.GetChildElement("ai"); if (ai != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 969c9c5bd..5acdb2a85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -29,7 +29,7 @@ namespace Barotrauma.Abilities nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true); } - protected void ApplyEffectSpecific(Character targetCharacter) + protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null) { //prevent an infinite loop if an effect triggers itself //(e.g. a talent that triggers when an affliction is applied, and applies that same affliction) @@ -66,6 +66,11 @@ namespace Barotrauma.Abilities statusEffect.SetUser(Character); statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null) + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb); + } else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffect.SetUser(Character); @@ -99,7 +104,7 @@ namespace Barotrauma.Abilities { if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf) { - ApplyEffectSpecific(targetCharacter); + ApplyEffectSpecific(targetCharacter, targetLimb: (abilityObject as AbilityApplyTreatment)?.TargetLimb); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 6c275b1c7..7214f5f83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -22,15 +22,13 @@ { foreach (Identifier afflictionIdentifier in afflictionIdentifiers) { - if (affliction.Identifier != afflictionIdentifier) { continue; } - affliction.Strength *= 1 + addedMultiplier; + if (affliction.Identifier != afflictionIdentifier) { continue; } + AfflictionPrefab afflictionPrefab = affliction.Prefab; if (!replaceWith.IsEmpty) { - if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab)) - { - abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength); - } - } + AfflictionPrefab.Prefabs.TryGet(replaceWith, out afflictionPrefab); + } + abilityAffliction.Affliction = new Affliction(afflictionPrefab, affliction.Strength * (1 + addedMultiplier)); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 428050ab2..74937c8ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,12 +1,11 @@ #nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -64,9 +63,14 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); - public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); - [return:NotNullIfNotNull("def")] - public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys) => Element.GetAttributeIdentifierArray(def, keys); + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return: NotNullIfNotNull("def")] + public ImmutableHashSet GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 84dde5cb0..3130d3fcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1878,7 +1878,9 @@ namespace Barotrauma commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("debugwiring", "Toggle the wiring debug mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); + commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 01763954d..813524345 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -12,21 +12,112 @@ namespace Barotrauma Exponential } + /// + /// ActionTypes define when a is executed. + /// public enum ActionType { - Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3, - OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7, - OnActive = 8, OnFailure = 9, OnBroken = 10, - OnFire = 11, InWater = 12, NotInWater = 13, + /// + /// Executes every frame regardless of the state of the entity. + /// + Always = 0, + /// + /// Executes when the item is picked up. Only valid for items. + /// + OnPicked = 1, + /// + /// Executes when the item is used. The meaning of "using" an item depends on the item, but generally it means the action that happens when holding the item and clicking LMB. Only valid for items. + /// + OnUse = 2, + /// + /// Executes when an item is held and the aim key is held. Only valid for items. + /// + OnSecondaryUse = 3, + /// + /// Executes continuously while the item is being worn. Only valid for wearable items. + /// + OnWearing = 4, + /// + /// Executes continuously when a specific Containable is inside an ItemContainer. Only valid for Containables defined in an ItemContainer component. + /// + OnContaining = 5, + /// + /// Executes continuously when the item is contained in some inventory. Only valid for items. + /// + OnContained = 6, + /// + /// Executes continuously when the item is NOT contained in an inventory. Only valid for items. + /// + OnNotContained = 7, + /// + /// Executes continuously when the item is active. The meaning of "active" depends on the item, but generally means the item is on, powered, and doing the thing it's intended for. Only valid for items. + /// + OnActive = 8, + /// + /// Executes when using the item fails due to a failed skill check. Only valid for items. + /// + OnFailure = 9, + /// + /// Executes when using the item's condition drops to 0. Only valid for items. + /// + OnBroken = 10, + /// + /// Executes continuously when the entity is within the damage range of fire. Valid for items and characters. + /// + OnFire = 11, + /// + /// Executes continuously when the entity is submerged. Valid for items and characters. + /// + InWater = 12, + /// + /// Executes continuously when the entity is NOT submerged. Valid for items and characters. + /// + NotInWater = 13, + /// + /// Executes when the entity hits something hard enough. For items, the threshold is determined by , + /// for characters by . Valid for items and characters. + /// OnImpact = 14, + /// + /// Executes continuously when the character is eating another character. Only valid for characters. + /// OnEating = 15, + /// + /// Executes when the entity receives damage from an external source (i.e. an affliction that increases in severity, or an item degrading by itself don't count). + /// Valid for items and characters. + /// OnDamaged = 16, + /// + /// Executes when the limb gets severed. Only valid for limbs. + /// OnSevered = 17, + /// + /// Executes when a produces an item (e.g. when a plant grows a fruit). Only valid for Growable items. + /// OnProduceSpawned = 18, - OnOpen = 19, OnClose = 20, + /// + /// Executes when a is opened. Only valid for doors. + /// + OnOpen = 19, + /// + /// Executes when a is closed. Only valid for doors. + /// + OnClose = 20, + /// + /// Executes when the entity spawns. Valid for items and characters. + /// OnSpawn = 21, + /// + /// Executes when using the item succeeds based on a skill check. Only valid for items. + /// OnSuccess = 22, + /// + /// Executes when an Ability (an effect from a talent) triggers the status effect. Only valid in Abilities, the target can be either a character or an item depending on the type of Ability. + /// OnAbility = 23, + /// + /// Executes when the character dies. Only valid for characters. + /// OnDeath = OnBroken } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index 454867de5..c6792de8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -49,7 +49,7 @@ namespace Barotrauma var limb = character.AnimController.GetLimb(LimbType); if (Strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength)); + character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); } else if (Strength < 0.0f) { @@ -60,7 +60,7 @@ namespace Barotrauma { if (Strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength)); + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); } else if (Strength < 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs index 6f1cb5867..c03e7991e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Barotrauma { - class CheckSelectedItemAction : BinaryOptionAction + class CheckSelectedAction : BinaryOptionAction { public enum SelectedItemType { Primary, Secondary, Any }; @@ -16,7 +16,7 @@ namespace Barotrauma [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] public SelectedItemType ItemType { get; set; } - public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { @@ -34,7 +34,7 @@ namespace Barotrauma } if (character == null) { - DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail."); + Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail."); return false; } if (!TargetTag.IsEmpty) @@ -42,11 +42,16 @@ namespace Barotrauma IEnumerable targets = ParentEvent.GetTargets(TargetTag); if (targets.None()) { - DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); return false; } foreach (var target in targets) { + if (target is Character targetCharacter) + { + if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; } + continue; + } if (target is not Item targetItem) { continue; @@ -79,6 +84,18 @@ namespace Barotrauma _ => false }; } +#if DEBUG + void Error(string errorMsg) + { + DebugConsole.ThrowError(errorMsg); + } +#else + + void Error(string errorMsg) + { + DebugConsole.LogError(errorMsg); + } +#endif } private string GetEventName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index be2f85fbd..3973643a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -60,18 +60,42 @@ namespace Barotrauma { if (isFinished) { return; } + Identifier missionDebugId = (MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier); + if (GameMain.GameSession.GameMode is CampaignMode campaign) { Mission unlockedMission = null; - var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes); + var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes, mustAllowLocationTypeChanges: false); + + if (unlockLocation == null && UnlockFurtherOnMap) + { + DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" further on the map. Attempting to find a location earlier on the map..."); + unlockLocation = FindUnlockLocation(MinLocationDistance, unlockFurtherOnMap: false, LocationTypes, mustAllowLocationTypeChanges: false); + } + if (unlockLocation == null && CreateLocationIfNotFound) { + DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\". Attempting to change the type of an empty location to create a suitable location..."); //find an empty location at least 3 steps away, further on the map - var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); + var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable(), + mustAllowLocationTypeChanges: true, + requireCorrectFaction: false); + if (emptyLocation == null) + { + DebugConsole.NewMessage($"Failed to find a suitable empty location further on the map. Attempting to find a location earlier on the map..."); + emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: false, "none".ToIdentifier().ToEnumerable(), + mustAllowLocationTypeChanges: true, + requireCorrectFaction: false); + } if (emptyLocation != null) { + System.Diagnostics.Debug.Assert(!emptyLocation.LocationTypeChangesBlocked); emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; + if (!RequiredFaction.IsEmpty) + { + emptyLocation.Faction = campaign.Factions.Find(f => f.Prefab.Identifier == RequiredFaction); + } } } @@ -115,13 +139,13 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); } } isFinished = true; } - private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable locationTypes) + private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction = true) { var campaign = GameMain.GameSession.GameMode as CampaignMode; if (LocationTypes.Length == 0 && minDistance <= 1) @@ -140,7 +164,7 @@ namespace Barotrauma foreach (var location in currentLocations) { checkedLocations.Add(location); - if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes)) + if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes, mustAllowLocationTypeChanges, requireCorrectFaction)) { return location; } @@ -160,9 +184,13 @@ namespace Barotrauma return null; } - private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable locationTypes) + private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction) { - if (!RequiredFaction.IsEmpty) + if (mustAllowLocationTypeChanges && location.LocationTypeChangesBlocked) + { + return false; + } + if (requireCorrectFaction && !RequiredFaction.IsEmpty) { if (location.Faction?.Prefab.Identifier != RequiredFaction && location.SecondaryFaction?.Prefab.Identifier != RequiredFaction) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 7905486c2..e86414d96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -15,6 +15,8 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] public bool RemoveFromCrew { get; set; } @@ -38,6 +40,8 @@ namespace Barotrauma { if (isFinished) { return; } + bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { @@ -49,9 +53,13 @@ namespace Barotrauma if (idCard != null) { idCard.TeamID = TeamID; + if (isPlayerTeam) + { + idCard.SubmarineSpecificID = 0; + } } } - if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2)) + if (AddToCrew && isPlayerTeam) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.AddCharacter(npc); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index e0d23114b..6546199a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -46,7 +46,7 @@ namespace Barotrauma var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = 100.0f, - IsFollowOrderObjective = true + IsFollowOrder = true }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index f05962579..3a6f61fe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -35,7 +35,9 @@ namespace Barotrauma AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = 100.0f, - SourceEventAction = this + SourceEventAction = this, + IsWaitOrder = true, + CloseEnough = 100 }; humanAiController.ObjectiveManager.AddObjective(gotoObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index c7253cc72..51e265f83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,12 +1,13 @@ -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { class TriggerEventAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier Identifier { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool NextRound { get; set; } + private bool isFinished; public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -26,17 +27,24 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { - var eventPrefab = EventSet.GetEventPrefab(Identifier); - if (eventPrefab == null) + if (NextRound) { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier); } else { - var ev = eventPrefab.CreateInstance(); - if (ev != null) + var eventPrefab = EventSet.GetEventPrefab(Identifier); + if (eventPrefab == null) { - GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + } + else + { + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index ae7e67384..b0655f113 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -75,7 +76,6 @@ namespace Barotrauma private readonly HashSet finishedEvents = new HashSet(); private readonly HashSet nonRepeatableEvents = new HashSet(); - private readonly HashSet usedUniqueSets = new HashSet(); #if DEBUG && SERVER @@ -102,7 +102,9 @@ namespace Barotrauma public readonly Queue QueuedEvents = new Queue(); - private struct TimeStamp + public readonly Queue QueuedEventsForNextRound = new Queue(); + + private readonly struct TimeStamp { public readonly double Time; public readonly Event Event; @@ -223,6 +225,21 @@ namespace Barotrauma } } + while (QueuedEventsForNextRound.TryDequeue(out var id)) + { + var eventPrefab = EventSet.GetEventPrefab(id); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); + continue; + } + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + QueuedEvents.Enqueue(ev); + } + } + PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; @@ -355,7 +372,6 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); - usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -1153,5 +1169,20 @@ namespace Barotrauma return false; } + + public void Load(XElement element) + { + foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty())) + { + QueuedEventsForNextRound.Enqueue(id); + } + } + + public XElement Save() + { + return new XElement("eventmanager", + new XAttribute(nameof(QueuedEventsForNextRound), + string.Join(',', QueuedEventsForNextRound))); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 2350443aa..d0cf6aea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -91,7 +91,7 @@ namespace Barotrauma if (IsClient) { return; } if (!swarmSpawned && level.CheckBeaconActive()) { - List connectedSubs = level.BeaconStation.GetConnectedSubs(); + IEnumerable connectedSubs = level.BeaconStation.GetConnectedSubs(); foreach (Item item in Item.ItemList) { if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 7ead29659..847a9a923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -320,7 +320,7 @@ namespace Barotrauma private static bool IsItemDelivered(Item item) { if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } - var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; + var submarine = item.Submarine ?? item.RootContainer?.Submarine; return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 917b31d90..296db0f2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using System.Collections.Generic; namespace Barotrauma diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 2afe74f6f..90ac22989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -11,20 +11,7 @@ namespace Barotrauma { partial class MineralMission : Mission { - private struct ResourceCluster - { - public int Amount; - public float Rotation; - - public ResourceCluster(int amount, float rotation) - { - Amount = amount; - Rotation = rotation; - } - - public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation); - } - private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary resourceAmounts = new Dictionary(); private readonly Dictionary> spawnedResources = new Dictionary>(); private readonly Dictionary relevantLevelResources = new Dictionary(); private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>(); @@ -81,13 +68,13 @@ namespace Barotrauma { var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { continue; } - if (resourceClusters.ContainsKey(identifier)) + if (resourceAmounts.ContainsKey(identifier)) { - resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation); + resourceAmounts[identifier]++; } else { - resourceClusters.Add(identifier, (1, 0.0f)); + resourceAmounts.Add(identifier, 1); } } } @@ -128,7 +115,7 @@ namespace Barotrauma if (IsClient) { return; } - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { @@ -136,10 +123,10 @@ namespace Barotrauma continue; } - var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves); - if (spawnedResources.Count < cluster.Amount) + var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves); + if (spawnedResources.Count < amount) { - DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}"); + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}"); } if (spawnedResources.None()) { continue; } @@ -194,7 +181,7 @@ namespace Barotrauma { // When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission) var handoverResources = new List(); - foreach (Identifier identifier in resourceClusters.Keys) + foreach (Identifier identifier in resourceAmounts.Keys) { if (relevantLevelResources.TryGetValue(identifier, out var availableResources)) { @@ -231,11 +218,11 @@ namespace Barotrauma private void FindRelevantLevelResources() { relevantLevelResources.Clear(); - foreach (var identifier in resourceClusters.Keys) + foreach (var identifier in resourceAmounts.Keys) { var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && i.Submarine == null && i.ParentInventory == null && - (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) + (i.GetComponent() is not Holdable h || (h.Attachable && h.Attached))) .ToArray(); relevantLevelResources.Add(identifier, items); } @@ -243,12 +230,12 @@ namespace Barotrauma private bool EnoughHaveBeenCollected() { - foreach (var kvp in resourceClusters) + foreach (var kvp in resourceAmounts) { if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { var collected = availableResources.Count(HasBeenCollected); - var needed = kvp.Value.Amount; + var needed = kvp.Value; if (collected < needed) { return false; } } else @@ -299,10 +286,10 @@ namespace Barotrauma protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true) { int i = 1; - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? ""); - Replace($"[resourcequantity{i}]", cluster.Amount.ToString()); + Replace($"[resourcequantity{i}]", amount.ToString()); i++; } Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 77e836405..7138946c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -363,7 +363,7 @@ namespace Barotrauma //make body dynamic when picked up foreach (var target in targets) { - var root = target.Item?.GetRootContainer() ?? target.Item; + var root = target.Item?.RootContainer ?? target.Item; if (root == null) { continue; } if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; } } @@ -389,7 +389,7 @@ namespace Barotrauma { TrySetRetrievalState(Target.RetrievalState.Interact); } - var root = target.Item?.GetRootContainer() ?? target.Item; + var root = target.Item?.RootContainer ?? target.Item; if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) { TrySetRetrievalState(Target.RetrievalState.PickedUp); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 0560da516..7312c3322 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -335,7 +335,7 @@ namespace Barotrauma var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost, - AllowStealing = validContainer.Key.Item.AllowStealing, + AllowStealing = validContainer.Key.Item.AllowStealing || validContainer.Key.Item.Prefab.AllowStealingContainedItems, Quality = quality, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, OriginalContainerIndex = diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index a2ab60f25..f14e1d6f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -9,6 +9,7 @@ using System.Text; using System.Xml.Linq; using Barotrauma.Networking; using System.Collections; +using System.Collections.Immutable; #if SERVER using Barotrauma.Networking; #endif @@ -415,14 +416,31 @@ namespace Barotrauma } } #endif - return Submarine.MainSub.GetItems(true).FindAll(item => + return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList(); + } + + public static IReadOnlyCollection FindAllItemsOnPlayerAndSub(Character character) + { + List allItems = new(); + if (character?.Inventory is { } inv) + { + allItems.AddRange(inv.FindAllItems(recursive: true)); + } + allItems.AddRange(FindAllSellableItems()); + return allItems; + } + + public static IEnumerable FindAllSellableItems() + { + if (Submarine.MainSub is null) { return Enumerable.Empty(); } + + return Submarine.MainSub.GetItems(true).FindAll(static item => { - if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } if (item.GetRootInventoryOwner() is Character) { return false; } - if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } - if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } + if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -592,7 +610,7 @@ namespace Barotrauma (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; static void itemSpawned(PurchasedItem purchased, Item item) { - Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; + Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; if (sub != null) { foreach (WifiComponent wifiComponent in item.GetComponents()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 85535d130..9ff3c9aa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -143,6 +143,8 @@ namespace Barotrauma public virtual bool PurchasedLostShuttles { get; set; } public virtual bool PurchasedItemRepairs { get; set; } + public bool DivingSuitWarningShown; + private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } @@ -738,9 +740,11 @@ namespace Barotrauma //if there's a sub docked to the outpost, we can leave the level if (Level.Loaded.StartOutpost.DockedTo.Any()) { - var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } - return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; + foreach (var dockedSub in Level.Loaded.StartOutpost.DockedTo) + { + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; } + return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; + } } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost @@ -776,9 +780,11 @@ namespace Barotrauma //if there's a sub docked to the outpost, we can leave the level if (Level.Loaded.EndOutpost.DockedTo.Any()) { - var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } - return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; + foreach (var dockedSub in Level.Loaded.EndOutpost.DockedTo) + { + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; } + return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; + } } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost @@ -930,6 +936,9 @@ namespace Barotrauma CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1); } + //no tutorials after finishing the campaign once + Settings.TutorialEnabled = false; + GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Complete, Preset?.Identifier.Value ?? "none"); @@ -985,7 +994,7 @@ namespace Barotrauma if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) { - if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + if (MathF.Round(GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation) { return false; } @@ -1204,15 +1213,17 @@ namespace Barotrauma { TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); + DivingSuitWarningShown = element.GetAttributeBool(nameof(DivingSuitWarningShown).ToLowerInvariant(), false); } protected XElement SaveStats() { return new XElement("stats", new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime), - new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels)); + new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels), + new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown)); } - + public void LogState() { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 97521a5b1..728dafe88 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -223,6 +223,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; case Wallet.LowerCaseSaveElementName: Bank = new Wallet(Option.None(), subElement); break; @@ -272,29 +275,35 @@ namespace Barotrauma bool isSubmarineVisible(SubmarineInfo s) => !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); - - List availableSubs = - SubmarineInfo.SavedSubmarines + + var availableSubs = SubmarineInfo.SavedSubmarines; +#if CLIENT + if (GameMain.Client != null) + { + availableSubs = GameMain.Client.ServerSubmarines; + } +#endif + + List campaignSubs = + availableSubs .Where(s => s.IsCampaignCompatible && isSubmarineVisible(s)) .ToList(); - if (!availableSubs.Any()) + if (!campaignSubs.Any()) { //None of the available subs were marked as campaign-compatible, just include all visible subs - availableSubs.AddRange( - SubmarineInfo.SavedSubmarines - .Where(isSubmarineVisible)); + campaignSubs.AddRange(availableSubs.Where(isSubmarineVisible)); } - if (!availableSubs.Any()) + if (!campaignSubs.Any()) { //No subs are visible at all! Just make the selected one available - availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); + campaignSubs.Add(GameMain.NetLobbyScreen.SelectedSub); } - return availableSubs; + return campaignSubs; } private static void WriteItems(IWriteMessage msg, Dictionary> purchasedItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index e376e4e22..7c7c9e453 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -328,11 +328,11 @@ namespace Barotrauma Campaign!.TransferItemsOnSubSwitch = transferItems; } - public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) + public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { - if (Campaign is null) { return; } + if (Campaign is null) { return false; } int price = newSubmarine.GetPrice(); - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; } + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); @@ -341,6 +341,7 @@ namespace Barotrauma (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); #endif } + return true; } public bool IsSubmarineOwned(SubmarineInfo query) @@ -504,7 +505,8 @@ namespace Barotrauma } GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}"); #endif - if (GameMode is CampaignMode campaignMode) + var campaignMode = GameMode as CampaignMode; + if (campaignMode != null) { if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) { @@ -532,7 +534,7 @@ namespace Barotrauma } #endif #if CLIENT - if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } + if (campaignMode != null && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary; if (existingRoundSummary?.ContinueButton != null) @@ -575,6 +577,14 @@ namespace Barotrauma HintManager.OnRoundStarted(); #endif + if (campaignMode is { DivingSuitWarningShown: false } && + Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000) + { +#if CLIENT + CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f); +#endif + campaignMode.DivingSuitWarningShown = true; + } } private void InitializeLevel(Level? level) @@ -691,7 +701,7 @@ namespace Barotrauma if (port.IsHorizontal || port.Docked) { continue; } if (port.Item.Submarine == level.StartOutpost) { - if (port.DockingTarget == null) + if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort)) { outPostPort = port; } @@ -972,7 +982,7 @@ namespace Barotrauma Dictionary submarineInventory = new Dictionary(); foreach (Item item in Item.ItemList) { - var rootContainer = item.GetRootContainer() ?? item; + var rootContainer = item.RootContainer ?? item; if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 90eb890d0..b1effb055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -85,6 +85,22 @@ namespace Barotrauma InitProjSpecific(element); + var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)); + int itemCount = itemElements.Count(); + if (itemCount > capacity) + { + DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for."); + } +#if DEBUG + else if (itemCount > capacity - 2) + { + DebugConsole.ThrowError( + $"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " + + "This can cause issues with talents that spawn extra loot in monsters' inventories." + + " Consider increasing the inventory size."); + } +#endif + if (!spawnInitialItems) { return; } #if CLIENT @@ -92,10 +108,8 @@ namespace Barotrauma if (GameMain.Client != null) { return; } #endif - foreach (var subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } - + foreach (var subElement in itemElements) + { string itemIdentifier = subElement.GetAttributeString("identifier", ""); if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 29ac8fea9..04a879f24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -1,13 +1,15 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; +#if CLIENT +using Barotrauma.Lights; +#endif namespace Barotrauma.Items.Components { @@ -243,10 +245,12 @@ namespace Barotrauma.Items.Components if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) { target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); + target.item.Submarine.RefreshConnectedSubs(); } if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) { item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); + item.Submarine.RefreshConnectedSubs(); } DockingTarget = target; @@ -507,9 +511,9 @@ namespace Barotrauma.Items.Components wire.RemoveConnection(DockingTarget.item); powerConnection.TryAddLink(wire); - wire.Connect(powerConnection, false, false); + wire.TryConnect(powerConnection, addNode: false); recipient.TryAddLink(wire); - wire.Connect(recipient, false, false); + wire.TryConnect(recipient, addNode: false); //Flag connections to be updated Powered.ChangedConnections.Add(powerConnection); @@ -558,6 +562,7 @@ namespace Barotrauma.Items.Components var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine }; bodies = new Body[4]; + RemoveConvexHulls(); if (DockingTarget.Door != null) { @@ -648,8 +653,10 @@ namespace Barotrauma.Items.Components hullRects[i].X -= expand; hullRects[i].Width += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(hullRects[i], subs[i]); - hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; + hulls[i] = new Hull(hullRects[i], subs[i]) + { + RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch" + }; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -661,6 +668,15 @@ namespace Barotrauma.Items.Components BodyType.Static); } } +#if CLIENT + for (int i = 0; i < 2; i++) + { + convexHulls[i] = + new ConvexHull(new Rectangle( + new Point((int)item.Position.X, item.Rect.Y - item.Rect.Height * i), + new Point((int)(DockingTarget.item.WorldPosition.X - item.WorldPosition.X), 0)), IsHorizontal, item); + } +#endif if (rightHullDiff <= 100 && hulls[0].Submarine != null) { @@ -764,15 +780,17 @@ namespace Barotrauma.Items.Components hullRects[1].Height += midHullDiff / 2 + 1; } - int expand = 5; for (int i = 0; i < 2; i++) { hullRects[i].Y += expand; hullRects[i].Height += expand * 2; hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); - hulls[i] = new Hull(hullRects[i], subs[i]); - hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; + hulls[i] = new Hull(hullRects[i], subs[i]) + { + RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch", + AvoidStaying = true + }; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -784,6 +802,15 @@ namespace Barotrauma.Items.Components BodyType.Static); } } +#if CLIENT + for (int i = 0; i < 2; i++) + { + convexHulls[i] = + new ConvexHull(new Rectangle( + new Point(item.Rect.X + item.Rect.Width * i, (int)item.Position.Y), + new Point(0, (int)(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y))), IsHorizontal, item); + } +#endif if (midHullDiff <= 100 && hulls[0].Submarine != null) { @@ -822,6 +849,8 @@ namespace Barotrauma.Items.Components } } + partial void RemoveConvexHulls(); + private void LinkHullsToGaps() { if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null) @@ -916,7 +945,9 @@ namespace Barotrauma.Items.Components } DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine); + DockingTarget.item.Submarine.RefreshConnectedSubs(); item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine); + item.Submarine.RefreshConnectedSubs(); if (Door != null && DockingTarget.Door != null) { @@ -976,6 +1007,8 @@ namespace Barotrauma.Items.Components hulls[0]?.Remove(); hulls[0] = null; hulls[1]?.Remove(); hulls[1] = null; + RemoveConvexHulls(); + if (gap != null) { gap.Remove(); @@ -1091,6 +1124,7 @@ namespace Barotrauma.Items.Components hulls[0]?.Remove(); hulls[0] = null; hulls[1]?.Remove(); hulls[1] = null; gap?.Remove(); gap = null; + RemoveConvexHulls(); overlaySprite?.Remove(); overlaySprite = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index ccf437103..e282bbb82 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components private bool isBroken; - public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable; + public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen); public bool IsBroken { @@ -186,13 +186,19 @@ namespace Barotrauma.Items.Components { get { return openState; } set - { + { openState = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT float size = IsHorizontal ? item.Rect.Width : item.Rect.Height; - if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; } - UpdateConvexHulls(); - lastConvexHullState = openState; + //refresh convex hulls if the body of the door has moved by 5 pixels, + //or if it becomes fully closed or fully open + if (Math.Abs(lastConvexHullState - openState) * size > 5.0f || + (openState <= 0.0f && lastConvexHullState > 0.0f) || + (openState >= 1.0f && lastConvexHullState < 1.0f)) + { + UpdateConvexHulls(); + lastConvexHullState = openState; + } #endif } } @@ -523,11 +529,11 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - Vector2[] corners = GetConvexHullCorners(Rectangle.Empty); - - convexHull = new ConvexHull(corners, Color.Black, item); - if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item); - + convexHull = new ConvexHull(doorRect, IsHorizontal, item); + if (Window != Rectangle.Empty) + { + convexHull2 = new ConvexHull(doorRect, IsHorizontal, item); + } UpdateConvexHulls(); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 059832004..11b26aee2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -96,11 +95,16 @@ namespace Barotrauma.Items.Components if (selectedEffect != null) { targetCharacter = character; - ApplyStatusEffects(ActionType.OnWearing, 1.0f); + ApplyStatusEffects(ActionType.OnWearing, 1.0f, targetCharacter); float selectedEffectStrength = GetCombinedEffectStrength(); character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); - if (affliction != null) { affliction.Strength = selectedEffectStrength; } + if (affliction != null) + { + affliction.Strength = selectedEffectStrength; + //force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing + affliction.SetStrength(selectedEffectStrength); + } #if SERVER item.CreateServerEvent(this); #endif @@ -110,7 +114,12 @@ namespace Barotrauma.Items.Components float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength(); character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); - if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; } + if (affliction != null) + { + affliction.Strength = selectedTaintedEffectStrength; + //force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing + affliction.SetStrength(selectedTaintedEffectStrength); + } targetCharacter = character; #if SERVER item.CreateServerEvent(this); @@ -127,7 +136,7 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); if (targetCharacter != null) { - var rootContainer = item.GetRootContainer(); + var rootContainer = item.RootContainer; if (!targetCharacter.HasEquippedItem(item) && (rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface))) { @@ -220,7 +229,7 @@ namespace Barotrauma.Items.Components return MathHelper.Clamp(probability, 0.0f, 1.0f); } - private float GetTaintedProbabilityOnCombine(Character user) + private static float GetTaintedProbabilityOnCombine(Character user) { if (user == null) { return 1.0f; } float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index ccb9ba004..2ecabc5e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -409,6 +409,7 @@ namespace Barotrauma.Items.Components private int leafVariants; private int[] flowerTiles; + [Serialize(100.0f, IsPropertySaveable.Yes)] public float Health { get => health; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 5890b1ec9..14d8699c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -321,12 +321,12 @@ namespace Barotrauma.Items.Components } } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { - Drop(true, dropper); + Drop(true, dropper, setTransform); } - private void Drop(bool dropConnectedWires, Character dropper) + private void Drop(bool dropConnectedWires, Character dropper, bool setTransform = true) { GetRope()?.Snap(); if (dropConnectedWires) @@ -343,8 +343,11 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } - if (Pusher != null) { Pusher.Enabled = false; } - if (item.body != null) { item.body.Enabled = true; } + if (setTransform) + { + if (Pusher != null) { Pusher.Enabled = false; } + if (item.body != null) { item.body.Enabled = true; } + } IsActive = false; attachTargetCell = null; @@ -357,7 +360,7 @@ namespace Barotrauma.Items.Components item.Submarine = picker.Submarine; - if (item.body != null) + if (item.body != null && setTransform) { if (item.body.Removed) { @@ -599,6 +602,10 @@ namespace Barotrauma.Items.Components throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier})."); } + body.Enabled = false; + body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f); + item.body = null; + //outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub if (item.CurrentHull == null && item.Submarine == null) { @@ -638,9 +645,6 @@ namespace Barotrauma.Items.Components } } - body.Enabled = false; - item.body = null; - DisplayMsg = prevMsg; PickKey = prevPickKey; requiredItems = new Dictionary>(prevRequiredItems); @@ -812,7 +816,7 @@ namespace Barotrauma.Items.Components foreach (var edge in cell.Edges) { if (!edge.IsSolid) { continue; } - if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection)) + if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection)) { attachPos = intersection; edgeFound = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 816ff3c9b..353d25d9a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -97,11 +97,18 @@ namespace Barotrauma.Items.Components { if (holdable != null && !holdable.Attached) { - trigger.Enabled = false; + if (trigger != null) + { + trigger.Enabled = false; + } IsActive = false; } else { + if (trigger == null) + { + CreateTriggerBody(); + } if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) { trigger.SetTransform(item.SimPosition, 0.0f); @@ -123,12 +130,15 @@ namespace Barotrauma.Items.Components { holdable.PickingTime = float.MaxValue; } + } + private void CreateTriggerBody() + { + System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!"); var body = item.body ?? holdable.Body; - - if (body != null) + if (body != null && Attached) { - trigger = new PhysicsBody(body.Width, body.Height, body.Radius, + trigger = new PhysicsBody(body.Width, body.Height, body.Radius, body.Density, BodyType.Static, Physics.CollisionWall, @@ -143,7 +153,6 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { - base.RemoveComponentSpecific(); if (trigger != null) { trigger.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index da760edf6..817f07d59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -170,9 +170,9 @@ namespace Barotrauma.Items.Components return characterUsable || character == null; } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { - base.Drop(dropper); + base.Drop(dropper, setTransform); hitting = false; hitPos = 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 9f83723a2..03f31ed8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -241,12 +241,9 @@ namespace Barotrauma.Items.Components } } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { - if (picker == null) - { - picker = dropper; - } + picker ??= dropper; Vector2 bodyDropPos = Vector2.Zero; @@ -255,8 +252,7 @@ namespace Barotrauma.Items.Components if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed) { bodyDropPos = item.ParentInventory.Owner.SimPosition; - - if (item.body != null) item.body.ResetDynamics(); + item.body?.ResetDynamics(); } } else if (!picker.Removed) @@ -270,7 +266,7 @@ namespace Barotrauma.Items.Components picker = null; } - if (item.body != null && !item.body.Enabled) + if (item.body != null && !item.body.Enabled && setTransform) { if (item.body.Removed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index bbee3559e..71d2510a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -912,17 +912,16 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { - if (!(target is Door door)) { continue; } + if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } - for (int i = 0; i < effect.propertyNames.Length; i++) + foreach (var propertyEffect in effect.PropertyEffects) { - Identifier propertyName = effect.propertyNames[i]; - if (propertyName != "stuck") { continue; } - if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } + if (propertyEffect.propertyName != "stuck") { continue; } + if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); if (door.Stuck > 0) { - bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0; + bool isCutting = propertyEffect.value is float and < 0; var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, textTag: isCutting ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 422152d7c..41d9c5df0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -56,9 +56,9 @@ namespace Barotrauma.Items.Components return false; } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { - base.Drop(dropper); + base.Drop(dropper, setTransform); throwState = ThrowState.None; throwAngle = ThrowAngleStart; Item.ResetWaterDragCoefficient(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 3dacee1f8..dbefb0e5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -440,7 +440,7 @@ namespace Barotrauma.Items.Components } /// a Character has dropped the item - public virtual void Drop(Character dropper) { } + public virtual void Drop(Character dropper, bool setTransform = true) { } /// true if the operation was completed public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) @@ -722,20 +722,50 @@ namespace Barotrauma.Items.Components { if (character.IsBot && item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } - if (requiredItems.None()) { return true; } - if (character.Inventory != null) + if (requiredItems.Count == 0) { return true; } + if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List relatedItems)) { - foreach (Item item in character.Inventory.AllItems) + foreach (RelatedItem relatedItem in relatedItems) { - if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item)))) + foreach (Item otherItem in character.Inventory.AllItems) { - return true; - } + if (relatedItem.MatchesItem(otherItem)) + { + if (otherItem.GetComponent() is IdCard idCard) + { + if (!CheckIdCardAccess(relatedItem, idCard)) + { + continue; + } + } + return true; + } + } } } return false; } + /// + /// Presumes that matching is already checked. + /// + private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard) + { + if (item.Submarine != null) + { + //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) + if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) + { + return false; + } + else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID) + { + return false; + } + } + return true; + } + public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { if (requiredItems.None()) { return true; } @@ -771,23 +801,14 @@ namespace Barotrauma.Items.Components bool CheckItems(RelatedItem relatedItem, IEnumerable itemList) { - bool Predicate(Item it) + bool Predicate(Item it) { if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; } - if (item.Submarine != null) + if (it.GetComponent() is IdCard idCard) { - var idCard = it.GetComponent(); - if (idCard != null) + if (!CheckIdCardAccess(relatedItem, idCard)) { - //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) - if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) - { - return false; - } - else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID) - { - return false; - } + return false; } } return true; @@ -1027,7 +1048,7 @@ namespace Barotrauma.Items.Components prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null; if (prevRequiredItem != null) { - newRequiredItem.statusEffects = prevRequiredItem.statusEffects; + newRequiredItem.StatusEffects = prevRequiredItem.StatusEffects; newRequiredItem.Msg = prevRequiredItem.Msg; newRequiredItem.IsOptional = prevRequiredItem.IsOptional; newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 548cc0319..74d18f5b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -20,11 +20,13 @@ namespace Barotrauma.Items.Components { public readonly int MaxStackSize; public List ContainableItems; + public readonly bool AutoInject; - public SlotRestrictions(int maxStackSize, List containableItems) + public SlotRestrictions(int maxStackSize, List containableItems, bool autoInject) { MaxStackSize = maxStackSize; ContainableItems = containableItems; + AutoInject = autoInject; } public bool MatchesItem(Item item) @@ -269,7 +271,7 @@ namespace Barotrauma.Items.Components List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { - newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems)); + newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems, autoInject: false)); } int subContainerIndex = capacity; @@ -279,6 +281,7 @@ namespace Barotrauma.Items.Components int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); + bool autoInject = subElement.GetAttributeBool("autoinject", false); var subContainableItems = new List(); foreach (var subSubElement in subElement.Elements()) @@ -298,7 +301,7 @@ namespace Barotrauma.Items.Components for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) { - newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems)); + newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject)); } subContainerIndex += subCapacity; } @@ -351,7 +354,7 @@ namespace Barotrauma.Items.Components foreach (var containableItem in slotRestrictions[index].ContainableItems) { if (!containableItem.MatchesItem(containedItem)) { continue; } - foreach (StatusEffect effect in containableItem.statusEffects) + foreach (StatusEffect effect in containableItem.StatusEffects) { activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); } @@ -466,7 +469,7 @@ namespace Barotrauma.Items.Components prevContainedItemPositions = item.Position; } - if (AutoInject) + if (AutoInject || slotRestrictions.Any(s => s.AutoInject)) { //normally autoinjection should delete the (medical) item, so it only gets applied once //but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times @@ -480,7 +483,21 @@ namespace Barotrauma.Items.Components ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold && ownerCharacter.HasEquippedItem(item)) { - foreach (Item item in Inventory.AllItemsMod) + if (AutoInject) + { + Inventory.AllItemsMod.ForEach(i => Inject(i)); + } + else + { + for (int i = 0; i < slotRestrictions.Length; i++) + { + if (slotRestrictions[i].AutoInject) + { + Inventory.GetItemsAt(i).ForEachMod(i => Inject(i)); + } + } + } + void Inject(Item item) { item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter); item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter); @@ -632,7 +649,7 @@ namespace Barotrauma.Items.Components return false; } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { IsActive = true; SetContainedActive(false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index b1a12a52c..a9f4467de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -98,6 +98,20 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")] + public bool AllowSelectingWhenSelectedByOther + { + get; + set; + } + + [Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")] + public bool AllowSelectingWhenSelectedByBot + { + get; + set; + } + public bool ControlCharacterPose { get { return limbPositions.Count > 0; } @@ -466,8 +480,18 @@ namespace Barotrauma.Items.Components IsActive = false; CancelUsing(user); user = null; - return false; } + else if (user.IsBot && !activator.IsBot) + { + if (AllowSelectingWhenSelectedByBot) + { + CancelUsing(user); + user = activator; + IsActive = true; + return true; + } + } + return AllowSelectingWhenSelectedByOther; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index e3d29d09b..af7668a08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -10,6 +10,18 @@ namespace Barotrauma.Items.Components { private float force; + /// + /// Latest signal the set_force connection received, used to set in the Update method. + /// We use a separate variable, because otherwise specific item update orders and sending multiple signals to set_force would lead to bugs: + /// targetForce could be set to 0, then a power grid might update as if the engine was off and mark the voltage of the grid as 1, + /// then another item could set the targetForce to 100 and make it run without power. + /// + private float? lastReceivedTargetForce; + + /// + /// The amount of force the engine is aiming for (the actual force may be less than this, + /// depending on the amount of power, the condition of the engine or boosts from talents) + /// private float targetForce; private float maxForce; @@ -58,7 +70,7 @@ namespace Barotrauma.Items.Components public float CurrentVolume { - get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); } + get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); } } public float CurrentBrokenVolume @@ -110,7 +122,10 @@ namespace Barotrauma.Items.Components hasPower = Voltage > MinVoltage; } - + if (lastReceivedTargetForce.HasValue) + { + targetForce = lastReceivedTargetForce.Value; + } Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) { @@ -254,7 +269,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce)) { controlLockTimer = 0.1f; - targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f); + lastReceivedTargetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f); User = signal.sender; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index f8f3df86e..71af0286b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -784,8 +784,10 @@ namespace Barotrauma.Items.Components } else { + float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0; + float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0; //prefer items in worse condition - return Math.Sign(item2.Condition - item1.Condition); + return Math.Sign(condition2 - condition1); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 3ecd2752f..1fdff1981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -775,7 +775,6 @@ namespace Barotrauma.Items.Components if (shutDown) { PowerOn = false; - AutoTemp = false; TargetFissionRate = 0.0f; TargetTurbineOutput = 0.0f; unsentChanges = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 1beb6ee19..f76dace91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components public bool UseDirectionalPing => useDirectionalPing; private bool useDirectionalPing = false; private Vector2 pingDirection = new Vector2(1.0f, 0.0f); - private bool useMineralScanner; private bool aiPingCheckPending; @@ -133,6 +132,9 @@ namespace Barotrauma.Items.Components } } + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool UseMineralScanner { get; set; } + public float Zoom { get { return zoom; } @@ -366,7 +368,7 @@ namespace Barotrauma.Items.Components bool isActive = msg.ReadBoolean(); bool directionalPing = useDirectionalPing; float zoomT = zoom, pingDirectionT = 0.0f; - bool mineralScanner = useMineralScanner; + bool mineralScanner = UseMineralScanner; if (isActive) { zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); @@ -391,13 +393,13 @@ namespace Barotrauma.Items.Components float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT); pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); } - useMineralScanner = mineralScanner; + UseMineralScanner = mineralScanner; #if CLIENT zoomSlider.BarScroll = zoomT; directionalModeSwitch.Selected = useDirectionalPing; if (mineralScannerSwitch != null) { - mineralScannerSwitch.Selected = useMineralScanner; + mineralScannerSwitch.Selected = UseMineralScanner; } #endif } @@ -418,7 +420,7 @@ namespace Barotrauma.Items.Components float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } - msg.WriteBoolean(useMineralScanner); + msg.WriteBoolean(UseMineralScanner); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index cc73b26fd..daa50559e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -444,7 +444,7 @@ namespace Barotrauma.Items.Components if (connectedSubUpdateTimer <= 0.0f) { connectedSubs.Clear(); - connectedSubs = controlledSub?.GetConnectedSubs(); + connectedSubs.AddRange(controlledSub.GetConnectedSubs()); connectedSubUpdateTimer = ConnectedSubUpdateInterval; } @@ -535,7 +535,7 @@ namespace Barotrauma.Items.Components foreach (GraphEdge edge in cell.Edges) { - if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection)) + if (MathUtils.GetLineSegmentIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection)) { Vector2 diff = controlledSub.WorldPosition - intersection; //far enough -> ignore diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 8c15f3019..c1c82e68f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components { //other junction boxes don't need to receive the signal in the pass-through signal connections //because we relay it straight to the connected items without going through the whole chain of junction boxes - if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } + if (ic is PowerTransfer && ic is not RelayComponent) { continue; } ic.ReceiveSignal(signal, recipient); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index dfc4db02a..1ad742343 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -709,7 +709,7 @@ namespace Barotrauma.Items.Components return hits; } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { Item.ResetWaterDragCoefficient(); if (dropper != null) @@ -717,7 +717,7 @@ namespace Barotrauma.Items.Components DisableProjectileCollisions(); Unstick(); } - base.Drop(dropper); + base.Drop(dropper, setTransform); } public override void Update(float deltaTime, Camera cam) @@ -939,7 +939,7 @@ namespace Barotrauma.Items.Components Character character = null; if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item) { - item.Move(-submarine.Position); + item.Move(-submarine.Position, ignoreContacts: false); item.Submarine = submarine; item.body.Submarine = submarine; return !Hitscan; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index d15979d30..64cc2add8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -339,18 +339,19 @@ namespace Barotrauma.Items.Components if (Math.Abs(TargetPullForce) > 0.001f) { var targetBody = GetBodyToPull(target); - if (user != null && targetCharacter != null && !user.AnimController.InWater) + bool lerpForces = LerpForces; + if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater) { - // Prevents rubberbanding horizontally when dragging a corpse. if ((forceDir.X < 0) != (user.AnimController.Dir < 0)) { - forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f); + // Prevents rubberbanding horizontally when dragging a corpse. + lerpForces = true; } } - float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce; + float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce; targetBody?.ApplyForce(-forceDir * force); var targetRagdoll = targetCharacter?.AnimController; - if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround)) + if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround)) { targetRagdoll.Collider.ApplyForce(-forceDir * force * 3); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 224d1882f..6f0e8a1cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -29,10 +29,10 @@ namespace Barotrauma.Items.Components private readonly Item item; public readonly bool IsOutput; - + public readonly List Effects; - public readonly List LoadedWireIds; + public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires; //The grid the connection is a part of public GridInfo Grid; @@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components //Priority in which power output will be handled - load is unaffected public PowerPriority Priority = PowerPriority.Default; + public Signal LastSentSignal { get; private set; } + public Signal LastReceivedSignal {get; private set;} + public bool IsPower { get; @@ -151,16 +154,20 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - LoadedWireIds = new List(); + LoadedWires = new List<(ushort wireId, int? connectionIndex)>(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "link": int id = subElement.GetAttributeInt("w", 0); + int? i = null; + if (subElement.GetAttribute("i") != null) + { + i = subElement.GetAttributeInt("i", 0); + } if (id < 0) { id = 0; } - if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); } - + if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); } break; case "statuseffect": Effects ??= new List(); @@ -288,6 +295,7 @@ namespace Barotrauma.Items.Components public void SendSignal(Signal signal) { + LastSentSignal = signal; enumeratingWires = true; foreach (var wire in wires) { @@ -298,6 +306,10 @@ namespace Barotrauma.Items.Components signal.source?.LastSentSignalRecipients.Add(recipient); Connection connection = recipient; + connection.LastReceivedSignal = signal; +#if CLIENT + wire.RegisterSignal(signal, source: this); +#endif foreach (ItemComponent ic in recipient.item.Components) { @@ -351,22 +363,29 @@ namespace Barotrauma.Items.Components public void InitializeFromLoaded() { - if (LoadedWireIds.Count == 0) { return; } + if (LoadedWires.Count == 0) { return; } - for (int i = 0; i < LoadedWireIds.Count; i++) + foreach ((ushort wireId, int? connectionIndex) in LoadedWires) { - if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; } + if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; } var wire = wireItem.GetComponent(); if (wire != null && TryAddLink(wire)) { - if (wire.Item.body != null) wire.Item.body.Enabled = false; - wire.Connect(this, false, false); + if (wire.Item.body != null) { wire.Item.body.Enabled = false; } + if (connectionIndex.HasValue) + { + wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false); + } + else + { + wire.TryConnect(this, addNode: false, sendNetworkEvent: false); + } wire.FixNodeEnds(); recipientsDirty = true; } } - LoadedWireIds.Clear(); + LoadedWires.Clear(); } @@ -377,7 +396,8 @@ namespace Barotrauma.Items.Components foreach (var wire in wires.OrderBy(w => w.Item.ID)) { newElement.Add(new XElement("link", - new XAttribute("w", wire.Item.ID.ToString()))); + new XAttribute("w", wire.Item.ID.ToString()), + new XAttribute("i", wire.Connections[0] == this ? 0 : 1))); } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 3954c06ed..7c9fd3a1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -295,8 +295,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - Connections[i].LoadedWireIds.Clear(); - Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds); + Connections[i].LoadedWires.Clear(); + Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires); } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index d196c1b03..20ec92857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -82,8 +82,10 @@ namespace Barotrauma.Items.Components signalOut.SendDuration -= 1; item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out"); if (signalOut.SendDuration <= 0) - { - signalQueue.Dequeue(); + { + //check the queue isn't empty again, because sending the signal may empty it + //if this component is set to reset when it receives a signal and the signal is routed back to this component + signalQueue.TryDequeue(out _); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 097d23817..d2c88ab2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -173,6 +173,8 @@ namespace Barotrauma.Items.Components set { lightColor = value; + //reset previously received signal to force updating the color if we receive a set_color signal after the color has been modified manually + prevColorSignal = string.Empty; #if CLIENT if (Light != null) { @@ -249,6 +251,11 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); SetLightSourceState(IsActive, lightBrightness); turret = item.GetComponent(); + if (item.body != null) + { + item.body.FarseerBody.OnEnabled += CheckIfNeedsUpdate; + item.body.FarseerBody.OnDisabled += CheckIfNeedsUpdate; + } #if CLIENT Drawable = AlphaBlend && Light.LightSprite != null; if (Screen.Selected.IsEditor) @@ -277,15 +284,24 @@ namespace Barotrauma.Items.Components return; } - if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null && + if ((item.body == null || !item.body.Enabled) && + powerConsumption <= 0.0f && Parent == null && turret == null && (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - lightBrightness = 1.0f; - SetLightSourceState(true, lightBrightness); + if (item.body != null && !item.body.Enabled) + { + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); + } + else + { + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); + } + isOn = true; SetLightSourceTransformProjSpecific(); base.IsActive = false; - isOn = true; #if CLIENT Light.ParentSub = item.Submarine; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 93527069d..eec77455a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -222,10 +222,10 @@ namespace Barotrauma.Items.Components { Vector2 e1 = edge.Point1 + cell.Translation; Vector2 e2 = edge.Point2 + cell.Translation; - if (MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) || - MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) || - MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) || - MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom))) + if (MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) || + MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) || + MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) || + MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom))) { MotionDetected = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs index ff178fde8..5385f05da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs @@ -8,6 +8,9 @@ namespace Barotrauma.Items.Components internal Item source; internal float power; internal float strength; + public readonly double CreationTime; + + public double TimeSinceCreated => Timing.TotalTimeUnpaused - CreationTime; internal Signal(string value, int stepsTaken = 0, Character sender = null, Item source = null, float power = 0.0f, float strength = 1.0f) @@ -18,6 +21,7 @@ namespace Barotrauma.Items.Components this.source = source; this.power = power; this.strength = strength; + CreationTime = Timing.TotalTimeUnpaused; } internal Signal WithStepsTaken(int stepsTaken) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index f596fd3d5..ebc87ccb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -64,7 +64,7 @@ namespace Barotrauma.Items.Components set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat, onlyInEditors: false)] [Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 09dfe9080..39da66dbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private Vector2 end; private readonly float angle; - private readonly float length; + public readonly float Length; public Vector2 Start { @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components this.end = end; angle = MathUtils.VectorToAngle(end - start); - length = Vector2.Distance(start, end); + Length = Vector2.Distance(start, end); } } @@ -52,7 +52,7 @@ namespace Barotrauma.Items.Components private List nodes; private readonly List sections; - private Connection[] connections; + private readonly Connection[] connections; private bool canPlaceNode; private Vector2 newNodePos; @@ -81,6 +81,8 @@ namespace Barotrauma.Items.Components get { return connections; } } + public float Length { get; private set; } + [Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")] public float MaxLength { @@ -162,14 +164,50 @@ namespace Barotrauma.Items.Components SetConnectedDirty(); } - public bool Connect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + + public bool TryConnect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + { + if (connections[0] == null) + { + return Connect(newConnection, 0, addNode, sendNetworkEvent); + } + else if (connections[1] == null) + { + return Connect(newConnection, 1, addNode, sendNetworkEvent); + } + return false; + } + + + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + /// Which end of the wire to add the connection to? 0 or 1. + /// Normally doesn't make a difference, but matters if we're copying/loading a wire, + /// in which case the 1st node should be located at the same item as the 1st connection. + /// + public bool Connect(Connection newConnection, int connectionIndex, bool addNode = true, bool sendNetworkEvent = false) { for (int i = 0; i < 2; i++) { if (connections[i] == newConnection) { return false; } } - if (!connections.Any(c => c == null)) { return false; } + if (connectionIndex < 0 || connectionIndex > 1) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: {connectionIndex} is not a valid index."); + return false; + } + if (connections[connectionIndex] != null) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: a wire is already connected to the index {connectionIndex}."); + return false; + } for (int i = 0; i < 2; i++) { @@ -183,70 +221,12 @@ namespace Barotrauma.Items.Components newConnection.ConnectionPanel.DisconnectedWires.Remove(this); - for (int i = 0; i < 2; i++) + connections[connectionIndex] = newConnection; + FixNodeEnds(); + + if (addNode) { - if (connections[i] != null) { continue; } - - connections[i] = newConnection; - FixNodeEnds(); - - if (!addNode) { break; } - - Submarine refSub = newConnection.Item.Submarine; - if (refSub == null) - { - Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); - if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) - { - connections[i] = null; - continue; - } - refSub = attachTarget?.Submarine; - } - - Vector2 nodePos = refSub == null ? - newConnection.Item.Position : - newConnection.Item.Position - refSub.HiddenSubPosition; - - if (nodes.Count > 0 && nodes[0] == nodePos) { break; } - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } - - //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) - int newNodeIndex = 0; - if (nodes.Count > 1) - { - if (connections[0] != null && connections[0] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (connections[1] != null && connections[1] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) - { - newNodeIndex = nodes.Count; - } - } - - if (newNodeIndex == 0 && nodes.Count > 1) - { - nodes.Insert(0, nodePos); - } - else - { - nodes.Add(nodePos); - } - - break; + AddNode(newConnection, connectionIndex); } SetConnectedDirty(); @@ -258,7 +238,7 @@ namespace Barotrauma.Items.Components if (ic == this) { continue; } ic.Drop(null); } - if (item.Container != null) { item.Container.RemoveContained(this.item); } + item.Container?.RemoveContained(item); if (item.body != null) { item.body.Enabled = false; } IsActive = false; @@ -286,6 +266,63 @@ namespace Barotrauma.Items.Components return true; } + private void AddNode(Connection newConnection, int selectedIndex) + { + Submarine refSub = newConnection.Item.Submarine; + if (refSub == null) + { + Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); + if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) + { + connections[selectedIndex] = null; + return; + } + refSub = attachTarget?.Submarine; + } + + Vector2 nodePos = refSub == null ? + newConnection.Item.Position : + newConnection.Item.Position - refSub.HiddenSubPosition; + + if (nodes.Count > 0 && nodes[0] == nodePos) { return; } + if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; } + + //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) + int newNodeIndex = 0; + if (nodes.Count > 1) + { + if (connections[0] != null && connections[0] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (connections[1] != null && connections[1] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + + if (newNodeIndex == 0 && nodes.Count > 1) + { + nodes.Insert(0, nodePos); + } + else + { + nodes.Add(nodePos); + } + } + public override void Equip(Character character) { if (shouldClearConnections) { ClearConnections(character); } @@ -298,7 +335,7 @@ namespace Barotrauma.Items.Components IsActive = false; } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { if (shouldClearConnections) { ClearConnections(dropper); } IsActive = false; @@ -528,6 +565,7 @@ namespace Barotrauma.Items.Components sections.Add(new WireSection(nodes[i], nodes[i + 1])); } Drawable = IsActive || sections.Count > 0; + Length = sections.Count > 0 ? sections.Sum(s => s.Length) : 0; CalculateExtents(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index c6ea9bc03..02f0807b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -845,8 +845,13 @@ namespace Barotrauma.Items.Components { public readonly Item Projectile; - public EventData(Item projectile) + public EventData(Item projectile, Turret turret) { + System.Diagnostics.Debug.Assert(projectile != null, $"Tried to create Turret {nameof(EventData)} with no projectile."); + GameAnalyticsManager.AddErrorEventOnce( + "Turret.EventData:entitynull"+ turret.Item.Prefab.Identifier, + GameAnalyticsManager.ErrorSeverity.Error, + $"Turret \"{turret.Item.Prefab.Identifier}\" tried to create {nameof(EventData)} with no projectile."); Projectile = projectile; } } @@ -918,7 +923,7 @@ namespace Barotrauma.Items.Components projectile.Container?.RemoveContained(projectile); } #if SERVER - item.CreateServerEvent(this, new EventData(projectile)); + item.CreateServerEvent(this, new EventData(projectile, this)); #endif ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user); @@ -1314,7 +1319,9 @@ namespace Barotrauma.Items.Components } // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } - if (HumanAIController.IsFriendly(character, enemy)) { continue; } + if (HumanAIController.IsFriendly(character, enemy)) { continue; } + // Don't shoot at captured enemies. + if (enemy.LockHands) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); if (dist > closestDistance) { continue; } if (dist < shootDistance * shootDistance) @@ -1413,7 +1420,7 @@ namespace Barotrauma.Items.Components { // The closest point can't be targeted -> get a point directly in front of the turret Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); - if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) + if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { closestPoint = intersection; if (!CheckTurretAngle(closestPoint)) { continue; } @@ -1889,22 +1896,27 @@ namespace Barotrauma.Items.Components { if (TryExtractEventData(extraData, out EventData eventData)) { - msg.WriteUInt16(eventData.Projectile.ID); - msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); + msg.WriteUInt16(eventData.Projectile?.ID ?? Entity.NullEntityID); + msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(rotation), minRotation, maxRotation), minRotation, maxRotation, 16); } else { msg.WriteUInt16((ushort)0); - float wrappedTargetRotation = targetRotation; - while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation)) + msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(targetRotation), minRotation, maxRotation), minRotation, maxRotation, 16); + } + + float wrapAngle(float angle) + { + float wrappedAngle = angle; + while (wrappedAngle < minRotation && MathUtils.IsValid(wrappedAngle)) { - wrappedTargetRotation += MathHelper.TwoPi; + wrappedAngle += MathHelper.TwoPi; } - while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation)) + while (wrappedAngle > maxRotation && MathUtils.IsValid(wrappedAngle)) { - wrappedTargetRotation -= MathHelper.TwoPi; + wrappedAngle -= MathHelper.TwoPi; } - msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16); + return wrappedAngle; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c5a73d53f..92e4bc74c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -482,11 +482,11 @@ namespace Barotrauma.Items.Components character.OnWearablesChanged(); } - public override void Drop(Character dropper) + public override void Drop(Character dropper, bool setTransform = true) { Character previousPicker = picker; Unequip(picker); - base.Drop(dropper); + base.Drop(dropper, setTransform); previousPicker?.OnWearablesChanged(); picker = null; IsActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 7ee77aca8..8be00ac66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -575,8 +575,8 @@ namespace Barotrauma if (removeItem) { - item.Drop(user); - if (item.ParentInventory != null) { item.ParentInventory.RemoveItem(item); } + item.Drop(user, setTransform: false); + item.ParentInventory?.RemoveItem(item); } slots[i].Add(item); @@ -835,13 +835,31 @@ namespace Barotrauma if (otherIsEquipped) { - existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true)); - stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true)); + TryPutAndForce(existingItems, this, index); + TryPutAndForce(stackedItems, otherInventory, otherIndex); } else { - stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true)); - existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true)); + TryPutAndForce(stackedItems, otherInventory, otherIndex); + TryPutAndForce(existingItems, this, index); + } + + void TryPutAndForce(IEnumerable items, Inventory inventory, int slotIndex) + { + foreach (var item in items) + { + if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) && + !inventory.GetItemsAt(slotIndex).Contains(item)) + { + inventory.ForceToSlot(item, slotIndex); + } + } + } + + if (createNetworkEvent) + { + CreateNetworkEvent(); + otherInventory.CreateNetworkEvent(); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index f24dd2f6c..dcdedaa7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -43,6 +43,13 @@ namespace Barotrauma /// public static IReadOnlyCollection CleanableItems => cleanableItems; + private static readonly List sonarVisibleItems = new List(); + + /// + /// Items whose is larger than 0 + /// + public static IReadOnlyCollection SonarVisibleItems => sonarVisibleItems; + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; @@ -126,7 +133,8 @@ namespace Barotrauma private float condition; private bool inWater; - private readonly bool hasWaterStatusEffects; + private readonly bool hasInWaterStatusEffects; + private readonly bool hasNotInWaterStatusEffects; private Inventory parentInventory; private readonly ItemInventory ownInventory; @@ -206,6 +214,10 @@ namespace Barotrauma } } + public Item RootContainer { get; private set; } + + private bool inWaterProofContainer; + private Item container; public Item Container { @@ -217,6 +229,8 @@ namespace Barotrauma container = value; CheckCleanable(); SetActiveSprite(); + + RefreshRootContainer(); } } } @@ -408,14 +422,15 @@ namespace Barotrauma set { spriteColor = value; } } - [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)] public Color InventoryIconColor { get; protected set; } - [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true."), + ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)] public Color ContainerColor { get; @@ -699,14 +714,26 @@ namespace Barotrauma } } + [Serialize(false, IsPropertySaveable.No)] public bool FireProof { - get { return Prefab.FireProof; } + get; private set; } + private bool waterProof; + [Serialize(false, IsPropertySaveable.No)] public bool WaterProof { - get { return Prefab.WaterProof; } + get { return waterProof; } + private set + { + if (waterProof == value) { return; } + waterProof = value; + foreach (Item containedItem in ContainedItems) + { + containedItem.RefreshInWaterProofContainer(); + } + } } public bool UseInHealthInterface @@ -735,7 +762,7 @@ namespace Barotrauma { //if the item has an active physics body, inWater is updated in the Update method if (body != null && body.Enabled) { return inWater; } - if (hasWaterStatusEffects) { return inWater; } + if (hasInWaterStatusEffects) { return inWater; } //if not, we'll just have to check return IsInWater(); @@ -1067,7 +1094,8 @@ namespace Barotrauma } } - hasWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater] || hasStatusEffectsOfType[(int)ActionType.NotInWater]; + hasInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater]; + hasNotInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.NotInWater]; if (body != null) { @@ -1123,6 +1151,7 @@ namespace Barotrauma ItemList.Add(this); if (Prefab.IsDangerous) { dangerousItems.Add(this); } if (Repairables.Any()) { repairableItems.Add(this); } + if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); } CheckCleanable(); DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1416,7 +1445,7 @@ namespace Barotrauma } } - public override void Move(Vector2 amount, bool ignoreContacts = false) + public override void Move(Vector2 amount, bool ignoreContacts = true) { if (!MathUtils.IsValid(amount)) { @@ -1424,7 +1453,7 @@ namespace Barotrauma return; } - base.Move(amount); + base.Move(amount, ignoreContacts); if (ItemList != null && body != null) { @@ -1508,17 +1537,51 @@ namespace Barotrauma return CurrentHull; } - public Item GetRootContainer() + private void RefreshRootContainer() { - if (Container == null) { return null; } - Item rootContainer = Container; - while (rootContainer.Container != null) + Item newRootContainer = null; + inWaterProofContainer = false; + if (Container != null) { - rootContainer = rootContainer.Container; + Item rootContainer = Container; + inWaterProofContainer |= Container.WaterProof; + + while (rootContainer.Container != null) + { + rootContainer = rootContainer.Container; + inWaterProofContainer |= rootContainer.WaterProof; + } + newRootContainer = rootContainer; + } + if (newRootContainer != RootContainer) + { + RootContainer = newRootContainer; + isActive = true; + foreach (Item containedItem in ContainedItems) + { + containedItem.RefreshRootContainer(); + } } - return rootContainer; } - + + private void RefreshInWaterProofContainer() + { + inWaterProofContainer = false; + if (container == null) { return; } + if (container.WaterProof || container.inWaterProofContainer) + { + inWaterProofContainer = true; + } + foreach (Item containedItem in ContainedItems) + { + containedItem.RefreshInWaterProofContainer(); + } + } + + /// + /// Used by the AI to check whether they can (in principle) and are allowed (in practice) to interact with an object or not. + /// Unlike CanInteractWith(), this method doesn't check the distance, the triggers, or anything like that. + /// public bool HasAccess(Character character) { if (character.IsBot && IgnoreByAI(character)) { return false; } @@ -1526,6 +1589,7 @@ namespace Barotrauma var itemContainer = GetComponent(); if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; } if (Container != null && !Container.HasAccess(character)) { return false; } + if (GetComponent() is { CanBePicked: false }) { return false; } return true; } @@ -1535,9 +1599,8 @@ namespace Barotrauma { if (ParentInventory == null) { return this; } if (ParentInventory.Owner is Character) { return ParentInventory.Owner; } - var rootContainer = GetRootContainer(); - if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; } - return rootContainer ?? this; + if (RootContainer?.ParentInventory?.Owner is Character) { return RootContainer.ParentInventory.Owner; } + return RootContainer ?? this; } public Inventory FindParentInventory(Func predicate) @@ -1781,8 +1844,23 @@ namespace Barotrauma bool wasInFullCondition = IsFullCondition; + float diff = value - condition; + if (GetComponent() is Door door && door.IsStuck && diff < 0) + { + float dmg = -diff; + // When the door is fully welded shut, reduce the welded state instead of the condition. + float prevStuck = door.Stuck; + door.Stuck -= dmg; + if (door.IsStuck) { return; } + // Reduce the damage by the amount we just adjusted the welded state by. + float damageReduction = dmg - prevStuck; + if (damageReduction < 0) { return; } + value -= damageReduction; + } + condition = MathHelper.Clamp(value, 0.0f, MaxCondition); - if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; } + + if (MathUtils.NearlyEqual(prevCondition, value, epsilon: 0.000001f)) { return; } RecalculateConditionValues(); @@ -2005,7 +2083,7 @@ namespace Barotrauma if (Removed) { return; } - bool needsWaterCheck = hasWaterStatusEffects; + bool needsWaterCheck = hasInWaterStatusEffects || hasNotInWaterStatusEffects; if (body != null && body.Enabled) { System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null); @@ -2034,7 +2112,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = IsInWater() && !WaterProof; + inWater = !inWaterProofContainer && IsInWater() && !WaterProof; if (inWater) { //the item has gone through the surface of the water @@ -2047,36 +2125,29 @@ namespace Barotrauma body.LinearVelocity *= 0.2f; } } - - Item container = this.Container; - while (container != null) - { - if (container.WaterProof) - { - inWater = false; - break; - } - container = container.Container; - } } - if (hasWaterStatusEffects && condition > 0.0f) + if ((hasInWaterStatusEffects || hasNotInWaterStatusEffects) && condition > 0.0f) { ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); } - } - else - { - if (updateableComponents.Count == 0 && - (aiTarget == null || !aiTarget.NeedsUpdate) && - !hasStatusEffectsOfType[(int)ActionType.Always] && - (body == null || !body.Enabled)) + if (inWaterProofContainer && !hasNotInWaterStatusEffects) { -#if CLIENT - positionBuffer.Clear(); -#endif - isActive = false; + needsWaterCheck = false; } } + + if (!needsWaterCheck && + updateableComponents.Count == 0 && + (aiTarget == null || !aiTarget.NeedsUpdate) && + !hasStatusEffectsOfType[(int)ActionType.Always] && + (body == null || !body.Enabled)) + { +#if CLIENT + positionBuffer.Clear(); +#endif + isActive = false; + } + } partial void Splash(); @@ -2865,7 +2936,7 @@ namespace Barotrauma if (user != null) { - var abilityItem = new AbilityApplyTreatment(user, character, this); + var abilityItem = new AbilityApplyTreatment(user, character, this, targetLimb); user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem); } @@ -2926,7 +2997,7 @@ namespace Barotrauma } } - foreach (ItemComponent ic in components) { ic.Drop(dropper); } + foreach (ItemComponent ic in components) { ic.Drop(dropper, setTransform); } if (Container != null) { @@ -3561,7 +3632,7 @@ namespace Barotrauma element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture))); } - Item rootContainer = GetRootContainer() ?? this; + Item rootContainer = RootContainer ?? this; System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character); Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition; @@ -3738,6 +3809,7 @@ namespace Barotrauma ItemList.Remove(this); dangerousItems.Remove(this); repairableItems.Remove(this); + sonarVisibleItems.Remove(this); cleanableItems.Remove(this); } @@ -3761,12 +3833,14 @@ namespace Barotrauma public Character Character { get; set; } public Character User { get; set; } public Item Item { get; set; } + public Limb TargetLimb { get; set; } - public AbilityApplyTreatment(Character user, Character target, Item item) + public AbilityApplyTreatment(Character user, Character target, Item item, Limb limb) { Character = target; User = user; Item = item; + TargetLimb = limb; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index b6ceab82c..af2e67a85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -88,12 +88,15 @@ namespace Barotrauma public abstract ItemPrefab FirstMatchingPrefab { get; } - public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition) + public LocalizedString OverrideDescription { get; } + + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) { Amount = amount; MinCondition = minCondition; MaxCondition = maxCondition; UseCondition = useCondition; + OverrideDescription = overrideDescription; } public readonly int Amount; public readonly float MinCondition; @@ -129,12 +132,14 @@ namespace Barotrauma public override ItemPrefab FirstMatchingPrefab => ItemPrefab; + public override bool MatchesItem(Item item) { return item?.Prefab.Identifier == ItemPrefabIdentifier; } - public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) : + base(amount, minCondition, maxCondition, useCondition, overrideDescription) { ItemPrefabIdentifier = itemPrefab; using MD5 md5 = MD5.Create(); @@ -163,7 +168,8 @@ namespace Barotrauma return item.HasTag(Tag); } - public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) + : base(amount, minCondition, maxCondition, useCondition, overrideDescription) { Tag = tag; using MD5 md5 = MD5.Create(); @@ -260,6 +266,12 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); + LocalizedString description = string.Empty; + if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty()) + { + description = TextManager.Get(texTag); + } + if (requiredItemIdentifier != Identifier.Empty) { var existing = requiredItems.FindIndex(r => @@ -272,7 +284,7 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition)); + requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description)); } else { @@ -286,7 +298,7 @@ namespace Barotrauma amount += requiredItems[existing].Amount; requiredItems.RemoveAt(existing); } - requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition)); + requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description)); } break; } @@ -669,8 +681,19 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.No)] public float OffsetOnSelected { get; private set; } + private float health; + [Serialize(100.0f, IsPropertySaveable.No)] - public float Health { get; private set; } + public float Health + { + get { return health; } + private set + { + //don't allow health values higher than this, because they lead to various issues: + //e.g. integer overflows when we're casting to int to display a health value, value being set to float.Infinity if it's high enough + health = Math.Min(value, 1000000.0f); + } + } [Serialize(false, IsPropertySaveable.No)] public bool AllowSellingWhenBroken { get; private set; } @@ -702,12 +725,6 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DamagedByMonsters { get; private set; } - [Serialize(false, IsPropertySaveable.No)] - public bool FireProof { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool WaterProof { get; private set; } - private float impactTolerance; [Serialize(0.0f, IsPropertySaveable.No)] public float ImpactTolerance @@ -813,10 +830,13 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] public float AISlowTurretPriority { get; private set; } - + [Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")] public float AITurretTargetingMaxDistance { get; private set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")] + public bool AllowStealingContainedItems { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1437,6 +1457,22 @@ namespace Barotrauma "Specify the amount in the variant to fix this."); } } + if (originalElement?.Name.ToIdentifier() == "Deconstruct" && + variantElement?.Name.ToIdentifier() == "Deconstruct") + { + if (originalElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") && + variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly."); + } + if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") && + originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) + { + DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " + + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly."); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 30c88bb93..c4647ef25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -35,7 +35,11 @@ namespace Barotrauma /// The item this relation is defined in must be inside a specific kind of container. /// Can for example by used to make an item do something when it's inside some other type of item. /// - Container + Container, + /// + /// Signifies an error (type could not be parsed) + /// + Invalid } /// @@ -60,9 +64,9 @@ namespace Barotrauma /// public ImmutableHashSet ExcludedIdentifiers { get; private set; } - private RelationType type; + private readonly RelationType type; - public List statusEffects; + public List StatusEffects = new List(); /// /// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). @@ -198,8 +202,121 @@ namespace Barotrauma { this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + } + + public RelatedItem(ContentXElement element, string parentDebugName) + { + Identifier[] identifiers; + if (element.GetAttribute("name") != null) + { + //backwards compatibility + a console warning + DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); + Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); + //attempt to convert to identifiers and tags + List convertedIdentifiers = new List(); + foreach (Identifier itemName in itemNames) + { + var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); + if (matchingItem != null) + { + convertedIdentifiers.Add(matchingItem.Identifier); + } + else + { + //no matching item found, this must be a tag + convertedIdentifiers.Add(itemName); + } + } + identifiers = convertedIdentifiers.ToArray(); + } + else + { + identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); + } + } + } + this.Identifiers = identifiers.ToImmutableHashSet(); + + Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); + } + } + this.ExcludedIdentifiers = excludedIdentifiers.ToImmutableHashSet(); + + ExcludeBroken = element.GetAttributeBool("excludebroken", true); + RequireEmpty = element.GetAttributeBool("requireempty", false); + ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false); + AllowVariants = element.GetAttributeBool("allowvariants", true); + Rotation = element.GetAttributeFloat("rotation", 0f); + SetActive = element.GetAttributeBool("setactive", false); + + if (element.GetAttribute(nameof(Hide)) != null) + { + Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } + string typeStr = element.GetAttributeString("type", ""); + if (string.IsNullOrEmpty(typeStr)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "containable": + typeStr = "Contained"; + break; + case "suitablefertilizer": + case "suitableseed": + typeStr = "None"; + break; + } + } + if (!Enum.TryParse(typeStr, true, out type)) + { + DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); + type = RelationType.Invalid; + } + + MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); + LocalizedString msg = TextManager.Get(MsgTag); + if (!msg.Loaded) + { + Msg = MsgTag.Value; + } + else + { +#if CLIENT + foreach (InputType inputType in Enum.GetValues(typeof(InputType))) + { + msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); + } + Msg = msg; +#endif + } + + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } + StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + } + + IsOptional = element.GetAttributeBool("optional", false); + IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); + MatchOnEmpty = element.GetAttributeBool("matchonempty", false); + TargetSlot = element.GetAttributeInt("targetslot", -1); - statusEffects = new List(); } public bool CheckRequirements(Character character, Item parentItem) @@ -301,120 +418,10 @@ namespace Barotrauma } public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) - { - Identifier[] identifiers; - if (element.GetAttribute("name") != null) - { - //backwards compatibility + a console warning - DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); - Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); - //attempt to convert to identifiers and tags - List convertedIdentifiers = new List(); - foreach (Identifier itemName in itemNames) - { - var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); - if (matchingItem != null) - { - convertedIdentifiers.Add(matchingItem.Identifier); - } - else - { - //no matching item found, this must be a tag - convertedIdentifiers.Add(itemName); - } - } - identifiers = convertedIdentifiers.ToArray(); - } - else - { - identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); - } - } - } - - Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); - } - } - - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } - - RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) - { - ExcludeBroken = element.GetAttributeBool("excludebroken", true), - RequireEmpty = element.GetAttributeBool("requireempty", false), - ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true), - Rotation = element.GetAttributeFloat("rotation", 0f), - SetActive = element.GetAttributeBool("setactive", false) - }; - if (element.GetAttribute(nameof(Hide)) != null) - { - ri.Hide = element.GetAttributeBool(nameof(Hide), false); - } - if (element.GetAttribute(nameof(ItemPos)) != null) - { - ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); - } - string typeStr = element.GetAttributeString("type", ""); - if (string.IsNullOrEmpty(typeStr)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "containable": - typeStr = "Contained"; - break; - case "suitablefertilizer": - case "suitableseed": - typeStr = "None"; - break; - } - } - if (!Enum.TryParse(typeStr, true, out ri.type)) - { - DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); - return null; - } - - ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); - LocalizedString msg = TextManager.Get(ri.MsgTag); - if (!msg.Loaded) - { - ri.Msg = ri.MsgTag.Value; - } - else - { -#if CLIENT - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); - } - ri.Msg = msg; -#endif - } - - foreach (var subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } - ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); - } - - ri.IsOptional = element.GetAttributeBool("optional", false); - ri.IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); - ri.MatchOnEmpty = element.GetAttributeBool("matchonempty", false); - ri.TargetSlot = element.GetAttributeInt("targetslot", -1); - + { + RelatedItem ri = new RelatedItem(element, parentDebugName); + if (ri.Type == RelationType.Invalid) { return null; } + if (ri.Identifiers.None() && ri.ExcludedIdentifiers.None() && !returnEmpty) { return null; } return ri; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index e7bd78a35..91797b6ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -326,7 +326,9 @@ namespace Barotrauma var lightComponent = item.GetComponent(); if (lightComponent != null) { - lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f); + //multiply by 10 to make the effect more noticeable + //(a strength of 1 is already enough to kill power and shut down the lights, but we want weaker EMPs to make the lights flicker noticeably) + lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor * 10.0f, 10.0f); } //discharge batteries diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index d00151206..6d9577b89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -254,7 +254,7 @@ namespace Barotrauma d.ForceRefreshFadeTimer(Math.Min(d.FadeTimer, d.FadeInTime)); } - UpdateProjSpecific(growModifier); + UpdateProjSpecific(growModifier, deltaTime); if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) @@ -273,7 +273,7 @@ namespace Barotrauma position.X -= GrowSpeed * growModifier * 0.5f * deltaTime; } - partial void UpdateProjSpecific(float growModifier); + partial void UpdateProjSpecific(float growModifier, float deltaTime); private void OnChangeHull(Vector2 pos, Hull particleHull) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index ddd99a9d0..77b7a1e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -32,6 +32,8 @@ namespace Barotrauma /// public bool IsDiagonal { get; } + public readonly float GlowEffectT; + //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; @@ -193,6 +195,8 @@ namespace Barotrauma GapList.Add(this); InsertToList(); + GlowEffectT = Rand.Range(0.0f, 1.0f); + float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, BodyType.Static, @@ -215,7 +219,7 @@ namespace Barotrauma return new Gap(rect, IsHorizontal, Submarine); } - public override void Move(Vector2 amount, bool ignoreContacts = false) + public override void Move(Vector2 amount, bool ignoreContacts = true) { if (!MathUtils.IsValid(amount)) { @@ -223,7 +227,7 @@ namespace Barotrauma return; } - base.Move(amount); + base.Move(amount, ignoreContacts); if (!DisableHullRechecks) { FindHulls(); } } @@ -336,8 +340,28 @@ namespace Barotrauma } } + private int updateCount; + public override void Update(float deltaTime, Camera cam) { + int updateInterval = 4; + float flowMagnitude = flowForce.LengthSquared(); + if (flowMagnitude < 1.0f) + { + //very sparse updates if there's practically no water moving + updateInterval = 8; + } + else if (linkedTo.Count == 2 && flowMagnitude > 10.0f) + { + //frequent updates if water is moving between hulls + updateInterval = 1; + } + + updateCount++; + if (updateCount < updateInterval) { return; } + deltaTime *= updateCount; + updateCount = 0; + flowForce = Vector2.Zero; outsideColliderRaycastTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index c604cc10d..5d98d6a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -590,7 +590,7 @@ namespace Barotrauma return index; } - public override void Move(Vector2 amount, bool ignoreContacts = false) + public override void Move(Vector2 amount, bool ignoreContacts = true) { if (!MathUtils.IsValid(amount)) { @@ -851,7 +851,21 @@ namespace Barotrauma { decal.Update(deltaTime); } - decals.RemoveAll(d => d.FadeTimer >= d.LifeTime || d.BaseAlpha <= 0.001f); + //clients don't remove decals unless the server says so + if (GameMain.NetworkMember is not { IsClient: true }) + { + for (int i = decals.Count - 1; i >= 0; i--) + { + var decal = decals[i]; + if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= 0.001f) + { + decals.RemoveAt(i); + #if SERVER + decalUpdatePending = true; + #endif + } + } + } if (aiTarget != null) { @@ -1509,9 +1523,8 @@ namespace Barotrauma public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired) { bool decalsCleaned = false; - for (int i = 0; i < decals.Count; i++) + foreach (Decal decal in decals) { - Decal decal = decals[i]; if (decal.AffectsSection(section)) { decal.Clean(cleanVal); @@ -1672,5 +1685,9 @@ namespace Barotrauma return element; } + public override string ToString() + { + return $"{base.ToString()} ({Name ?? "unnamed"})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index f37d520d0..706996a5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -136,7 +136,7 @@ namespace Barotrauma { me.Move(position); me.Submarine = sub; - if (!(me is Item item)) { continue; } + if (me is not Item item) { continue; } Wire wire = item.GetComponent(); //Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition; if (wire != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index d4c14c59e..313b832ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -296,9 +296,9 @@ namespace Barotrauma Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3; foreach (GraphEdge nearbyEdge in nearbyCell.Edges) { - if (!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) && - !MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) && - !MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2)) + if (!MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) && + !MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) && + !MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2)) { isInside = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 7427002f8..720f13874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1454,7 +1454,7 @@ namespace Barotrauma if (node2.X <= pathNodes.Last().X) { continue; } if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) { continue; } if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth && - !MathUtils.LinesIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2())) + !MathUtils.LineSegmentsIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2())) { continue; } @@ -1550,7 +1550,7 @@ namespace Barotrauma foreach (GraphEdge edge in tunnel.Cells[i].Edges) { if (edge.AdjacentCell(tunnel.Cells[i])?.CellType == CellType.Solid && - MathUtils.LinesIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2)) + MathUtils.LineSegmentsIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2)) { solidCellBetween = true; break; @@ -2801,7 +2801,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) { return true; } // If there is a line from a previous path point to one of its existing cluster locations // which intersects with the line from this path point to the new possible cluster location - if (MathUtils.LinesIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; } + if (MathUtils.LineSegmentsIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; } return false; } } @@ -2975,13 +2975,13 @@ namespace Barotrauma } /// Used by clients to set the rotation for the resources - public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable targetCaves = null) + public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable targetCaves = null) { var allValidLocations = GetAllValidClusterLocations(); var placedResources = new List(); - rotation = 0.0f; - if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?! + // if there are no valid locations, don't place anything + if (allValidLocations.None()) { return placedResources; } // Make sure not to pick a spot that already has other level resources for (int i = allValidLocations.Count - 1; i >= 0; i--) @@ -3077,7 +3077,6 @@ namespace Barotrauma } PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); - rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); return placedResources; static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath; @@ -3182,13 +3181,11 @@ namespace Barotrauma Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient); - item.Move(edgeNormal * moveAmount, ignoreContacts: true); + item.Move(edgeNormal * moveAmount); if (item.GetComponent() is Holdable h) { h.AttachToWall(); -#if CLIENT item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); -#endif } else if (item.body != null) { @@ -3509,7 +3506,7 @@ namespace Barotrauma { foreach (GraphEdge e in cell.Edges) { - if (!MathUtils.LinesIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; } + if (!MathUtils.LineSegmentsIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; } cell.CellType = CellType.Removed; for (int x = 0; x < cellGrid.GetLength(0); x++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index e361bd38b..b3c614074 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -294,40 +294,37 @@ namespace Barotrauma throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}"); } - foreach (Location location in Locations) - { - if (location.Type.Identifier != "outpost") { continue; } - SetStartLocation(location); - } + FindStartLocation(l => l.Type.Identifier == "outpost"); //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) + { + FindStartLocation(l => l.Type.HasOutpost); + } + + void FindStartLocation(Func predicate) { foreach (Location location in Locations) { - if (!location.Type.HasOutpost) { continue; } - SetStartLocation(location); + if (!predicate(location)) { continue; } + if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + { + CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + } } } - - void SetStartLocation(Location location) + + StartLocation.SecondaryFaction = null; + var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); + if (startOutpostFaction != null) { - if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + StartLocation.Faction = startOutpostFaction; + foreach (var connection in StartLocation.Connections) { - CurrentLocation = StartLocation = furthestDiscoveredLocation = location; - StartLocation.SecondaryFaction = null; - var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); - if (startOutpostFaction != null) + var otherLocation = connection.OtherLocation(StartLocation); + if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - StartLocation.Faction = startOutpostFaction; - foreach (var connection in StartLocation.Connections) - { - var otherLocation = connection.OtherLocation(StartLocation); - if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) - { - otherLocation.Faction = startOutpostFaction; - } - } - } + otherLocation.Faction = startOutpostFaction; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 93268f505..09fcedaf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -19,9 +19,6 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; - private const int GapUpdateInterval = 4; - private static int gapUpdateTimer; - /// /// List of upgrades this item has /// @@ -315,7 +312,7 @@ namespace Barotrauma } } - public virtual void Move(Vector2 amount, bool ignoreContacts = false) + public virtual void Move(Vector2 amount, bool ignoreContacts = true) { rect.X += (int)amount.X; rect.Y += (int)amount.Y; @@ -452,7 +449,7 @@ namespace Barotrauma List orphanedWires = new List(); for (int i = 0; i < clones.Count; i++) { - if (!(clones[i] is Item cloneItem)) { continue; } + if (clones[i] is not Item cloneItem) { continue; } var door = cloneItem.GetComponent(); door?.RefreshLinkedGap(); @@ -507,10 +504,12 @@ namespace Barotrauma } (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); - cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false); + cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false); } - if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) + if (originalWire.Connections.Any(c => c != null) && + (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && + cloneItem.GetComponent() == null) { if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) { @@ -628,14 +627,9 @@ namespace Barotrauma //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through //one gap in a room even if there are several - gapUpdateTimer++; - if (gapUpdateTimer >= GapUpdateInterval) + foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) { - foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) - { - gap.Update(deltaTime * GapUpdateInterval, cam); - } - gapUpdateTimer = 0; + gap.Update(deltaTime, cam); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index c7d5968ed..5f410a025 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -786,7 +786,7 @@ namespace Barotrauma //check if the connection overlaps with this module's connection if (selfGapPos1.HasValue && selfGapPos2.HasValue && !gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) && - MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value)) + MathUtils.LineSegmentsIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value)) { return true; } @@ -1105,8 +1105,8 @@ namespace Barotrauma DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")"); continue; } - wire.Connect(thisJunctionBox.Connections[i], addNode: false); - wire.Connect(previousJunctionBox.Connections[i], addNode: false); + wire.TryConnect(thisJunctionBox.Connections[i], addNode: false); + wire.TryConnect(previousJunctionBox.Connections[i], addNode: false); wire.SetNodes(new List()); } } @@ -1374,11 +1374,6 @@ namespace Barotrauma endWaypoint.linkedTo.Add(prevWayPoint); } } - else - { - startWaypoint.linkedTo.Add(endWaypoint); - endWaypoint.linkedTo.Add(startWaypoint); - } WayPoint closestWaypoint = null; float closestDistSqr = 30.0f * 30.0f; @@ -1595,10 +1590,11 @@ namespace Barotrauma { var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == bottomGap); var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == topGap); + float margin = 100; if (startWaypoint != null && endWaypoint != null) { WayPoint prevWaypoint = startWaypoint; - for (float y = startWaypoint.Position.Y + WayPoint.LadderWaypointInterval; y <= endWaypoint.Position.Y - WayPoint.LadderWaypointInterval; y += WayPoint.LadderWaypointInterval) + for (float y = bottomGap.Position.Y + margin; y <= topGap.Position.Y - margin; y += WayPoint.LadderWaypointInterval) { var wayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, ladder.Item.Submarine) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index d77a65b7b..4c6d9ecf7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -59,7 +59,7 @@ namespace Barotrauma private static Explosion explosionOnBroken; #if DEBUG - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)] #else [Serialize(false, IsPropertySaveable.Yes)] #endif @@ -104,9 +104,11 @@ namespace Barotrauma public List Bodies { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)] public bool CastShadow { - get { return Prefab.CastShadow; } + get; + set; } public bool IsHorizontal { get; private set; } @@ -118,7 +120,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0)] + [Serialize(100.0f, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody, MinValueFloat = 0)] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -189,14 +191,14 @@ namespace Barotrauma set { spriteColor = value; } } - [Editable, Serialize(false, IsPropertySaveable.Yes)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody), Serialize(false, IsPropertySaveable.Yes)] public bool UseDropShadow { get; private set; } - [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] + [ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody), Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] public Vector2 DropShadowOffset { get; @@ -367,7 +369,7 @@ namespace Barotrauma private set; } - public override void Move(Vector2 amount, bool ignoreContacts = false) + public override void Move(Vector2 amount, bool ignoreContacts = true) { if (!MathUtils.IsValid(amount)) { @@ -375,7 +377,7 @@ namespace Barotrauma return; } - base.Move(amount); + base.Move(amount, ignoreContacts); for (int i = 0; i < Sections.Length; i++) { @@ -440,13 +442,11 @@ namespace Barotrauma } else { + float width = BodyWidth > 0.0f ? BodyWidth : rect.Width; + float height = BodyHeight > 0.0f ? BodyHeight : rect.Height; if (BodyWidth > 0.0f && BodyHeight > 0.0f) { - IsHorizontal = BodyWidth > BodyHeight; - } - else - { - IsHorizontal = (rect.Width > rect.Height); + IsHorizontal = width > height; } } @@ -455,29 +455,28 @@ namespace Barotrauma InitProjSpecific(); - if (!HiddenInGame) + SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); + if (element?.GetAttribute(nameof(CastShadow)) == null) { - if (Prefab.Body) - { - Bodies = new List(); - WallList.Add(this); - - CreateSections(); - UpdateSections(); - } - else - { - Sections = new WallSection[1]; - Sections[0] = new WallSection(rect, this); - - if (StairDirection != Direction.None) - { - CreateStairBodies(); - } - } + CastShadow = Prefab.CastShadow; } - SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); + if (Prefab.Body) + { + Bodies = new List(); + WallList.Add(this); + CreateSections(); + UpdateSections(); + } + else if (StairDirection != Direction.None) + { + CreateStairBodies(); + } + if (Sections == null) + { + Sections = new WallSection[1]; + Sections[0] = new WallSection(rect, this); + } #if CLIENT foreach (var subElement in sp.ConfigElement.Elements()) @@ -1546,16 +1545,16 @@ namespace Barotrauma } } - if (element.GetAttributeBool("flippedx", false)) { s.FlipX(false); } - if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); } + if (element.GetAttributeBool(nameof(FlippedX), false)) { s.FlipX(false); } + if (element.GetAttributeBool(nameof(FlippedY), false)) { s.FlipY(false); } //structures with a body drop a shadow by default - if (element.GetAttribute("usedropshadow") == null) + if (element.GetAttribute(nameof(UseDropShadow)) == null) { s.UseDropShadow = prefab.Body; } - if (element.GetAttribute("noaitarget") == null) + if (element.GetAttribute(nameof(NoAITarget)) == null) { s.NoAITarget = prefab.NoAITarget; } @@ -1604,12 +1603,12 @@ namespace Barotrauma (int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," + width + "," + height)); - if (FlippedX) element.Add(new XAttribute("flippedx", true)); - if (FlippedY) element.Add(new XAttribute("flippedy", true)); + if (FlippedX) { element.Add(new XAttribute("flippedx", true)); } + if (FlippedY) { element.Add(new XAttribute("flippedy", true)); } for (int i = 0; i < Sections.Length; i++) { - if (Sections[i].damage == 0.0f) continue; + if (Sections[i].damage == 0.0f) { continue; } var sectionElement = new XElement("section", new XAttribute("i", i), @@ -1619,6 +1618,11 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); + if (CastShadow == Prefab.CastShadow) + { + element.GetAttribute(nameof(CastShadow))?.Remove(); + } + foreach (var upgrade in Upgrades) { upgrade.Save(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 0ec801d02..57d2d0424 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; using Voronoi2; @@ -427,17 +428,20 @@ namespace Barotrauma /// /// Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts /// - public Rectangle GetDockedBorders() + public Rectangle GetDockedBorders(bool allowDifferentTeam = true) { checkSubmarineBorders.Clear(); - return GetDockedBordersRecursive(); + return GetDockedBordersRecursive(allowDifferentTeam); } - private Rectangle GetDockedBordersRecursive() + private Rectangle GetDockedBordersRecursive(bool allowDifferentTeam) { Rectangle dockedBorders = Borders; checkSubmarineBorders.Add(this); - var connectedSubs = DockedTo.Where(s => !checkSubmarineBorders.Contains(s) && !s.Info.IsOutpost); + var connectedSubs = DockedTo.Where(s => + !checkSubmarineBorders.Contains(s) && + !s.Info.IsOutpost && + (allowDifferentTeam || s.TeamID == TeamID)); foreach (Submarine dockedSub in connectedSubs) { //use docking ports instead of world position to determine @@ -446,7 +450,7 @@ namespace Barotrauma Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive(); + Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive(allowDifferentTeam); dockedSubBorders.Location += MathUtils.ToPoint(expectedLocation.Value); dockedBorders.Y = -dockedBorders.Y; @@ -458,23 +462,23 @@ namespace Barotrauma return dockedBorders; } - /// - /// Don't use this directly, because the list is updated only when GetConnectedSubs() is called. The method is called so frequently that we don't want to create new list here. - /// - private readonly List connectedSubs = new List(2); + private readonly HashSet connectedSubs; /// /// Returns a list of all submarines that are connected to this one via docking ports, including this sub. /// - public List GetConnectedSubs() + public IEnumerable GetConnectedSubs() + { + return connectedSubs; + } + + public void RefreshConnectedSubs() { connectedSubs.Clear(); connectedSubs.Add(this); GetConnectedSubsRecursive(connectedSubs); - - return connectedSubs; } - private void GetConnectedSubsRecursive(List subs) + private void GetConnectedSubsRecursive(HashSet subs) { foreach (Submarine dockedSub in DockedTo) { @@ -1067,6 +1071,8 @@ namespace Barotrauma public void Update(float deltaTime) { + RefreshConnectedSubs(); + if (Info.IsWreck) { WreckAI?.Update(deltaTime); @@ -1393,6 +1399,13 @@ namespace Barotrauma public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { + Stopwatch sw = Stopwatch.StartNew(); + + connectedSubs = new HashSet(2) + { + this + }; + upgradeEventIdentifier = new Identifier($"Submarine{ID}"); Loading = true; GameMain.World.Enabled = false; @@ -1489,8 +1502,14 @@ namespace Barotrauma if (me.Submarine != this) { continue; } if (me is Item item) { - item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; - item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; + item.AllowStealing = true; + if (info.OutpostGenerationParams != null) + { + item.SpawnedInCurrentOutpost = true; + item.AllowStealing = + info.OutpostGenerationParams.AllowStealing || + item.RootContainer is { Prefab: { AllowStealingContainedItems: true } }; + } if (item.GetComponent() != null && indestructible) { item.Indestructible = true; @@ -1569,6 +1588,7 @@ namespace Barotrauma #if CLIENT GameMain.LightManager.OnMapLoaded(); + Lights.ConvexHull.RecalculateAll(this); #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula @@ -1596,6 +1616,10 @@ namespace Barotrauma Loading = false; GameMain.World.Enabled = true; } + sw.Stop(); + string debugMsg = $"Loading {Info?.Name ?? "unknown"} took {sw.ElapsedMilliseconds} ms."; + DebugConsole.Log(debugMsg); + System.Diagnostics.Debug.WriteLine(debugMsg); } protected override ushort DetermineID(ushort id, Submarine submarine) @@ -1745,8 +1769,7 @@ namespace Barotrauma } #endif if (e.Submarine != this) { continue; } - var rootContainer = item.GetRootContainer(); - if (rootContainer != null && rootContainer.Submarine != this) { continue; } + if (item.RootContainer != null && item.RootContainer.Submarine != this) { continue; } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 356ee4dcf..a83578cfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -777,12 +777,14 @@ namespace Barotrauma characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); float price = Price; - - if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + if (characterList.Any()) { - price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); } - price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); return (int)price; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 2bb863cef..12ea2fef8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -19,7 +19,7 @@ namespace Barotrauma public static bool ShowWayPoints = true, ShowSpawnPoints = true; - public const float LadderWaypointInterval = 55.0f; + public const float LadderWaypointInterval = 75.0f; protected SpawnType spawnType; private string[] idCardTags; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index a1ce193dc..4e56ebe95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -208,19 +208,6 @@ namespace Barotrauma.Networking public TimeSpan UpdateInterval => new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / ServerSettings.TickRate, 1, 500)); - - public bool CanUseRadio(Character sender) - { - if (sender == null) { return false; } - - var radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent() != null); - if (radio == null || !sender.HasEquippedItem(radio)) { return false; } - - var radioComponent = radio.GetComponent(); - if (radioComponent == null) { return false; } - return radioComponent.HasRequiredContainedItems(sender, addMessage: false); - } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null) { AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 6f5bf9235..91ebdc203 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Networking listBox.UpdateScrollBarSize(); } #endif - if (unsavedLines.Count() >= LinesPerFile) + if (unsavedLines.Count >= LinesPerFile) { Save(); unsavedLines.Clear(); @@ -143,7 +143,7 @@ namespace Barotrauma.Networking #if CLIENT while (listBox != null && listBox.Content.CountChildren > LinesPerFile) { - listBox.RemoveChild(reverseOrder ? listBox.Content.Children.First() : listBox.Content.Children.Last()); + listBox.Content.RemoveChild(!reverseOrder ? listBox.Content.Children.First() : listBox.Content.Children.Last()); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 7448dcd5e..88c0c9776 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -43,8 +43,8 @@ namespace Barotrauma.Networking partial class ServerSettings : ISerializableEntity { public const int PacketLimitMin = 1200, - PacketLimitWarning = 2400, - PacketLimitDefault = 3000, + PacketLimitWarning = 3500, + PacketLimitDefault = 4000, PacketLimitMax = 10000; public const string SettingsFile = "serversettings.xml"; @@ -392,8 +392,10 @@ namespace Barotrauma.Networking set; } - private int tickRate = 20; - [Serialize(20, IsPropertySaveable.Yes)] + public const int DefaultTickRate = 20; + + private int tickRate = DefaultTickRate; + [Serialize(DefaultTickRate, IsPropertySaveable.Yes)] public int TickRate { get { return tickRate; } @@ -522,7 +524,7 @@ namespace Barotrauma.Networking } } - [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Transparent, IsPropertySaveable.Yes)] public LosMode LosMode { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 557860b69..7a6e24432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -205,7 +205,7 @@ namespace Voronoi2 Vector2 transformedPoint = point - Translation; foreach (GraphEdge edge in Edges) { - if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } + if (MathUtils.LineSegmentsIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 5e5317022..64032091c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -67,13 +67,15 @@ namespace Barotrauma [AttributeUsage(AttributeTargets.Property)] class ConditionallyEditable : Editable { - public ConditionallyEditable(ConditionType conditionType) + public ConditionallyEditable(ConditionType conditionType, bool onlyInEditors = true) { this.conditionType = conditionType; + this.onlyInEditors = onlyInEditors; } - private readonly ConditionType conditionType; + private readonly bool onlyInEditors; + public enum ConditionType { //These need to exist at compile time, so it is a little awkward @@ -81,26 +83,37 @@ namespace Barotrauma AllowLinkingWifiToChat, IsSwappableItem, AllowRotating, - Attachable + Attachable, + HasBody, + Pickable } public bool IsEditable(ISerializableEntity entity) { + if (onlyInEditors && Screen.Selected is { IsEditor: false }) { return false; } switch (conditionType) { case ConditionType.AllowLinkingWifiToChat: return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; case ConditionType.IsSwappableItem: { - return entity is Item item && item.Prefab.SwappableItem != null && Screen.Selected == GameMain.SubEditorScreen; + return entity is Item item && item.Prefab.SwappableItem != null; } case ConditionType.AllowRotating: { - return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen; + return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor; } case ConditionType.Attachable: { - return entity is Holdable holdable && holdable.Attachable && Screen.Selected == GameMain.SubEditorScreen; + return entity is Holdable holdable && holdable.Attachable; + } + case ConditionType.HasBody: + { + return entity is Structure { HasBody: true } || entity is Item { body: not null }; + } + case ConditionType.Pickable: + { + return entity is Item item && item.GetComponent() != null; } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index fa9610d09..ebd748ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -213,6 +213,16 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, Identifier[] defaultValue, params string[] matchingAttributeName) + { + if (element == null) { return defaultValue; } + foreach (string name in matchingAttributeName) + { + var value = element.GetAttributeIdentifierArray(name, defaultValue); + if (value != defaultValue) { return value; } + } + return defaultValue; + } public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 06e291b5b..c88eb0204 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -64,6 +64,7 @@ namespace Barotrauma SubEditorBackground = new Color(13, 37, 69, 255), EnableSplashScreen = true, PauseOnFocusLost = true, + RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, EnableMouseLook = true, @@ -112,6 +113,11 @@ namespace Barotrauma retVal.Language = TextManager.DefaultLanguage; } #endif + //RemoteMainMenuContentUrl gets set to default it left empty - lets allow leaving it empty to make it possible to disable the remote content + if (element.Attribute("RemoteMainMenuContentUrl")?.Value == string.Empty) + { + retVal.RemoteMainMenuContentUrl = string.Empty; + } retVal.Graphics = GraphicsSettings.FromElements(element.GetChildElements("graphicsmode", "graphicssettings"), retVal.Graphics); retVal.Audio = AudioSettings.FromElements(element.GetChildElements("audio"), retVal.Audio); #if CLIENT @@ -147,6 +153,7 @@ namespace Barotrauma public bool DisableInGameHints; public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; + public string RemoteMainMenuContentUrl; #if CLIENT public XElement SavedCampaignSettings; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 5ccdfc56c..20831e9be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -42,129 +42,183 @@ namespace Barotrauma } } - class AITrigger : ISerializableEntity - { - public string Name => "ai trigger"; - - public Dictionary SerializableProperties { get; set; } - - [Serialize(AIState.Idle, IsPropertySaveable.No)] - public AIState State { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float Duration { get; private set; } - - [Serialize(1f, IsPropertySaveable.No)] - public float Probability { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float MinDamage { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToOverride { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToBeOverridden { get; private set; } - - public bool IsTriggered { get; private set; } - - public float Timer { get; private set; } - - public bool IsActive { get; private set; } - - public bool IsPermanent { get; private set; } - - public void Launch() - { - IsTriggered = true; - IsActive = true; - IsPermanent = Duration <= 0; - if (!IsPermanent) - { - Timer = Duration; - } - } - - public void Reset() - { - IsTriggered = false; - IsActive = false; - Timer = 0; - } - - public void UpdateTimer(float deltaTime) - { - if (IsPermanent) { return; } - Timer -= deltaTime; - if (Timer < 0) - { - Timer = 0; - IsActive = false; - } - } - - public AITrigger(XElement element) - { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - } - } - + /// + /// StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in some way, spawning things, playing sounds, + /// emitting particles, creating fire and explosions, increasing a characters' skill. They are a crucial part of modding Barotrauma: all kinds of + /// custom behaviors of an item or a creature for example are generally created using StatusEffects. + /// + /// + /// + /// Can be used to delay the execution of the effect. For example, you could have an effect that triggers when a character receives damage, + /// but takes 5 seconds before it starts to do anything. + /// + /// + /// An arbitrary tag (or a list of tags) that describe the status effect and can be used by Conditionals to check whether some StatusEffect is running. + /// For example, an item could execute a StatusEffect with the tag "poisoned" on some character, and the character could have an effect that makes + /// the character do something when an effect with that tag is active. + /// + /// + /// And/Or. Do all of the Conditionals defined in the effect be true for the effect to execute, or should the effect execute when any of them is true? + /// + /// + /// These are the meat of the StatusEffects. You can set, increment or decrement any value of the target, be it an item, character, limb or hull. + /// By default, the value is added to the existing value. If you want to instead set the value, use the setValue attribute. + /// For example, Condition="-5" would decrease the condition of the item the effect is targeting by 5 per second. If the target has no property + /// with the specified name, the attribute does nothing. + /// + /// partial class StatusEffect { + private static readonly ImmutableHashSet FieldNames; + static StatusEffect() + { + FieldNames = typeof(StatusEffect).GetFields().AsEnumerable().Select(f => f.Name.ToIdentifier()).ToImmutableHashSet(); + } + [Flags] public enum TargetType { + /// + /// The entity (item, character, limb) the StatusEffect is defined in. + /// This = 1, + /// + /// In the context of items, the container the item is inside (if any). In the context of limbs, the character the limb belongs to. + /// Parent = 2, + /// + /// The character the StatusEffect is defined in. In the context of items and attacks, the character using the item/attack. + /// Character = 4, + /// + /// The item(s) contained in the inventory of the entity the StatusEffect is defined in. + /// Contained = 8, + /// + /// Characters near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyCharacters = 16, + /// + /// Items near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyItems = 32, + /// + /// The entity the item/attack is being used on. + /// UseTarget = 64, + /// + /// The hull the entity is inside. + /// Hull = 128, + /// + /// The entity the item/attack is being used on. In the context of characters, one of the character's limbs (specify which one using ). + /// Limb = 256, + /// + /// All limbs of the character the effect is being used on. + /// AllLimbs = 512, + /// + /// Last limb of the character the effect is being used on. + /// LastLimb = 1024 } + /// + /// Defines items spawned by the effect, and where and how they're spawned. + /// class ItemSpawnInfo { public enum SpawnPositionType { + /// + /// The position of the StatusEffect's target. + /// This, - //the inventory of the StatusEffect's target entity + /// + /// The inventory of the StatusEffect's target. + /// ThisInventory, - //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + /// + /// The same inventory the StatusEffect's target entity is in. Only valid if the target is an Item. + /// SameInventory, - //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// + /// The inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// ContainedInventory } public enum SpawnRotationType { + /// + /// Fixed rotation specified using the Rotation attribute. + /// Fixed, + /// + /// The rotation of the entity executing the StatusEffect + /// Target, + /// + /// The rotation of the limb executing the StatusEffect, or the limb the StatusEffect is targeting + /// Limb, + /// + /// The rotation of the main limb (usually torso) of the character executing the StatusEffect + /// MainLimb, + /// + /// The rotation of the collider of the character executing the StatusEffect + /// Collider, + /// + /// Random rotation between 0 and 360 degrees. + /// Random } public readonly ItemPrefab ItemPrefab; + /// + /// Where should the item spawn? + /// public readonly SpawnPositionType SpawnPosition; + + /// + /// Should the item spawn even if the container is already full? + /// public readonly bool SpawnIfInventoryFull; /// - /// Should the item spawn even if the container can't contain items of this type + /// Should the item spawn even if the container can't contain items of this type or if it's already full? /// public readonly bool SpawnIfCantBeContained; + /// + /// Impulse applied to the item when it spawns (i.e. how fast the item launched off). + /// public readonly float Impulse; public readonly float RotationRad; + /// + /// How many items to spawn. + /// public readonly int Count; + /// + /// Random offset added to the spawn position in pixels. + /// public readonly float Spread; + /// + /// What should the initial rotation of the item be? + /// public readonly SpawnRotationType RotationType; + /// + /// Amount of random variance in the initial rotation of the item (in degrees). + /// public readonly float AimSpreadRad; + /// + /// Should the item be automatically equipped when it spawns? Only valid if the item spawns in a character's inventory. + /// public readonly bool Equip; - + /// + /// Condition of the item when it spawns (1.0 = max). + /// public readonly float Condition; public ItemSpawnInfo(XElement element, string parentDebugName) @@ -213,6 +267,14 @@ namespace Barotrauma } } + /// + /// Can be used by to check whether some specific StatusEffect is running. + /// + /// + /// + /// An arbitrary identifier the Ability can check for. + /// + /// public class AbilityStatusEffectIdentifier : AbilityObject { public AbilityStatusEffectIdentifier(Identifier effectIdentifier) @@ -222,9 +284,18 @@ namespace Barotrauma public Identifier EffectIdentifier { get; set; } } + /// + /// Unlocks a talent, or multiple talents when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveTalentInfo { + /// + /// The identifier(s) of the talents that should be unlocked. + /// public Identifier[] TalentIdentifiers; + /// + /// If true and there's multiple identifiers defined, a random one will be chosen instead of unlocking all of them. + /// public bool GiveRandom; public GiveTalentInfo(XElement element, string _) @@ -234,10 +305,22 @@ namespace Barotrauma } } + /// + /// Increases a character's skills when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveSkill { + /// + /// The identifier of the skill to increase. + /// public readonly Identifier SkillIdentifier; + /// + /// How much to increase the skill. + /// public readonly float Amount; + /// + /// Should the talents that trigger when the character gains skills be triggered by the effect? + /// public readonly bool TriggerTalents; public GiveSkill(XElement element, string parentDebugName) @@ -253,52 +336,66 @@ namespace Barotrauma } } + /// + /// Defines characters spawned by the effect, and where and how they're spawned. + /// public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } - [Serialize(false, IsPropertySaveable.No)] - public bool TransferBuffs { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferAfflictions { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferInventory { get; private set; } - - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "The species name (identifier) of the character to spawn.")] public Identifier SpeciesName { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")] public int Count { get; private set; } - /// - /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. - /// Also the creatures spawned by other means are counted in the check. - /// - [Serialize(0, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the buffs of the character executing the effect be transferred to the spawned character?"+ + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferBuffs { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the afflictions of the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferAfflictions { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the the items from the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferInventory { get; private set; } + + [Serialize(0, IsPropertySaveable.No, description: + "The maximum number of creatures of the given species and team that can exist in the current level before this status effect stops spawning any more.")] public int TotalMaxCount { get; private set; } - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "Amount of stun to apply on the spawned character.")] public int Stun { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")] public Identifier AfflictionOnSpawn { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: + $"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")] public int AfflictionStrength { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the player controlling the character that executes the effect gain control of the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool TransferControl { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the character that executes the effect be removed when the effect executes?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool RemovePreviousCharacter { get; private set; } - [Serialize(0f, IsPropertySaveable.No)] + [Serialize(0f, IsPropertySaveable.No, description: "Amount of random spread to add to the spawn position. " + + "Can be used to prevent all the characters from spawning at the exact same position if the effect spawns multiple ones.")] public float Spread { get; private set; } - [Serialize("0,0", IsPropertySaveable.No)] + [Serialize("0,0", IsPropertySaveable.No, description: + "Offset added to the spawn position. " + + "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")] public Vector2 Offset { get; private set; } public CharacterSpawnInfo(XElement element, string parentDebugName) @@ -311,45 +408,150 @@ namespace Barotrauma } } + /// + /// Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy characters, not humans. + /// + public class AITrigger : ISerializableEntity + { + public string Name => "ai trigger"; + + public Dictionary SerializableProperties { get; set; } + + [Serialize(AIState.Idle, IsPropertySaveable.No, description: "The AI state the character should switch to.")] + public AIState State { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: "How long should the character stay in the specified state? If 0, the effect is permanent (unless overridden by another AITrigger).")] + public float Duration { get; private set; } + + [Serialize(1f, IsPropertySaveable.No, description: "How likely is the AI to change the state when this effect executes? 1 = always, 0.5 = 50% chance, 0 = never.")] + public float Probability { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: + "How much damage the character must receive for this AITrigger to become active? " + + "Checks the amount of damage the latest attack did to the character.")] + public float MinDamage { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger override other active AITriggers?")] + public bool AllowToOverride { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger be overridden by other AITriggers?")] + public bool AllowToBeOverridden { get; private set; } + + public bool IsTriggered { get; private set; } + + public float Timer { get; private set; } + + public bool IsActive { get; private set; } + + public bool IsPermanent { get; private set; } + + public void Launch() + { + IsTriggered = true; + IsActive = true; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } + } + + public void Reset() + { + IsTriggered = false; + IsActive = false; + Timer = 0; + } + + public void UpdateTimer(float deltaTime) + { + if (IsPermanent) { return; } + Timer -= deltaTime; + if (Timer < 0) + { + Timer = 0; + IsActive = false; + } + } + + public AITrigger(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + + + /// + /// What should this status effect be applied on? + /// private readonly TargetType targetTypes; /// - /// Index of the slot the target must be in when targeting a Contained item + /// Index of the slot the target must be in. Only valid when targeting a Contained item. /// public int TargetSlot = -1; - private readonly List requiredItems; + private readonly List requiredItems = new List(); - public readonly Identifier[] propertyNames; - public readonly object[] propertyEffects; + public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); + /// + /// If set to true, the effect will set the properties of the target to the given values, instead of incrementing them by the given value. + /// private readonly bool setValue; + /// + /// If set to true, the values will not be multiplied by the elapsed time. + /// In other words, the values are treated as an increase per frame, as opposed to an increase per second. + /// Useful for effects that are intended to just run for one frame (e.g. firing a gun, an explosion). + /// private readonly bool disableDeltaTime; + /// + /// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration. + /// private readonly HashSet tags; + /// + /// How long the effect runs (in seconds). Note that if is true, + /// there can be multiple instances of the effect running at a time. + /// In other words, if the effect has a duration and executes every frame, you probably want + /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. + /// private readonly float duration; + + /// + /// How long _can_ the event run (in seconds). The difference to is that + /// lifetime doesn't force the effect to run for the given amount of time, only restricts how + /// long it can run in total. For example, you could have an effect that makes a projectile + /// emit particles for 1 second when it's active, and not do anything after that. + /// private readonly float lifeTime; private float lifeTimer; public Dictionary intervalTimers = new Dictionary(); + /// + /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). + /// private readonly bool oneShot; public static readonly List DurationList = new List(); /// - /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers, + /// or for the whole duration it executes / when the delay runs out and the effect executes? In other words, if false, the conditionals + /// are only checked once when the effect triggers, but after that it can keep running for the whole duration, or is + /// guaranteed to execute after the delay. /// public readonly bool CheckConditionalAlways; /// - /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s) if the effect is already being applied? /// public readonly bool Stackable = true; @@ -361,6 +563,9 @@ namespace Barotrauma public readonly float Interval; #if CLIENT + /// + /// Should the sound(s) configured in the effect be played if the required items aren't found? + /// private readonly bool playSoundOnRequiredItemFailure = false; #endif @@ -371,41 +576,79 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - public readonly List Explosions; + public readonly List Explosions = new List(); - private readonly List spawnItems; + private readonly List spawnItems = new List(); + + /// + /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. + /// private readonly bool spawnItemRandomly; - private readonly List spawnCharacters; + private readonly List spawnCharacters = new List(); - public readonly List giveTalentInfos; + public readonly List giveTalentInfos = new List(); - private readonly List aiTriggers; + private readonly List aiTriggers = new List(); - private readonly List triggeredEvents; - private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), - triggeredEventEntityTag = "statuseffectentity".ToIdentifier(), - triggeredEventUserTag = "statuseffectuser".ToIdentifier(); + private readonly List triggeredEvents = new List(); + + /// + /// If the effect triggers a scripted event, the target of this effect is added as a target for the event using the specified tag. + /// For example, an item could have an effect that executes when used on some character, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventTargetTag; + + /// + /// If the effect triggers a scripted event, the entity executing this effect is added as a target for the event using the specified tag. + /// For example, a character could have an effect that executes when the character takes damage, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventEntityTag; + + /// + /// If the effect triggers a scripted event, the user of the StatusEffect (= the character who caused it to happen, e.g. a character who used an item) is added as a target for the event using the specified tag. + /// For example, a gun could have an effect that executes when a character uses it, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventUserTag; private Character user; public readonly float FireSize; + /// + /// Which types of limbs this effect can target? Only valid when targeting characters or limbs. + /// public readonly LimbType[] targetLimbs; + /// + /// The probability of severing a limb damaged by this status effect. Only valid when targeting characters or limbs. + /// public readonly float SeverLimbsProbability; public PhysicsBody sourceBody; + /// + /// If enabled, this effect can only execute inside a hull. + /// public readonly bool OnlyInside; + /// + /// If enabled, this effect can only execute outside hulls. + /// public readonly bool OnlyOutside; - // Currently only used for OnDamaged. TODO: is there a better, more generic way to do this? - public readonly bool OnlyPlayerTriggered; /// - /// Can the StatusEffect be applied when the item applying it is broken + /// If enabled, the effect only executes when the entity receives damage from a player character + /// (a character controlled by a human player). Only valid for characters, and effects of the type . + /// + public readonly bool OnlyWhenDamagedByPlayer; + + /// + /// Can the StatusEffect be applied when the item applying it is broken? /// public readonly bool AllowWhenBroken = false; + /// + /// Identifier(s), tag(s) or species name(s) of the entity the effect can target. Null if there's no identifiers. + /// public readonly ImmutableHashSet TargetIdentifiers; /// @@ -419,8 +662,13 @@ namespace Barotrauma { get; private set; - } + } = new List(); + /// + /// Should the affliction strength be directly proportional to the maximum vitality of the character? + /// In other words, when enabled, the strength of the affliction(s) caused by this effect is higher on higher-vitality characters. + /// Can be used to make characters take the same relative amount of damage regardless of their maximum vitality. + /// private readonly bool? multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters @@ -428,21 +676,27 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction; + public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); - private readonly List talentTriggers; - private readonly List giveExperiences; - private readonly List giveSkills; + private readonly List talentTriggers = new List(); + private readonly List giveExperiences = new List(); + private readonly List giveSkills = new List(); public float Duration => duration; - //only applicable if targeting NearbyCharacters or NearbyItems + /// + /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. + /// public float Range { get; private set; } + /// + /// An offset added to the position of the effect is executed at. Only relevant if the effect does something where position matters, + /// for example emitting particles or explosions, spawning something or playing sounds. + /// public Vector2 Offset { get; private set; } public string Tags @@ -476,34 +730,21 @@ namespace Barotrauma protected StatusEffect(ContentXElement element, string parentDebugName) { - requiredItems = new List(); - spawnItems = new List(); - spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); - spawnCharacters = new List(); - giveTalentInfos = new List(); - aiTriggers = new List(); - Afflictions = new List(); - Explosions = new List(); - triggeredEvents = new List(); - ReduceAffliction = new List<(Identifier affliction, float amount)>(); - talentTriggers = new List(); - giveExperiences = new List(); - giveSkills = new List(); - var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); - if (multiplyAfflictionsElement != null) - { - multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); - } - tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); - OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); + OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false)); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); - TargetSlot = element.GetAttributeInt("targetslot", -1); - Interval = element.GetAttributeFloat("interval", 0.0f); + duration = element.GetAttributeFloat("duration", 0.0f); + disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); + setValue = element.GetAttributeBool("setvalue", false); + Stackable = element.GetAttributeBool("stackable", true); + lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f); + CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false); + + TargetSlot = element.GetAttributeInt("targetslot", -1); Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -518,9 +759,7 @@ namespace Barotrauma if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } - IEnumerable attributes = element.Attributes(); - List propertyAttributes = new List(); - propertyConditionals = new List(); + SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f); string[] targetTypesStr = element.GetAttributeStringArray("target", null) ?? @@ -529,7 +768,7 @@ namespace Barotrauma { if (!Enum.TryParse(s, true, out TargetType targetType)) { - DebugConsole.ThrowError("Invalid target type \"" + s + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})"); } else { @@ -537,37 +776,55 @@ namespace Barotrauma } } - foreach (XAttribute attribute in attributes) + var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty(), "targetnames", "targets", "targetidentifiers", "targettags"); + if (targetIdentifiers.Any()) + { + TargetIdentifiers = targetIdentifiers.ToImmutableHashSet(); + } + + triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", Identifier.Empty); + triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", Identifier.Empty); + triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", Identifier.Empty); + + spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); + + var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); + if (multiplyAfflictionsElement != null) + { + multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); + } + +#if CLIENT + playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false); +#endif + + List propertyAttributes = new List(); + propertyConditionals = new List(); + foreach (XAttribute attribute in element.Attributes()) { switch (attribute.Name.ToString().ToLowerInvariant()) { case "type": if (!Enum.TryParse(attribute.Value, true, out type)) { - DebugConsole.ThrowError("Invalid action type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; case "targettype": case "target": - break; - case "disabledeltatime": - disableDeltaTime = attribute.GetAttributeBool(false); - break; - case "setvalue": - setValue = attribute.GetAttributeBool(false); - break; - case "severlimbs": - case "severlimbsprobability": - SeverLimbsProbability = MathHelper.Clamp(attribute.GetAttributeFloat(0.0f), 0.0f, 1.0f); - break; case "targetnames": case "targets": case "targetidentifiers": case "targettags": - TargetIdentifiers = attribute.Value.Split(',').ToIdentifiers().ToImmutableHashSet(); + case "severlimbs": + case "targetlimb": + case "delay": + case "interval": + //aliases for fields we're already reading above, and which shouldn't be interpreted as values we're trying to set break; case "allowedafflictions": case "requiredafflictions": + //backwards compatibility, should be defined as child elements instead string[] types = attribute.Value.Split(','); requiredAfflictions ??= new HashSet<(Identifier, float)>(); for (int i = 0; i < types.Length; i++) @@ -575,43 +832,15 @@ namespace Barotrauma requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f)); } break; - case "duration": - duration = attribute.GetAttributeFloat(0.0f); - break; - case "stackable": - Stackable = attribute.GetAttributeBool(true); - break; - case "lifetime": - lifeTime = attribute.GetAttributeFloat(0); - lifeTimer = lifeTime; - break; - case "eventtargettag": - triggeredEventTargetTag = attribute.Value.ToIdentifier(); - break; - case "evententitytag": - triggeredEventEntityTag = attribute.Value.ToIdentifier(); - break; - case "checkconditionalalways": - CheckConditionalAlways = attribute.GetAttributeBool(false); - break; case "conditionalcomparison": case "comparison": if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) { - DebugConsole.ThrowError("Invalid conditional comparison type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; -#if CLIENT - case "playsoundonrequireditemfailure": - playSoundOnRequiredItemFailure = attribute.GetAttributeBool(false); - break; -#endif case "sound": - DebugConsole.ThrowError("Error in StatusEffect " + element.Parent.Name.ToString() + - " - sounds should be defined as child elements of the StatusEffect, not as attributes."); - break; - case "delay": - case "interval": + DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -619,35 +848,30 @@ namespace Barotrauma propertyAttributes.Add(attribute); } break; + case "tags": + if (duration <= 0.0f || setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags + propertyAttributes.Add(attribute); + } + break; case "oneshot": oneShot = attribute.GetAttributeBool(false); break; default: + if (FieldNames.Contains(attribute.Name.ToIdentifier())) { continue; } propertyAttributes.Add(attribute); break; } } - if (duration > 0.0f && !setValue) - { - //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: - //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. - propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); - } - - int count = propertyAttributes.Count; - - propertyNames = new Identifier[count]; - propertyEffects = new object[count]; - - int n = 0; + List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>(); foreach (XAttribute attribute in propertyAttributes) { - - propertyNames[n] = attribute.NameAsIdentifier(); - propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); - n++; + propertyEffects.Add((attribute.NameAsIdentifier(), XMLExtensions.GetAttributeObject(attribute))); } + PropertyEffects = propertyEffects.ToImmutableArray(); foreach (var subElement in element.Elements()) { @@ -817,13 +1041,11 @@ namespace Barotrauma public bool ReducesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect < 0.0f || (setValue && (float)propertyEffect <= 0.0f); + return (float)value < 0.0f || (setValue && (float)value <= 0.0f); } } return false; @@ -831,13 +1053,11 @@ namespace Barotrauma public bool IncreasesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect > 0.0f || (setValue && (float)propertyEffect > 0.0f); + return (float)value > 0.0f || (setValue && (float)value > 0.0f); } } return false; @@ -1408,7 +1628,7 @@ namespace Barotrauma for (int i = 0; i < targets.Count; i++) { var target = targets[i]; - if (target == null) { continue; } + if (target?.SerializableProperties == null) { continue; } if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -1418,14 +1638,13 @@ namespace Barotrauma if (limb.Removed) { continue; } position = limb.WorldPosition + Offset; } - - for (int j = 0; j < propertyNames.Length; j++) + foreach (var (propertyName, value) in PropertyEffects) { - if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[j], out SerializableProperty property)) + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } - ApplyToProperty(target, property, j, deltaTime); + ApplyToProperty(target, property, value, deltaTime); } } } @@ -2010,16 +2229,15 @@ namespace Barotrauma partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull currentHull, Vector2 worldPosition, bool playSound); - private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, int effectIndex, float deltaTime) + private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - object propertyEffect = propertyEffects[effectIndex]; - if (propertyEffect is int || propertyEffect is float) + if (value is int || value is float) { float propertyValueF = property.GetFloatValue(target); if (property.PropertyType == typeof(float)) { - float floatValue = propertyEffect is float single ? single : (int)propertyEffect; + float floatValue = value is float single ? single : (int)value; floatValue *= deltaTime; if (!setValue) { @@ -2030,7 +2248,7 @@ namespace Barotrauma } else if (property.PropertyType == typeof(int)) { - int intValue = (int)(propertyEffect is float single ? single * deltaTime : (int)propertyEffect * deltaTime); + int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime); if (!setValue) { intValue += (int)propertyValueF; @@ -2039,12 +2257,12 @@ namespace Barotrauma return; } } - else if (propertyEffect is bool propertyValueBool) + else if (value is bool propertyValueBool) { property.TrySetValue(target, propertyValueBool); return; } - property.TrySetValue(target, propertyEffect); + property.TrySetValue(target, value); } public static void UpdateAll(float deltaTime) @@ -2073,15 +2291,16 @@ namespace Barotrauma foreach (ISerializableEntity target in element.Targets) { - for (int n = 0; n < element.Parent.propertyNames.Length; n++) + if (target?.SerializableProperties != null) { - if (target == null || - target.SerializableProperties == null || - !target.SerializableProperties.TryGetValue(element.Parent.propertyNames[n], out SerializableProperty property)) + foreach (var (propertyName, value) in element.Parent.PropertyEffects) { - continue; + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) + { + continue; + } + element.Parent.ApplyToProperty(target, property, value, CoroutineManager.DeltaTime); } - element.Parent.ApplyToProperty(target, property, n, CoroutineManager.DeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 08658ad68..5de759e46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -175,12 +175,14 @@ namespace Barotrauma.Steam public static void Update(float deltaTime) { + //this should be run even if SteamManager is uninitialized + //servers need to be able to notify clients of unlocked talents even if the server isn't connected to Steam + SteamAchievementManager.Update(deltaTime); + if (!IsInitialized) { return; } if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.RunCallbacks(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.RunCallbacks(); } - - SteamAchievementManager.Update(deltaTime); } public static void ShutDown() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 5b6f6f15f..69e268be3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -490,7 +490,7 @@ namespace Barotrauma.Steam root.Attributes().Remove(); root.Add( - new XAttribute("name", itemTitle), + new XAttribute("name", modName), new XAttribute("steamworkshopid", itemId), new XAttribute("corepackage", isCorePackage), new XAttribute("modversion", modVersion), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 53f0d75cf..3dfef48c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -46,8 +46,9 @@ namespace Barotrauma CJK = 0x1, Cyrillic = 0x2, - - All = 0x3 + Japanese = 0x4, + + All = 0x7 } public static readonly ImmutableArray SpeciallyHandledCharCategories @@ -60,8 +61,6 @@ namespace Barotrauma { (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges( UnicodeRanges.HangulJamo, - UnicodeRanges.Hiragana, - UnicodeRanges.Katakana, UnicodeRanges.CjkRadicalsSupplement, UnicodeRanges.CjkSymbolsandPunctuation, UnicodeRanges.EnclosedCjkLettersandMonths, @@ -69,7 +68,13 @@ namespace Barotrauma UnicodeRanges.CjkUnifiedIdeographsExtensionA, UnicodeRanges.CjkUnifiedIdeographs, UnicodeRanges.HangulSyllables, - UnicodeRanges.CjkCompatibilityForms + UnicodeRanges.CjkCompatibilityForms, + //not really CJK symbols, but these seem to be present in the CJK fonts but not in the default ones, so we can use them as a fallback + UnicodeRanges.BlockElements + )), + (SpeciallyHandledCharCategory.Japanese, UnicodeToIntRanges( + UnicodeRanges.Hiragana, + UnicodeRanges.Katakana )), (SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges( UnicodeRanges.Cyrillic, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 4d7ca053c..02f086571 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -7,7 +7,14 @@ namespace Barotrauma { private static double alpha; + /// + /// Total amount of the time the game has run. + /// public static double TotalTime; + /// + /// Total amount of time the game has run, excluding when the game is paused. + /// + public static double TotalTimeUnpaused; public static double Accumulator; public const int FixedUpdateRate = 60; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 610c74cc2..9114c6f6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -538,23 +538,25 @@ namespace Barotrauma if (character is null) { return false; } if (!ResourceCosts.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character); + return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem)); } + // ReSharper disable PossibleMultipleEnumeration public bool TryTakeResources(Character character, int currentLevel) { - IEnumerable costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); + var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); if (!costs.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character); HashSet itemsToRemove = new HashSet(); foreach (UpgradeResourceCost cost in costs) { int amountNeeded = cost.Amount; - foreach (Item item in allItems.Where(cost.MatchesItem)) + foreach (Item item in inventoryItems.Where(cost.MatchesItem)) { itemsToRemove.Add(item); amountNeeded--; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 76671aea5..195cba7fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -273,9 +273,9 @@ namespace Barotrauma } /// - /// check whether line from a to b is intersecting with line from c to b + /// Check whether a line segment from a to b is intersecting with a line segment from c to d /// - public static bool LinesIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d) + public static bool LineSegmentsIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d) { float denominator = ((b.X - a.X) * (d.Y - c.Y)) - ((b.Y - a.Y) * (d.X - c.X)); float numerator1 = ((a.Y - c.Y) * (d.X - c.X)) - ((a.X - c.X) * (d.Y - c.Y)); @@ -289,13 +289,18 @@ namespace Barotrauma return (r >= 0 && r <= 1) && (s >= 0 && s <= 1); } - public static bool GetLineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, out Vector2 intersection) + /// + /// Find where the line segments (i.e. non-infinite lines between the points) intersect + /// + public static bool GetLineSegmentIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, out Vector2 intersection) { - return GetLineIntersection(a1, a2, b1, b2, false, out intersection); + return GetLineIntersection(a1, a2, b1, b2, areLinesInfinite: false, out intersection); } - // a1 is line1 start, a2 is line1 end, b1 is line2 start, b2 is line2 end - public static bool GetLineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, bool ignoreSegments, out Vector2 intersection) + /// + /// Find where the lines intersect. Use the areLinesInfinite argument to specify whether the lines should be finite segments or inifinite + /// + public static bool GetLineIntersection(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, bool areLinesInfinite, out Vector2 intersection) { intersection = Vector2.Zero; @@ -308,10 +313,13 @@ namespace Barotrauma Vector2 c = b1 - a1; float t = (c.X * d.Y - c.Y * d.X) / bDotDPerp; - if ((t < 0 || t > 1) && !ignoreSegments) return false; - float u = (c.X * b.Y - c.Y * b.X) / bDotDPerp; - if ((u < 0 || u > 1) && !ignoreSegments) return false; + if (!areLinesInfinite) + { + if (t < 0 || t > 1) { return false; } + float u = (c.X * b.Y - c.Y * b.X) / bDotDPerp; + if (u < 0 || u > 1) { return false; } + } intersection = a1 + t * b; return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 9429dcf93..feedf094a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -12,15 +12,27 @@ namespace Barotrauma private static readonly Dictionary> cachedNonAbstractTypes = new Dictionary>(); + private static readonly Dictionary>> cachedDerivedNonAbstract + = new Dictionary>>(); + public static IEnumerable GetDerivedNonAbstract() { + Type t = typeof(T); Assembly assembly = typeof(T).Assembly; if (!cachedNonAbstractTypes.ContainsKey(assembly)) { cachedNonAbstractTypes[assembly] = assembly.GetTypes() .Where(t => !t.IsAbstract).ToImmutableArray(); + + cachedDerivedNonAbstract[assembly] = new Dictionary>(); } - return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); + if (cachedDerivedNonAbstract[assembly].TryGetValue(t, out var cachedArray)) + { + return cachedArray; + } + var newArray = cachedNonAbstractTypes[assembly].Where(t2 => t2.IsSubclassOf(t)).ToImmutableArray(); + cachedDerivedNonAbstract[assembly].Add(t, newArray); + return newArray; } public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 02bf3c4b2..75b4957f2 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,148 @@ +--------------------------------------------------------------------------------------------------------- +v1.0.20.1 +--------------------------------------------------------------------------------------------------------- + +Fixes: +- Fixed hidden structures not colliding anymore. +- Wiring debugger: Fixed tooltip rendering under the outer frame of the connection panel. +- Wiring debugger: Fixed the glow sprite on connections having an inconsistent size in different resolutions- +- Fixed jailbreak_sootman event getting stuck at the 1st SpawnAction, preventing most of the event from working at all. + +--------------------------------------------------------------------------------------------------------- +v1.0.20.0 +--------------------------------------------------------------------------------------------------------- + +Optimization: +- Major optimizations to situations where there's lots of items in the submarine (especially water-reactant items and minerals). +- Significantly reduced loading times when loading subs with lots of items in cabinets. +- Partially multithreaded lighting: FindRaycastHits (the heaviest individual part of the lighting algorithm) is now handled in a separate thread. + +Line of sight rework: +- Improved the LOS effect to get rid of weird, jagged geometry in spots with intersecting walls. +- Improved the wall damage effect to make leaks easier to see. +- Added a command called "debugdrawlos" which visualizes the LOS geometry, making it easier to adjust the walls if you want to tweak the LOS in the sub editor. +- Note that some submarines may require small manual adjustments to get the LOS to work perfectly with the new algorithm (especially if the sub includes “unnecessary” structures that were used to work around issues in the previous algorithm). You can also disable the “cast shadows” setting on a wall to make it see-through. + +Changes: +- Added "electrician's goggles", an item that visualizes power and signals on wires. It also makes connections on the connection panel flash when they're sending/receiving something, and allows you to see what exactly is going through (amount of power, signal values) by hovering the cursor over the connection. +- Added "debugwiring" console command which enables the same visualization as the electrician's goggles. +- Oxygenite tanks can be recycled. +- Shutting down the reactor doesn't automatically turn off automatic control. +- Allow monsters to damage flares and glowsticks. +- Jacov Subra needs to be escorted to the next location after he's rescued (it was weird to have him just disappear after the mission). +- Taking items from trash cans is no longer considered stealing. +- Sulphurite shards explode when thrown (causes an acid cloud that inflicts mild burns). +- Adjusted music intensity ranges: now the "intensity tracks" kick in later (i.e. they require a higher-intensity situation) meaning the "actual songs" play a little more often. +- Upgrades that require materials can be purchased with materials from the sub instead of having to carry the materials on you. +- Made clown and husk events and missions more common. +- Added category buttons to the fabricator interface to make it easier to find items. +- Changed fabricator's "skills required" texts to make it clearer insufficient skills don't prevent you from fabricating the items. +- Hospital beds heal injuries faster than normal beds. + +Fixes: +- Fixed "hash mismatch" errors when publishing an update to a mod after you've given it a different name in the Steam Workshop in the language your Steam UI is set to. +- Fixed publishing an update to a mod always overriding the English description, instead of the language your Steam UI is set to. +- Fixed nav terminal's sonar control panel disappearing when switching resolution or display mode if the terminal has a mineral scanner. +- Fixed secure steel cabinet in abandoned outposts being openable with a randomly selected bandit's ID card, not the bandit leader's card as intended. +- Workaround to Sootman potentially getting stuck in the jailbreak mission if it can't find a way to the sub (e.g. due to broken waypoints in a custom sub or outpost module): if he can't find a way to the sub in 2 minutes, he'll start following the player instead. +- Fixed some z-fighting issues in various outpost modules. +- Fixed flak cannon shells exploding in loaders if there's a monster near the loader. +- Fixed oxygenite tanks not being set to 0 condition when they explode in a welding tool or flamer. +- Fixed/changed welding a door shut not protecting the door from damage. Modding: Added a new tag "weldable" to define targets that can be welded. +- Fixed flamer damaging duct blocks, unlike intended. +- Fixed Harpoons only reeling in when facing the target. +- Fixed wires that aren't connected to anything not getting copied (with Ctrl+Z or by dragging and dropping with Ctrl) in the sub editor. +- Fixed Hognose event being possible to get multiple times, potentially allowing you to hire multiple Captain Hognoses. +- Fixed lights ignoring constant set_color signals after the color is changed manually. +- Fixed both biome gate locations sometimes belonging to the same faction (and in general, random locations getting changed into Coalition outposts across the map). +- Fixed server log stopping to update after there's 800 messages in it. +- Fixed missions that take place in a location further away on the map sometimes not unlocking when there was no suitable location available. +- Fixed 3rd part of the Honkmotherian Scriptures not being available anywhere. +- Fixed tutorials reappearing after finishing the campaign. +- Fixed progressive stun's visual effect and speed multiplier abruptly resetting when passing affliction strength 9. +- Fixed certain special symbols (the BlockElements unicode range) not showing up on text displays. +- Fixed characters with husk symbiosis transforming to an AI-controlled husk when the host dies. +- Fixed rewards not being shown in the round summary unless the mission was completed successfully (i.e. you couldn't see what the reward is when you start the mission). +- Fixed advanced gene splicer deconstruction output. +- Fixed weird behavior when selecting a periscope a bot is controlling (UI disappearing as if you were operating the turret). +- Fixed waypoint visibility checkbox not being selected when you generate waypoints in the sub editor, even though generating the waypoints toggles their visibility on. +- Fixed water moving erratically in rooms with lots of connected hulls (with weird spikes and water level jumping up and down). +- Fixes anaparalyzant not reducing paralysis. +- Fixed some Chinese characters not displaying correctly when the language is set to something else than Chinese. +- Adjusted fire particles to make them clip through walls less. +- Fixed characters not falling through holes appearing in the floor below them until they move. +- Fixed characters twisting to a weird pose if you grab them while they're lying in bed. +- Miscellaneous tutorial fixes: don't allow the player to access the areas of the other jobs in the roles tutorial, fixed getting stuck if you enter the rightmost room (where the role tutorials start) as an assistant, fixed bots sometimes wandering outside the initial room. +- Fixed a rare crash caused by a null reference exception in Rope.Update. +- Fixed characters being allowed to aim with weapons in single player when incapacitated/ragdolled. They couldn't actually move the aim position, but the crosshairs appeared and harpoon ropes could be kept from snapping, even though they're set to snap when not aiming. +- Fixes to some of the UI layout issues on ultrawide resolutions. The UI is still not optimal on ultrawide resolutions, but the most severe issues (such as the non-functional crew list) should be fixed now. +- Fixed negative treatment suitability definitions not working correctly, which has had some effect on the bots’ decisions and the suitable treatment listing on the health interface. +- Fixed the min and max strength thresholds of periodic effects not working. Of the vanilla afflictions, affects paralysis, sufforine poisoning, and morbusine poisoning, causing them to trigger stun effects earlier than intended. +- Fixed hanging end of the wire sometimes being "at the wrong end" of the wire after copying entities or saving/reloading. +- Added an option to disable the "remote content" (update notifications, changelogs) in the main menu by adding RemoteContentUrl="" to the player config file. We're suspecting the freezes some players are experiencing when they open the main menu are related to the content in the main menu being fetched from our server, and testing if disabling it makes the issue go away would give us more clues for diagnosing the issue. + +Talents: +- Fixed "Medical Expertise" not increasing bandage effectiveness. +- Fixed "Mudraptor Wrestler" not affecting unarmored or veteran Mudraptors. +- Fixed monsters not properly ignoring characters with the "Non-Threatening" talent. +- Fixed Moloch, Black Moloch, Crawler Broodmother and Giant Spineling not having enough inventory space for extra loot given by "Bloody Business" and "Gene Harvester" talents. +- Fixed "Rifleman" talent damage bonus not applying to Assault Rifles. + +Multiplayer fixes: +- Increased the default packet limits. It seems the previous defaults were so low it was possible to get kicked due to "sending too many network messages" in some situations even if you weren't actually trying to spam the server using a modified version of the game. +- Fixed rate limiter kicking in too eagerly when the server is configured to use a higher-than normal tickrate. +- Fixed an exploit that allowed getting new subs for free. +- Fixed submarine store displaying the client's subs instead of the server's. +- Fixed Hangul (Korean symbols) not being included in the default allowed client name characters. +- Fixed headset/radio channel resetting between multiplayer rounds. +- Fixed clients hearing radio static when someone talks in a different channel near them. +- Fixed tainted genetic materials in someone's inventory appearing to become untainted when a new round starts. +- Fixed local voice chat icon switching to radio icon (from yellow to gray) at the end of conversations when you release the push-to-talk key. +- Fixed server description getting cut off after 22 lines. +- Fixed changing the name of a dedicated server using the textbox in the server lobby not changing it in the server list. +- Fixed clients not getting positional info for remotely controlled turrets if the character controlling them is far away (> 250 m), meaning you wouldn't see a turret move if you're watching some e.g. operate a remotely controlled turret on a drone. +- Fixed mission minerals spawning with an incorrect rotation (always facing up) in multiplayer. +- Fixed inability to unlock some achievements (such as "Nuclear blast survivor") in multiplayer. +- Fixed cursor positions sometimes "desyncing" when the cursor is very far from the character (e.g. when operating a far-away drone). +- Fixed characters sometimes getting stuck on the wrong side of a door client-side, and not getting corrected until the client moves in the opposite direction or opens the door. + +Bot and AI fixes: +- Improved how the bots navigate on ladders. +- Fixed bots sometimes not being able to release the ladders when there's two ladders close to each other. +- ClownDistrict_Colony_01: Fix a ladder waypoint not being linked to the ladder, causing bots to get stuck. +- Adjusted the waypoint generation logic on ladders. Fixes bots sometimes not being able to open the hatches, because they couldn't get close enough to the waypoints just below/above them. +- Improvements to medic bot AI: they should now be better at determining which injuries are minor enough to ignore, making them waste less medicine. +- Fixed outpost NPCs who should stay in a specific room (e.g. outpost manager) never returning to the room if they leave it e.g. due to a fire, flood or someone attacking them. +- Fixed bots picking up non-pickable items (items that have CanBePicked disabled in the Pickable component settings). +- Fixed bots saying "Cannot reach [name]!", when they can't reach a wait target. +- Fixed bots removing diving suits when they shouldn't, if they are ordered to wait using the contextual wait order. Should behave the same as when told to wait without using the contextual order. +- Fixed bots having issues with doors that are both broken and welded (which is another bug that shouldn’t happen anymore). +- Fixed NPCs running towards doors if they somehow get on another sub, because they didn't detect the access restrictions correctly. +- Set Remora's and Kastrull's drones, outpost jail rooms and (automatically generated) hulls in docking ports as AvoidStaying zones, which makes the bots to try not to idle there. +- Waypoint fixes to Typhon, R-29, and Camel. +- Fixed bots never leaving the drones (or other connected subs) and returning back to the main submarine in the idle state (i.e. without giving them an order or reacting to an emergency situation etc.) +- Addressed bots and NPCs sometimes twitching between two waypoints after reaching their target position. +- Fixed bots not always being able to open the doors in beacons or wrecks while attempting to return back to the main sub. +- Fixed the start and the end node of the vertical hallways in the outposts being incorrectly linked, causing some pathing issues for the bots. +- Fixed bots operating turrets shooting at handcuffed enemies. +- Fixed defense bots attacking husk containers. +- Fixed monsters never ignoring targets that they fail to damage. They should leave those be after a while (depending on the initial priority). +- Fixed pets becoming hostile to all humans (except the owner) when attacked by a hostile human (a bandit in an abandoned outpost for example). +- Potential fix for (hard to repro) issues where a bot runs towards a door without being able to open it, because they happen to skip the nodes past the door when it closes. +- Fixed NPCs and bots sometimes not being able to open the hatch, because they couldn't get close enough to skip the last node before the node connected to the hatch. +- Fixed incorrect linking of the start and the end waypoints in the vertical hallway modules. Caused the bots to open the hatches too early. + +Modding: +- Fixed "hair with hat sprite" (the short hair the characters switch to when wearing a hat) being visible even if the whole head should be hidden by another wearable. Not noticeable in the vanilla game, but can affect some mods. +- Fixed crashing when trying to create a character that doesn't have any health parameters. +- Fixed submarine spawning docked to a random docking port of the outpost even if one of them is marked as a main docking port. Doesn't affect vanilla content, since there's only ever one docking port per outpost. +- Fixed round not ending when you dock with an outpost if the outpost module contains any linked subs (lifts, drones, etc). +- Fixed items with absurd amounts of health (more than 38 digits) causing the item to have infinite health, which would lead to various issues and crashes. Now an item's health can't be set above a million. If you want to have an item whose condition never goes down, use the property "Indestructible" instead. +- Autoinjecting can be enabled on specific subcontainers (as opposed to having to make the item autoinject anything inside it). +- Fixed bots being unable to use diving gear that goes in the InnerClothes slot. Doesn't affect any vanilla content, because all diving gear goes in the OuterClothes slot. +- Items' FireProof and WaterProof properties can be changed using status effects. +- Fixed monsters' SimplePhysicsEnabled staying enabled when a client takes control of it. + --------------------------------------------------------------------------------------------------------- v1.0.13.2 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index 0f35438bd..9bc7771bb 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -25,7 +25,7 @@ namespace Steamworks SteamMatchmakingRulesResponse? responseHandler = null; void onRulesResponded(string key, string value) - => rules.Add(key, value); + => rules.TryAdd(key, value); void onRulesFailToRespond() { diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 0b9a0f7a5..62a8d13c0 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -334,6 +334,7 @@ namespace FarseerPhysics.Dynamics CreateProxies(); // Contacts are created the next time step. + OnEnabled?.Invoke(); } else { @@ -342,6 +343,7 @@ namespace FarseerPhysics.Dynamics DestroyProxies(); DestroyContacts(); } + OnDisabled?.Invoke(); } } } @@ -1205,6 +1207,8 @@ namespace FarseerPhysics.Dynamics remove { onSeparationEventHandler -= value; } } + public Action OnEnabled, OnDisabled; + public float Restitution { set { SetRestitution(value); }