diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f7f77dc2c..589a3ce6e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,13 +27,15 @@ body: attributes: label: Reproduction steps description: | - If possible, describe how the developers can get the bug to happen. It is often extremely hard to fix a bug if we don't know how to reproduce it. + If possible, describe how the developers can get the bug to happen (or, in other words, what actions lead to you encountering the bug). **This is by far the most important part of the report** - it is often extremely difficult, or even impossible, to diagnose an issue if we don't know the conditions it occurs in. If you have a save, a submarine file, screenshots or any other files that might help us diagnose the issue, you can attach them here. Note that GitHub doesn't support the .save or .sub file extensions, so you should .zip those types of files to allow them to be attached. placeholder: | 1. Start a multiplayer campaign 2. Spawn a bike horn with console commands 3. Use the bike horn 4. Observe how the game crashes + validations: + required: true - type: dropdown id: prevalence attributes: @@ -52,9 +54,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - 0.21.6.0 - - 0.21.6.0 (Unstable) - - Faction/endgame test branch + - v1.0.20.1 - Other validations: required: true 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/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 4be093613..3c2985c11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -10,7 +10,7 @@ namespace Barotrauma public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (Character == Character.Controlled) { return; } - if (!debugai) { return; } + if (!DebugAI) { return; } Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index a350ef029..8f503a225 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -6,11 +6,17 @@ namespace Barotrauma { partial class Attack { - [Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] + [Serialize("StructureBlunt", IsPropertySaveable.Yes, description: "Name of the sound effect the attack makes when it hits a structure."), Editable()] public string StructureSoundType { get; private set; } + /// + /// Sound to play when the attack deals damage. + /// private RoundSound sound; + /// + /// Particle emitter to use when the attack deals damage. + /// private ParticleEmitter particleEmitter; partial void InitProjSpecific(ContentXElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0f86bcc9c..5c54b9ba2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -582,7 +582,7 @@ namespace Barotrauma float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); foreach (MapEntity entity in entityList) { - if (!(entity is Item item)) + if (entity is not Item item) { continue; } 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 1acc8736a..61729cd15 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/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 76afc54f2..4c883b07d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -86,7 +86,7 @@ namespace Barotrauma public string ModVersion = ContentPackage.DefaultModVersion; - public Md5Hash? ExpectedHash { get; private set; } + public Md5Hash? ExpectedHash { get; set; } public bool IsCore = false; @@ -125,9 +125,11 @@ namespace Barotrauma public static string IncrementModVersion(string modVersion) { + if (string.IsNullOrWhiteSpace(modVersion)) { return string.Empty; } + //look for an integer at the end of the string and increment it int startIndex = modVersion.Length - 1; - while (char.IsDigit(modVersion[startIndex])) { startIndex--; } + while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; } startIndex++; if (startIndex >= modVersion.Length diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 1fe714a9b..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; @@ -636,15 +637,29 @@ namespace Barotrauma commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) => { if (Character.Controlled == null) { return; } - WikiImage.Create(Character.Controlled); + try + { + WikiImage.Create(Character.Controlled); + } + catch (Exception e) + { + DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e); + } })); commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) => { if (Submarine.MainSub == null) { return; } - MapEntity.SelectedList.Clear(); - MapEntity.ClearHighlightedEntities(); - WikiImage.Create(Submarine.MainSub); + try + { + MapEntity.SelectedList.Clear(); + MapEntity.ClearHighlightedEntities(); + WikiImage.Create(Submarine.MainSub); + } + catch (Exception e) + { + DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e); + } })); AssignRelayToServer("kick", false); @@ -1141,6 +1156,15 @@ namespace Barotrauma 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) => @@ -1263,8 +1287,8 @@ namespace Barotrauma AssignOnExecute("debugai", (string[] args) => { - HumanAIController.debugai = !HumanAIController.debugai; - if (HumanAIController.debugai) + HumanAIController.DebugAI = !HumanAIController.DebugAI; + if (HumanAIController.DebugAI) { GameMain.DevMode = true; GameMain.DebugDraw = true; @@ -1279,7 +1303,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); + NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); @@ -2338,7 +2362,7 @@ namespace Barotrauma { if (mapEntity is Item item) { - item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, + item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y, (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); } @@ -2811,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/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 1aa23640c..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); @@ -517,6 +518,10 @@ namespace Barotrauma GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) { + if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count) + { + throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}"); + } Texture2D tex = textures[gd.TexIndex]; Vector2 drawOffset; drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; 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/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 1ae121338..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, @@ -801,6 +802,7 @@ namespace Barotrauma IEnumerable GetAndSortTextBoxes(GUIComponent parent) => parent.GetAllChildren().OrderBy(t => t.Rect.Y).ThenBy(t => t.Rect.X); GUITextBox SelectNextTextBox(GUIListBox listBox) { + if (listBox?.SelectedComponent == null) { return null; } var textBoxes = GetAndSortTextBoxes(listBox.SelectedComponent); if (textBoxes.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index c43df5f39..e19738609 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -240,7 +240,7 @@ namespace Barotrauma private void UpdatePending() { - if (!(pendingHealList is { } healList)) { return; } + if (pendingHealList is not { } healList) { return; } ImmutableArray pendingList = medicalClinic.PendingHeals.ToImmutableArray(); @@ -493,20 +493,26 @@ namespace Barotrauma GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) { - OnClicked = (_, _) => + OnClicked = (button, _) => { + if (isWaitingForServer) { return true; } + + button.Enabled = false; isWaitingForServer = true; - medicalClinic.TreatAllButtonAction(OnReceived); + + bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton()); + if (!wasSuccessful) { ReEnableButton(); } + + void ReEnableButton() + { + isWaitingForServer = false; + button.Enabled = true; + } return true; } }; crewHealList = new CrewHealList(crewList, parent, treatAllButton); - - void OnReceived(MedicalClinic.CallbackOnlyRequest obj) - { - isWaitingForServer = false; - } } private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) @@ -585,8 +591,10 @@ namespace Barotrauma OnClicked = (button, _) => { button.Enabled = false; - medicalClinic.HealAllButtonAction(request => + isWaitingForServer = true; + bool wasSuccessful = medicalClinic.HealAllButtonAction(request => { + isWaitingForServer = false; switch (request.HealResult) { case MedicalClinic.HealRequestResult.InsufficientFunds: @@ -600,6 +608,12 @@ namespace Barotrauma button.Enabled = true; ClosePopup(); }); + + if (!wasSuccessful) + { + isWaitingForServer = false; + button.Enabled = true; + } ClosePopup(); return true; } @@ -610,11 +624,19 @@ namespace Barotrauma ClickSound = GUISoundType.Cart, OnClicked = (button, _) => { + if (isWaitingForServer) { return true; } + button.Enabled = false; - medicalClinic.ClearAllButtonAction(_ => + isWaitingForServer = true; + + bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton()); + if (!wasSuccessful) { ReEnableButton(); } + + void ReEnableButton() { + isWaitingForServer = false; button.Enabled = true; - }); + } return true; } }; @@ -701,10 +723,15 @@ namespace Barotrauma OnClicked = (button, _) => { button.Enabled = false; - medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => + bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => { button.Enabled = true; }); + + if (!wasSuccessful) + { + button.Enabled = true; + } return true; } }; @@ -792,7 +819,13 @@ namespace Barotrauma selectedCrewAfflictionList = popupAfflictionList; isWaitingForServer = true; - medicalClinic.RequestAfflictions(info, OnReceived); + bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived); + + if (!wasSuccessful) + { + isWaitingForServer = false; + ClosePopup(); + } void OnReceived(MedicalClinic.AfflictionRequest request) { @@ -800,6 +833,16 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { + switch (request.Result) + { + case MedicalClinic.RequestResult.CharacterInfoMissing: + DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName}\" in medical clini because the character health was missing."); + break; + case MedicalClinic.RequestResult.CharacterNotFound: + DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName} in medical clinic because the server was unable to find a character with ID {info.ID}."); + break; + } + feedbackBlock.Text = GetErrorText(request.Result); feedbackBlock.TextColor = GUIStyle.Red; return; @@ -953,14 +996,20 @@ namespace Barotrauma } existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); + ToggleElements(ElementState.Disabled, elementsToDisable); - medicalClinic.AddPendingButtonAction(existingMember, request => + bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request => { if (request.Result == MedicalClinic.RequestResult.Timeout) { ToggleElements(ElementState.Enabled, elementsToDisable); } }); + + if (!wasSuccessful) + { + ToggleElements(ElementState.Enabled, elementsToDisable); + } } #warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString @@ -1090,9 +1139,8 @@ namespace Barotrauma { return result switch { - MedicalClinic.RequestResult.Error => TextManager.Get("error"), MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), - _ => "What the hell did you just do" // this should never happen + _ => TextManager.Get("error") }; } 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/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/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index d893dc526..89364a512 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -166,6 +166,9 @@ namespace Barotrauma if (Submarine.MainSub == null || Level.Loaded == null) { return; } bool allowEndingRound = false; + endRoundButton.Color = endRoundButton.Style.Color; + endRoundButton.HoverColor = endRoundButton.Style.HoverColor; + RichString overrideEndRoundButtonToolTip = string.Empty; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); LocalizedString buttonText = ""; switch (availableTransition) @@ -194,13 +197,23 @@ namespace Barotrauma break; case TransitionType.None: default: - if (Level.Loaded.Type == LevelData.LevelType.Outpost && - !Level.Loaded.IsEndBiome && - (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) + bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true }; + if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome && + (inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { + if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub)) + { + overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub"); + endRoundButton.Color = GUIStyle.Red * 0.7f; + endRoundButton.HoverColor = GUIStyle.Red; + } buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } + else + { + allowEndingRound = false; + } break; } if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) @@ -227,7 +240,11 @@ namespace Barotrauma prevCampaignUIAutoOpenType = availableTransition; } endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); - if (endRoundButton.Text != buttonText) + if (overrideEndRoundButtonToolTip != string.Empty) + { + endRoundButton.ToolTip = overrideEndRoundButtonToolTip; + } + else if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index c7fb14619..ab23c52d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -193,7 +193,7 @@ namespace Barotrauma if (GameMain.Client == null) { - yield return CoroutineStatus.Failure; + yield return CoroutineStatus.Success; } if (GameMain.Client.LateCampaignJoin) @@ -335,7 +335,7 @@ namespace Barotrauma //-------------------------------------- //wait for the new level to be loaded - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60); + DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut; while (Level.Loaded == prevLevel || Level.Loaded == null) { if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } @@ -345,7 +345,11 @@ namespace Barotrauma endTransition.Stop(); overlayColor = Color.Transparent; - if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } + if (DateTime.Now > timeOut) + { + DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish."); + GameMain.NetLobbyScreen.Select(); + } if (Screen.Selected is not RoundSummaryScreen) { if (continueButton != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 3b4d31cf6..796546c78 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -17,7 +17,8 @@ namespace Barotrauma { Undecided, Success, - Error, + CharacterInfoMissing, + CharacterNotFound, Timeout } @@ -34,7 +35,9 @@ namespace Barotrauma private readonly List> addRequests = new List>(); private readonly List> removeRequests = new List>(); - public void RequestAfflictions(CharacterInfo info, Action onReceived) + private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10); + + public bool RequestAfflictions(CharacterInfo info, Action onReceived) { if (GameMain.IsSingleplayer) { @@ -42,23 +45,26 @@ namespace Barotrauma if (Screen.Selected is TestScreen) { onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); - return; + return true; } #endif if (info is not { Character.CharacterHealth: { } health }) { - onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); - return; + onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray.Empty)); + return true; } - ImmutableArray pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); + ImmutableArray pendingAfflictions = GetAllAfflictions(health); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); - return; + return true; } - afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); - SendAfflictionRequest(info); + return requestBucket.TryEnqueue(() => + { + afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendAfflictionRequest(info); + }); } public void RequestLatestPending(Action onReceived) @@ -66,8 +72,11 @@ namespace Barotrauma // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI if (GameMain.IsSingleplayer) { return; } - pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); - SendPendingRequest(); + requestBucket.TryEnqueue(() => + { + pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendPendingRequest(); + }); } public void Update(float deltaTime) @@ -79,6 +88,7 @@ namespace Barotrauma UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); + requestBucket.Update(deltaTime); static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } @@ -146,21 +156,25 @@ namespace Barotrauma return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } - public void TreatAllButtonAction(Action onReceived) + public bool TreatAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { AddEverythingToPending(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - addRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + }); } - public void HealAllButtonAction(Action onReceived) + + public bool HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { @@ -171,33 +185,39 @@ namespace Barotrauma OnUpdate?.Invoke(); } - return; + return true; } - if (campaign?.CampaignUI?.MedicalClinic is { } ui) + if (campaign?.CampaignUI?.MedicalClinic is { } openedUi) { - ui.ClosePopup(); + openedUi.ClosePopup(); } - healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + }); } - public void ClearAllButtonAction(Action onReceived) + public bool ClearAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) { ClearPendingHeals(); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + }); } - private void ClearRequstReceived() + private void ClearRequestReceived() { ClearPendingHeals(); if (TryDequeue(clearAllRequests, out var callback)) @@ -224,28 +244,31 @@ namespace Barotrauma OnUpdate?.Invoke(); } - public void AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) + public bool AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) { if (GameMain.IsSingleplayer) { InsertPendingCrewMember(crewMember); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } - addRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + }); } - public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) + public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) { if (GameMain.IsSingleplayer) { RemovePendingAffliction(crewMember, affliction); onReceived(new CallbackOnlyRequest(RequestResult.Success)); OnUpdate?.Invoke(); - return; + return true; } INetSerializableStruct removedAffliction = new NetRemovedAffliction @@ -254,11 +277,14 @@ namespace Barotrauma Affliction = affliction }; - removeRequests.Add(new RequestAction(onReceived, GetTimeout())); - ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + return requestBucket.TryEnqueue(() => + { + removeRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + }); } - private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) + private void NewAdditionReceived(IReadMessage inc, MessageFlag flag) { var crewMembers = INetSerializableStruct.Read>(inc); foreach (var crewMember in crewMembers) @@ -300,7 +326,7 @@ namespace Barotrauma NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { - RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; + RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } @@ -336,7 +362,7 @@ namespace Barotrauma IWriteMessage msg = StartSending(); msg.WriteByte((byte)header); netStruct?.Write(msg); - GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); + GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod); } public void ClientRead(IReadMessage inc) @@ -356,7 +382,7 @@ namespace Barotrauma PendingRequestReceived(inc); break; case NetworkHeader.ADD_PENDING: - NewAdditonReceived(inc, flag); + NewAdditionReceived(inc, flag); break; case NetworkHeader.REMOVE_PENDING: NewRemovalReceived(inc, flag); @@ -365,7 +391,7 @@ namespace Barotrauma HealRequestReceived(inc); break; case NetworkHeader.CLEAR_PENDING: - ClearRequstReceived(); + ClearRequestReceived(); break; } } 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 1093798a8..0be3b921f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -155,8 +155,6 @@ namespace Barotrauma.Items.Components convexHull.Enabled = true; SetVertices(convexHull, rect); } - convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; - if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } } @@ -169,12 +167,11 @@ namespace Barotrauma.Items.Components 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) { - convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; - if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } if (shakeTimer > 0.0f) { shakeTimer -= deltaTime; 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 1dd6f4341..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,9 +640,13 @@ namespace Barotrauma.Items.Components } toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; - if (!requiredItemPrefab.Description.IsNullOrEmpty()) + if (!requiredItem.OverrideDescription.IsNullOrEmpty()) { - toolTipText = '\n' + requiredItemPrefab.Description; + toolTipText += '\n' + requiredItem.OverrideDescription; + } + else if (!requiredItemPrefab.Description.IsNullOrEmpty()) + { + toolTipText += '\n' + requiredItemPrefab.Description; } tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } @@ -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 969a67312..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; } @@ -403,7 +400,8 @@ namespace Barotrauma.Items.Components private bool VisibleOnItemFinder(Item it) { - if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } + if (it?.Submarine == null) { return false; } + if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } if (it.NonInteractable || it.HiddenInGame) { return false; } if (it.GetComponent() == null) { return false; } @@ -436,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 }; @@ -444,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(); @@ -535,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(); @@ -550,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 @@ -730,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) { @@ -914,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; @@ -1063,7 +1065,9 @@ namespace Barotrauma.Items.Components waterVolume += linkedHull.WaterVolume; totalVolume += linkedHull.Volume; } - hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100); + hullData.HullWaterAmount = + waterVolume > 1.0f ? + MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f; } else { @@ -1302,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); @@ -1523,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 @@ -1536,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; @@ -1683,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 9c03c2a3c..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) @@ -1311,7 +1314,6 @@ namespace Barotrauma.Items.Components float worldPingRadiusSqr = worldPingRadius * worldPingRadius; disruptedDirections.Clear(); - if (Level.Loaded == null) { return; } for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { @@ -1513,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) @@ -1923,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); } } @@ -1935,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); @@ -1966,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; @@ -1983,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/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 6a2b6571b..aaca8fda0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); - SpreadCounter = msg.ReadByte(); + spreadIndex = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index a83d9a305..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) @@ -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 9f755a3a8..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() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 1668f9739..2f4c6a63c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components int visibleElementCount = 0; foreach (var uiElement in uiElements) { - if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } + if (uiElement.UserData is not CustomInterfaceElement element) { continue; } bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) @@ -337,7 +337,9 @@ namespace Barotrauma.Items.Components { if (uiElements[i] is GUITextBox tb) { - tb.Text = customInterfaceElementList[i].Signal; + tb.Text = Screen.Selected is { IsEditor: true } ? + customInterfaceElementList[i].Signal : + TextManager.Get(customInterfaceElementList[i].Signal).Value; } else if (uiElements[i] is GUINumberInput ni) { 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/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0f85d846d..c7da76748 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1398,7 +1398,7 @@ namespace Barotrauma } else { - throw new Exception("Failed to read component state - " + components[componentIndex].GetType() + " is not IServerSerializable."); + throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable."); } } break; @@ -1411,7 +1411,7 @@ namespace Barotrauma } else { - throw new Exception("Failed to read inventory state - " + components[containerIndex].GetType() + " is not an ItemContainer."); + throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer."); } } break; @@ -1460,9 +1460,9 @@ namespace Barotrauma byte length = msg.ReadByte(); for (int i = 0; i < length; i++) { - var statIdentifier = INetSerializableStruct.Read(msg); + var statIdentifier = INetSerializableStruct.Read(msg); var statValue = msg.ReadSingle(); - StatManager.ApplyStat(statIdentifier, statValue); + StatManager.ApplyStatDirect(statIdentifier, statValue); } break; case EventType.Upgrade: 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/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 3be5faa17..7faf33912 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -93,13 +93,16 @@ namespace Barotrauma.Lights private readonly int thickness; - public bool IsExteriorWall; - 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,7 +133,7 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } - public ConvexHull(Rectangle rect, bool? isHorizontal, MapEntity parent) + public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent) { shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { @@ -150,15 +153,15 @@ namespace Barotrauma.Lights BoundingBox = rect; - this.isHorizontal = 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; } } @@ -205,44 +208,97 @@ namespace Barotrauma.Lights { if (ch == this) { return; } - //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) - float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512); - mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2); + //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) + { + mergeDistParallel = Math.Max(mergeDistParallel, MaxMergeLosVerticesDist.Value); + } + else + { + 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)) + { + 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); - float mergeDistSqr = mergeDist * mergeDist; + Vector2 center = (losVertices[0].Pos + losVertices[1].Pos) / 2; bool changed = false; for (int i = 0; i < losVertices.Length; i++) { - //find the closest point on the other convex hull segment + 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 (Vector2.DistanceSquared(closest, losVertices[i].Pos) > mergeDistSqr) { continue; } + 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, - out Vector2 intersection)) + 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)) { - if (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistSqr || - Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f) - { - closest = intersection; - } + 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) { @@ -253,6 +309,13 @@ namespace Barotrauma.Lights } } + 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) { Matrix rotationMatrix = @@ -347,6 +410,7 @@ namespace Barotrauma.Lights for (int i = 0; i < 2; i++) { losVertices[i] = new SegmentPoint(losPoints[i], this); + losOffsets[i] = Vector2.Zero; } overlappingHulls.Clear(); @@ -612,8 +676,11 @@ namespace Barotrauma.Lights vertexPos0 += ParentEntity.Submarine.DrawPosition; vertexPos1 += ParentEntity.Submarine.DrawPosition; } - Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition; - float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f; + 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 95a4d3a07..e846bfa4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -189,7 +189,7 @@ namespace Barotrauma.Lights } } - private class RayCastTask + private sealed class RayCastTask { public LightSource LightSource; public Vector2 DrawPos; @@ -298,7 +298,8 @@ namespace Barotrauma.Lights range * ((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) * (light.CastShadows ? 10.0f : 1.0f) * - (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)); + (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)) * + light.PriorityMultiplier; } //find the lights with an active light volume @@ -477,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); @@ -686,19 +698,42 @@ namespace Barotrauma.Lights if (LosEnabled && LosMode != LosMode.None && ViewTarget != null) { Vector2 pos = ViewTarget.DrawPosition; - if (ViewTarget is Character character && + 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); + + //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(); @@ -706,7 +741,6 @@ namespace Barotrauma.Lights foreach (ConvexHull convexHull in convexHulls) { if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } - if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; }; Vector2 relativeLightPos = pos; if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } @@ -744,13 +778,14 @@ namespace Barotrauma.Lights public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam) { - if (ViewTarget == null) { return; } + Vector2 pos = ViewTarget?.Position ?? cam.Position; spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform); - var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine); + 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(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index d4aa9dd17..9574a3d6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -53,10 +53,6 @@ namespace Barotrauma.Lights [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } - [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ - " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] - public bool Directional { get; set; } - public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation))); private float flicker; @@ -233,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; } @@ -246,9 +243,15 @@ 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; } @@ -309,8 +312,6 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; - dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); - if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { return; @@ -321,8 +322,6 @@ namespace Barotrauma.Lights } } - private Vector2 dir = Vector2.UnitX; - private Vector2 _spriteScale = Vector2.One; public Vector2 SpriteScale @@ -396,6 +395,8 @@ namespace Barotrauma.Lights public float Priority; + public float PriorityMultiplier = 1.0f; + private Vector2 lightTextureTargetSize; public Vector2 LightTextureTargetSize @@ -444,7 +445,7 @@ namespace Barotrauma.Lights public bool Enabled = true; private readonly ISerializableEntity conditionalTarget; - private readonly PropertyConditional.LogicalOperatorType logicalOperator; + private readonly PropertyConditional.Comparison comparison; private readonly List conditionals = new List(); public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) @@ -452,8 +453,11 @@ namespace Barotrauma.Lights { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); - logicalOperator = element.GetAttributeEnum(nameof(logicalOperator), - element.GetAttributeEnum("comparison", logicalOperator)); + string comparison = element.GetAttributeString("comparison", null); + if (comparison != null) + { + Enum.TryParse(comparison, ignoreCase: true, out this.comparison); + } if (lightSourceParams.DeformableLightSpriteElement != null) { @@ -466,7 +470,13 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - conditionals.AddRange(PropertyConditional.FromXElement(subElement)); + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + conditionals.Add(new PropertyConditional(attribute)); + } + } break; } } @@ -529,32 +539,11 @@ namespace Barotrauma.Lights var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } - //used to check whether the lightsource hits the target hull if the light is directional - Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange; - Vector2 normal = new Vector2(-ray.Y, ray.X); - chList.List.Clear(); foreach (var convexHull in fullChList.List) { if (!convexHull.Enabled) { continue; } if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } - if (lightSourceParams.Directional && false) - { - Rectangle bounds = convexHull.BoundingBox; - //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left - bounds.Y -= bounds.Height; - - //the ray can't hit if - // center is in the opposite direction from the ray (cheapest check first) - if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 && - /*ray doesn't hit the convex hull*/ - !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) && - /*normal vectors of the ray don't hit the convex hull */ - !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _)) - { - continue; - } - } chList.List.Add(convexHull); } chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); @@ -704,7 +693,7 @@ namespace Barotrauma.Lights lock (mutex) { hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments); + hull.GetVisibleSegments(drawPos, visibleSegments); foreach (var visibleSegment in visibleSegments) { if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null) @@ -713,7 +702,6 @@ namespace Barotrauma.Lights } } } - } } foreach (ConvexHull hull in chList.List) @@ -812,7 +800,7 @@ namespace Barotrauma.Lights } else { - intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); + intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection); } if (intersects) @@ -1006,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) @@ -1041,9 +1029,11 @@ namespace Barotrauma.Lights Vector2 drawPos = calculatedDrawPos; + float cosAngle = (float)Math.Cos(Rotation); + float sinAngle = -(float)Math.Sin(Rotation); + Vector2 uvOffset = Vector2.Zero; Vector2 overrideTextureDims = Vector2.One; - Vector2 dir = this.dir; if (OverrideLightTexture != null) { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -1052,7 +1042,8 @@ namespace Barotrauma.Lights if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; - dir = -dir; + cosAngle = -cosAngle; + sinAngle = -sinAngle; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1125,8 +1116,8 @@ namespace Barotrauma.Lights //calculate texture coordinates based on the light's rotation Vector2 originDiff = diff; - diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; - diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; + diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle; + diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle; diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); diff += uvOffset; } @@ -1231,6 +1222,9 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; + float cosAngle = (float)Math.Cos(Rotation); + float sinAngle = -(float)Math.Sin(Rotation); + float bounds = TextureRange; if (OverrideLightTexture != null) @@ -1242,8 +1236,8 @@ namespace Barotrauma.Lights origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; - drawPos.X += origin.X * dir.Y + origin.Y * dir.X; - drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; + drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; + drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1349,7 +1343,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (logicalOperator == PropertyConditional.LogicalOperatorType.And) + if (comparison == PropertyConditional.Comparison.And) { Enabled = conditionals.All(c => c.Matches(conditionalTarget)); } @@ -1405,7 +1399,9 @@ namespace Barotrauma.Lights CalculateLightVertices(verts); LastRecalculationTime = (float)Timing.TotalTime; - NeedsRecalculation = false; + NeedsRecalculation = needsRecalculationWhenUpToDate; + needsRecalculationWhenUpToDate = false; + state = LightVertexState.UpToDate; } } 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 d82698f9d..9f1b8c657 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -58,11 +58,8 @@ namespace Barotrauma convexHulls ??= new List(); var h = new ConvexHull( new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), - IsHorizontal, - this) - { - IsExteriorWall = IsExteriorWall - }; + IsHorizontal, + this); if (Math.Abs(rotation) > 0.001f) { h.Rotate(position, rotation); @@ -501,7 +498,7 @@ namespace Barotrauma private bool ConditionalMatches(PropertyConditional conditional) { - if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) + if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) { return false; } 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 adf9fd222..bf6adf1cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -16,6 +15,10 @@ namespace Barotrauma.Networking { sealed class GameClient : NetworkMember { + public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100); + //this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file + public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150); + public override bool IsClient => true; public override bool IsServer => false; @@ -514,6 +517,7 @@ namespace Barotrauma.Networking DisplayInLoadingScreens = true }; Quit(); + GUI.DisableHUD = false; GameMain.ServerListScreen.Select(); return; } @@ -931,7 +935,7 @@ namespace Barotrauma.Networking ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: {roundInitStatus}." + campaignErrorInfo; + ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -1487,16 +1491,17 @@ namespace Barotrauma.Networking NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) { campaign.PendingSaveID = campaignSaveID; - DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60); + DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut; while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) { if (DateTime.Now > saveFileTimeOut) { GameStarted = true; - DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file)."); + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout")); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; - yield return CoroutineStatus.Failure; + //use success status, even though this is a failure (no need to show a console error because we show it in the message box) + yield return CoroutineStatus.Success; } yield return new WaitForSeconds(0.1f); } @@ -1712,7 +1717,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); } + GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType); ServerSettings.ServerDetailsChanged = true; @@ -2872,12 +2877,14 @@ namespace Barotrauma.Networking ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public bool SpectateClicked(GUIButton button, object userData) + public bool SpectateClicked(GUIButton button, object _) { - MultiPlayerCampaign campaign = + MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; - if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID) + + if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) || + (campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID))) { new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress")); return false; @@ -3063,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/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 35e579d2a..e833fa7eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -111,8 +111,25 @@ namespace Barotrauma.Networking ? NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); + try + { + IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); + ProcessP2PData(inc); + } + catch (Exception e) + { + string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; + GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + } + private void ProcessP2PData(IReadMessage inc) + { var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read(inc); if (!packetHeader.IsServerMessage()) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 5c77d37c7..27f6e1724 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -141,15 +141,31 @@ namespace Barotrauma.Networking if (remotePeer.DisconnectTime != null) { return; } - var peerPacketHeaders = INetSerializableStruct.Read(inc); - - PacketHeader packetHeader = peerPacketHeaders.PacketHeader; + try + { + ProcessP2PData(steamId, remotePeer, inc); + } + catch (Exception e) + { + string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}"; + GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#else + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } +#endif + } + } - if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) + private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc) + { + var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read(inc); + + if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep()) { remotePeer.DisconnectTime = null; - ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); + ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing"); if (initialization == ConnectionInitialization.SteamTicketAndVersion) { remotePeer.Authenticating = true; @@ -181,6 +197,7 @@ namespace Barotrauma.Networking ForwardToServerProcess(outMsg); } + } public override void Update(float deltaTime) 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/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index c23b2b190..f3e1cfdaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -18,7 +18,7 @@ namespace Barotrauma public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } private GUIButton nextButton; - private GUILayoutGroup characterInfoColumns; + private GUIListBox characterInfoColumns; public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) : base(newGameContainer, loadGameContainer) @@ -249,11 +249,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); - characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; + characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true); var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) @@ -306,8 +302,8 @@ namespace Barotrauma for (int i = 0; i < characterInfos.Count; i++) { - var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), - characterInfoColumns.RectTransform)); + var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f), + characterInfoColumns.Content.RectTransform)); var (characterInfo, job) = characterInfos[i]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 6cb2671ea..1cefb2a67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -202,18 +202,21 @@ namespace Barotrauma case CampaignMode.InteractionType.PurchaseSub: submarineSelection?.Update(); break; - case CampaignMode.InteractionType.Crew: CrewManagement?.Update(); break; - case CampaignMode.InteractionType.Store: Store?.Update(deltaTime); - break; - + break; case CampaignMode.InteractionType.MedicalClinic: MedicalClinic?.Update(deltaTime); break; + case CampaignMode.InteractionType.Map: + if (StartButton != null) + { + StartButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && Character.Controlled is { IsIncapacitated: false }; + } + break; } } @@ -568,7 +571,6 @@ namespace Barotrauma StartButton.Visible = false; missionList.Enabled = false; } - //locationInfoPanel?.UpdateAuto(1.0f); } public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 337cdb366..072a2a6d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -435,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 9f1a60b02..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; @@ -1525,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 3ad662591..a9b03c0ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,16 +1,16 @@ using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Xml.Linq; -using Microsoft.Xna.Framework.Input; -using Barotrauma.IO; -using Barotrauma.Steam; namespace Barotrauma { @@ -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(); @@ -3546,10 +3551,46 @@ namespace Barotrauma TextManager.Get("LoadingVanillaSubmarineHeader"), TextManager.Get("LoadingVanillaSubmarineDesc")); - public void LoadSub(SubmarineInfo info) + public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true) { Submarine.Unload(); Submarine selectedSub = null; + + if (checkIdConflicts) + { + Dictionary entities = new Dictionary(); + foreach (var subElement in info.SubmarineElement.Elements()) + { + int id = subElement.GetAttributeInt("ID", -1); + if (id == -1) { continue; } + Identifier identifier = subElement.GetAttributeIdentifier("identifier", string.Empty); + if (entities.TryGetValue(id, out Identifier duplicateEntity)) + { + var errorMsg = new GUIMessageBox( + TextManager.Get("error"), + TextManager.GetWithVariables("subeditor.duplicateiderror", + ("[entity1]", $"{duplicateEntity} ({id})"), + ("[entity2]", $"{identifier} ({id})")), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + errorMsg.Buttons[0].OnClicked = (bnt, userdata) => + { + subElement.Remove(); + LoadSub(info, checkIdConflicts: false); + errorMsg.Close(); + return true; + }; + errorMsg.Buttons[1].OnClicked = (bnt, userdata) => + { + LoadSub(info, checkIdConflicts: false); + errorMsg.Close(); + return true; + }; + return; + } + entities.Add(id, identifier); + } + } + try { selectedSub = new Submarine(info); @@ -5755,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(); @@ -5868,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 ed270c344..0159decbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -163,7 +163,7 @@ namespace Barotrauma.Steam CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh()); } - public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage) + public static async Task CreatePublishStagingCopy(string title, string modVersion, ContentPackage contentPackage) { await Task.Yield(); @@ -184,11 +184,13 @@ namespace Barotrauma.Steam throw new Exception("Staging copy could not be loaded", result.TryUnwrapFailure(out var exception) ? exception : null); } - - //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be + + //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be ModProject modProject = new ModProject(tempPkg) { - ModVersion = modVersion + 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 e3de26b71..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) @@ -478,7 +481,7 @@ namespace Barotrauma.Steam bool stagingReady = false; Exception? stagingException = null; TaskPool.Add("CreatePublishStagingCopy", - SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), + SteamManager.Workshop.CreatePublishStagingCopy(editor.Title ?? localPackage.Name, modVersion, localPackage), (t) => { stagingReady = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs new file mode 100644 index 000000000..5fec7cc05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LeakyBucket.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal class LeakyBucket + { + private readonly Queue queue; + private readonly int capacity; + private readonly float cooldownInSeconds; + private float timer; + + public LeakyBucket(float cooldownInSeconds, int capacity) + { + this.cooldownInSeconds = cooldownInSeconds; + this.capacity = capacity; + queue = new Queue(capacity); + } + + public void Update(float deltaTime) + { + if (timer > 0f) + { + timer -= deltaTime; + return; + } + + if (queue.Count is 0) { return; } + + TryDequeue(); + } + + private void TryDequeue() + { + timer = cooldownInSeconds; + if (queue.TryDequeue(out var action)) + { + action.Invoke(); + } + } + + public bool TryEnqueue(Action item) + { + if (queue.Count >= capacity) { return false; } + queue.Enqueue(item); + return true; + } + } +} \ No newline at end of file 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 0e2ac8220..4b5dccfa1 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index fd6943fa0..6d8354723 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico 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 0e53d121f..cec27c949 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f271ea211..f1b4c157c 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index d884d6e0d..592c084d0 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico 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/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a4c7e8509..0411715e8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -14,7 +14,7 @@ namespace Barotrauma static partial class DebugConsole { private static readonly RateLimiter rateLimiter = new( - maxRequests: 10, + maxRequests: 50, expiryInSeconds: 5, punishmentRules: new[] { @@ -2526,14 +2526,14 @@ namespace Barotrauma GameMain.Server.CreateEntityEvent(wall); } })); - commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that stalls each file transfer packet by the specified duration.", (string[] args) => + commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that makes all file transfers take at least the specified duration.", (string[] args) => { float seconds = 0.0f; if (args.Length > 0) { float.TryParse(args[0], out seconds); } - GameMain.Server.FileSender.StallPacketsTime = seconds; + GameMain.Server.FileSender.ForceMinimumFileTransferDuration = seconds; NewMessage("Set file transfer stall time to " + seconds); })); #endif diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index d2a706459..255635fb7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; using Barotrauma.Extensions; using Barotrauma.Networking; @@ -11,10 +12,10 @@ namespace Barotrauma { internal partial class MedicalClinic { - // allow 10 requests per 5 seconds, announce to chat if the limit is reached + // allow 20 requests per 5 seconds, announce to chat if the limit is reached private readonly RateLimiter rateLimiter = new( - maxRequests: 10, - expiryInSeconds: 5, + maxRequests: RateLimitMaxRequests, + expiryInSeconds: RateLimitExpiry, punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); @@ -126,6 +127,17 @@ namespace Barotrauma ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; + if (foundInfo is null) + { + StringBuilder sb = new(); + foreach (CharacterInfo character in GetCrewCharacters()) + { + sb.AppendLine($" - {character.DisplayName} ({character.ID})"); + } + + DebugConsole.ThrowError($"Could not find the requested crew member with ID {crewMember.CharacterInfoID}.\n{sb}"); + } + if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); 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/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 50e6146bb..84c67ceae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -184,6 +184,8 @@ namespace Barotrauma int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); + if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; } + TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); @@ -198,6 +200,8 @@ namespace Barotrauma TryPutItem(it, otherHandSlot, true, true, character, false); } } + + bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length; } public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index eae96b706..9b8ab528d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -108,9 +108,7 @@ namespace Barotrauma.Networking const int MaxTransferCount = 16; const int MaxTransferCountPerRecipient = 5; - - public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0); - + public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver); public FileTransferDelegate OnStarted; public FileTransferDelegate OnEnded; @@ -121,8 +119,9 @@ namespace Barotrauma.Networking private readonly ServerPeer peer; + public static DateTime StartTime; #if DEBUG - public float StallPacketsTime { get; set; } + public float ForceMinimumFileTransferDuration { get; set; } #endif public IReadOnlyList ActiveTransfers => activeTransfers; @@ -172,6 +171,8 @@ namespace Barotrauma.Networking return null; } + StartTime = DateTime.Now; + OnStarted(transfer); GameMain.Server.LastClientListUpdateID++; @@ -259,7 +260,18 @@ namespace Barotrauma.Networking for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) { long remaining = transfer.Data.Length - transfer.SentOffset; - int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); +#if DEBUG + bool stalling = false; + float elapsedTime = (float)(DateTime.Now - StartTime).TotalSeconds; + if (elapsedTime < ForceMinimumFileTransferDuration) + { + int remainingChunks = (int)Math.Max(remaining / chunkLen, 1); + transfer.WaitTimer = + Math.Max(transfer.WaitTimer, (ForceMinimumFileTransferDuration - elapsedTime) / remainingChunks); + if (remainingChunks <= 1) { break; } + } +#endif + int sendByteCount = remaining > chunkLen ? chunkLen : (int)remaining; message = new WriteOnlyMessage(); message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); @@ -293,11 +305,10 @@ namespace Barotrauma.Networking //this gets reset when packet loss or disorder sets in transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, transfer.PacketsPerUpdate + 0.05f); - } - #if DEBUG - transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); + if (stalling) { break; } #endif + } } catch (Exception e) @@ -330,7 +341,7 @@ namespace Barotrauma.Networking { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); - if (matchingTransfer != null) CancelTransfer(matchingTransfer); + if (matchingTransfer != null) { CancelTransfer(matchingTransfer); } return; } else if (messageType == FileTransferMessageType.Data) @@ -359,6 +370,7 @@ namespace Barotrauma.Networking if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) { matchingTransfer.Status = FileTransferStatus.Finished; + DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}"); } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index cb1f3d105..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); @@ -775,9 +771,12 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { - ServerSettings.CampaignSettings = settings; - ServerSettings.SaveSettings(); - MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); + using (dosProtection.Pause(connectedClient)) + { + ServerSettings.CampaignSettings = settings; + ServerSettings.SaveSettings(); + MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); + } } } } @@ -790,8 +789,11 @@ namespace Barotrauma.Networking break; } if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) - { - MultiPlayerCampaign.LoadCampaign(saveName); + { + using (dosProtection.Pause(connectedClient)) + { + MultiPlayerCampaign.LoadCampaign(saveName); + } } } break; @@ -1661,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) @@ -2594,16 +2612,20 @@ namespace Barotrauma.Networking { if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } - const int MaxSaves = 255; - var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); - IWriteMessage msg = new WriteOnlyMessage(); - msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); - for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + using (dosProtection.Pause(client)) { - msg.WriteNetSerializableStruct(saveInfos[i]); + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); + IWriteMessage msg = new WriteOnlyMessage(); + msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); + msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) + { + msg.WriteNetSerializableStruct(saveInfos[i]); + } + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + return true; } @@ -3084,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) @@ -3139,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 { @@ -3173,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 { @@ -3396,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/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index b8e4393bf..143ac4dae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -24,11 +24,28 @@ namespace Barotrauma.Networking } protected readonly Callbacks callbacks; + private readonly ImmutableArray contentPackages; protected ServerPeer(Callbacks callbacks) { this.callbacks = callbacks; - } + + List contentPackageList = new List(); + foreach (var cp in ContentPackageManager.EnabledPackages.All) + { + if (!cp.Files.Any()) { continue; } + if (!cp.HasMultiplayerSyncedContent && !cp.Files.All(f => f is SubmarineFile)) { continue; } + if (cp.UgcId.TryUnwrap(out var id1) && + contentPackageList.FirstOrDefault(cp => cp.UgcId.TryUnwrap(out var id2) && id1.Equals(id2)) is ContentPackage existingPackage) + { + //there can be multiple enabled mods with the same UgcId if the player has e.g. created a local copy of a workshop mod + DebugConsole.AddWarning($"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package."); + continue; + } + contentPackageList.Add(cp); + } + contentPackages = contentPackageList.ToImmutableArray(); + } public abstract void InitializeSteamServerCallbacks(); @@ -250,9 +267,7 @@ namespace Barotrauma.Networking structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, - ContentPackages = ContentPackageManager.EnabledPackages.All - .Where(cp => cp.Files.Any()) - .Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) + ContentPackages = contentPackages .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .ToImmutableArray() }; 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/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index ba77838e6..f8ce8c1d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -20,19 +20,6 @@ namespace Barotrauma } public delegate void MessageSender(string message); - public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender) - { - string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, - ("[codewords]", codeWords), - ("[coderesponse]", codeResponse)); - messageSender(greetingMessage); - Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character); - Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection); - if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null) - { - GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox); - } - } public void SendChatMessage(string serverText, Identifier iconIdentifier) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index d2397ec46..cbaeabea5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -224,10 +224,6 @@ namespace Barotrauma { pendingMessages.Add(traitor, new List()); } - foreach (var traitor in Traitors.Values) - { - traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message)); - } pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); 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 2370283c3..4c66ef099 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.4.0 - Copyright © FakeFish 2018-2022 + 1.0.20.1 + Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 131be5ec0..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 @@ -107,12 +104,26 @@ namespace Barotrauma } } - public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) => - steeringManager is IndoorsSteeringManager pathSteering && - pathSteering.CurrentPath != null && - (!requireUnfinished || !pathSteering.CurrentPath.Finished) && - !pathSteering.CurrentPath.Unreachable && - (!requireNonDirty || !pathSteering.IsPathDirty); + /// + /// Is the current path valid, using the provided parameters. + /// + /// + /// + /// + /// When is defined, returns false if any of the nodes fails to match the predicate. + public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func nodePredicate = null) + { + if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; } + if (pathSteering.CurrentPath == null) { return false; } + if (pathSteering.CurrentPath.Unreachable) { return false; } + if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; } + if (requireNonDirty && pathSteering.IsPathDirty) { return false; } + if (nodePredicate != null) + { + return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n)); + } + return true; + } public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null; public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable; @@ -251,7 +262,7 @@ namespace Barotrauma } private readonly HashSet unequippedItems = new HashSet(); - public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) + public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable targetTags = null) { var pickable = item.GetComponent(); if (pickable == null) { return false; } @@ -265,23 +276,28 @@ namespace Barotrauma } else { - var holdable = item.GetComponent(); - if (holdable != null) - { - pickable = holdable; - } + // Not allowed to wear -> don't use the Wearable component even when it's found. + pickable = item.GetComponent(); } if (item.ParentInventory is ItemInventory itemInventory) { if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } } - if (equip) + if (equip && pickable != null) { int targetSlot = -1; //check if all the slots required by the item are free foreach (InvSlotType slots in pickable.AllowedSlots) { if (slots.HasFlag(InvSlotType.Any)) { continue; } + if (!wear) + { + if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand)) + { + // Don't allow other than hand slots if not allowed to wear. + continue; + } + } for (int i = 0; i < targetInventory.Capacity; i++) { if (targetInventory is CharacterInventory characterInventory) @@ -294,7 +310,7 @@ namespace Barotrauma var otherItem = targetInventory.GetItemAt(i); if (otherItem == null) { continue; } //try to move the existing item to LimbSlot.Any and continue if successful - if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) + if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot)) { if (storeUnequipped && targetInventory.Owner == Character) { @@ -304,6 +320,11 @@ namespace Barotrauma } if (dropOtherIfCannotMove) { + if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags)) + { + // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory. + return false; + } //if everything else fails, simply drop the existing item otherItem.Drop(Character); } @@ -314,7 +335,7 @@ namespace Barotrauma } else { - return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); + return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot); } } @@ -339,7 +360,7 @@ namespace Barotrauma if (avoidDroppingInSea && !character.IsInFriendlySub) { // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it. - if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot)) { if (unequipMax.HasValue && ++removed >= unequipMax) { return; } continue; @@ -401,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 @@ -449,9 +465,10 @@ 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 || IsCurrentPathNullOrUnreachable || 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(); SteeringManager.Reset(); pathSteering?.ResetPath(); Vector2 dir = Vector2.Normalize(diff); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 24eebf8fd..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"; } @@ -480,7 +486,7 @@ namespace Barotrauma if (SelectedAiTarget?.Entity != null || EscapeTarget != null) { Entity t = SelectedAiTarget?.Entity ?? EscapeTarget; - float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; + float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; } else @@ -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; @@ -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; @@ -3934,7 +3957,7 @@ namespace Barotrauma { SteerAwayFromTheEnemy(); } - else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true)) + else if (canAttackDoors && HasValidPath()) { var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 9330350dc..890772d58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -10,7 +10,7 @@ namespace Barotrauma { partial class HumanAIController : AIController { - public static bool debugai; + public static bool DebugAI; public static bool DisableCrewAI; private readonly AIObjectiveManager objectiveManager; @@ -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; @@ -55,6 +57,7 @@ namespace Barotrauma private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5; private float obstacleRaycastTimer; + private bool isBlocked; private readonly float enemyCheckInterval = 0.2f; private readonly float enemySpotDistanceOutside = 800; @@ -92,7 +95,10 @@ namespace Barotrauma private readonly SteeringManager outsideSteering, insideSteering; - public bool UseIndoorSteeringOutside { get; set; } = false; + /// + /// Waypoints that are not linked to a sub (e.g. main path). + /// + public bool UseOutsideWaypoints { get; private set; } public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; @@ -225,14 +231,15 @@ namespace Barotrauma IgnoredItems.Clear(); } - bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true) + // Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null') + bool IsCloseEnoughToTarget(float threshold, bool targetSub = true) { Entity target = SelectedAiTarget?.Entity; if (target == null) { return false; } - if (useTargetSub) + if (targetSub) { if (target.Submarine is Submarine sub) { @@ -244,62 +251,71 @@ namespace Barotrauma return false; } } - return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2); + return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold); } - bool hasValidPath = HasValidPath(); - - if (Character.Submarine == null) + bool isOutside = Character.Submarine == null; + if (isOutside) { obstacleRaycastTimer -= deltaTime; if (obstacleRaycastTimer <= 0) { + bool hasValidPath = HasValidPath(); + isBlocked = false; + UseOutsideWaypoints = false; obstacleRaycastTimer = obstacleRaycastIntervalLong; - if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false)) + ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective()?.Target; + if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false))) { // If the target is behind a level wall, switch to the pathing to get around the obstacles. - ISpatialEntity spatialTarget = SelectedAiTarget?.Entity; - if (spatialTarget == null) + IEnumerable ignoredBodies = null; + Vector2 rayEnd = spatialTarget.SimPosition; + Submarine targetSub = spatialTarget.Submarine; + if (targetSub != null) { - var gotoObjective = ObjectiveManager.GetActiveObjective(); - spatialTarget = gotoObjective?.Target; + rayEnd += targetSub.SimPosition; + ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable(); } - if (spatialTarget == null) + var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + isBlocked = obstacle != null; + // Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead. + UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin); + bool resetPath = false; + if (UseOutsideWaypoints) { - UseIndoorSteeringOutside = false; + bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null); + if (isUsingInsideWaypoints) + { + resetPath = true; + } } else { - IEnumerable ignoredBodies = null; - Vector2 rayEnd = spatialTarget.SimPosition; - Submarine targetSub = spatialTarget.Submarine; - if (targetSub != null) + bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null); + if (isUsingOutsideWaypoints) { - rayEnd += targetSub.SimPosition; - ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable(); + resetPath = true; } - var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); - UseIndoorSteeringOutside = obstacle != null; + } + if (resetPath) + { + PathSteering.ResetPath(); } } - else + else if (hasValidPath) { - UseIndoorSteeringOutside = false; - if (hasValidPath) + obstacleRaycastTimer = obstacleRaycastIntervalShort; + // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - obstacleRaycastTimer = obstacleRaycastIntervalShort; - // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) - { - PathSteering.CurrentPath.Unreachable = true; - break; - } + PathSteering.CurrentPath.Unreachable = true; + break; } } } @@ -307,10 +323,11 @@ namespace Barotrauma } else { - UseIndoorSteeringOutside = false; + UseOutsideWaypoints = false; + isBlocked = false; } - if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) + if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) { // Spot enemies while staying outside or inside an enemy ship. // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior @@ -352,12 +369,13 @@ namespace Barotrauma } } } - - if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) + bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer); + if (useInsideSteering) { if (steeringManager != insideSteering) { insideSteering.Reset(); + PathSteering.ResetPath(); steeringManager = insideSteering; } if (IsCloseEnoughToTarget(maxSteeringBuffer)) @@ -420,6 +438,7 @@ namespace Barotrauma foreach (Hull h in VisibleHulls) { PropagateHullSafety(Character, h); + dirtyHullSafetyCalculations.Remove(h); } } else @@ -427,18 +446,33 @@ 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) { ReportProblems(); + + } + else + { + // Allows bots to heal targets autonomously while swimming outside of the sub. + if (AIObjectiveRescueAll.IsValidTarget(Character, Character)) + { + AddTargets(Character, Character); + } } reportProblemsTimer = reportProblemsInterval; } - UpdateSpeaking(); + SpeakAboutIssues(); UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } @@ -590,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)) { @@ -875,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)) { @@ -906,20 +940,40 @@ namespace Barotrauma } })) { - suitableContainer = targetContainer; - return true; + if (targetContainer != null && + character.AIController is HumanAIController humanAI && + humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable) + { + ignoredItems.Add(targetContainer); + itemIndex = 0; + return false; + } + else + { + suitableContainer = targetContainer; + return true; + } } return false; } private float draggedTimer; private float refuseDraggingTimer; - private const float RefuseDraggingAfter = 10.0f; + /// + /// The bot breaks free if being dragged by a human player from another team for longer than this + /// + private const float RefuseDraggingThresholdHigh = 10.0f; + /// + /// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster + /// + private const float RefuseDraggingThresholdLow = 0.5f; private const float RefuseDraggingDuration = 30.0f; private void UpdateDragged(float deltaTime) { if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; } + if (Character.IsEscorted) { return; } + if (Character.LockHands) { return; } //don't allow player characters who aren't in the same team to drag us for more than x seconds if (Character.SelectedBy == null || @@ -931,8 +985,8 @@ namespace Barotrauma } draggedTimer += deltaTime; - if (draggedTimer > RefuseDraggingAfter || - (draggedTimer > 0.5f && refuseDraggingTimer > 0.0f)) + if (draggedTimer > RefuseDraggingThresholdHigh || + (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow)) { draggedTimer = 0.0f; refuseDraggingTimer = RefuseDraggingDuration; @@ -945,7 +999,8 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; - bool speak = Character.SpeechImpediment < 100; + // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented + bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -1044,25 +1099,21 @@ namespace Barotrauma } if (newOrder != null && speak) { - // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented - if (!Character.IsEscorted) + if (Character.TeamID == CharacterTeamType.FriendlyNPC) { - if (Character.TeamID == CharacterTeamType.FriendlyNPC) - { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, - identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), - minDurationBetweenSimilar: 60.0f); - } - else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) - { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, + identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), + minDurationBetweenSimilar: 60.0f); + } + else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) + { + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder - .WithManualPriority(CharacterInfo.HighestManualOrderPriority) - .WithTargetEntity(targetHull) - .WithOrderGiver(Character), "", null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder + .WithManualPriority(CharacterInfo.HighestManualOrderPriority) + .WithTargetEntity(targetHull) + .WithOrderGiver(Character), "", null, Character)); #endif - } } } } @@ -1093,21 +1144,33 @@ namespace Barotrauma } } - private void UpdateSpeaking() + private void SpeakAboutIssues() { if (!Character.IsOnPlayerTeam) { return; } if (Character.SpeechImpediment >= 100) { return; } - if (Character.Oxygen < 20.0f) + float minDelay = 0.5f, maxDelay = 2f; + if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold) { - Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); + 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) + if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic) { - Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); + string msgId = "DialogBleeding"; + Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) + if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure) { - Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); + if (Character.PressureProtection > 0) + { + string msgId = "DialogInsufficientPressureProtection"; + Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); + } + else if (Character.CurrentHull?.DisplayName != null) + { + string msgId = "DialogPressure"; + Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); + } } } @@ -1236,7 +1299,7 @@ namespace Barotrauma bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; if (isAccidental) { - if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) + if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)) { AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } @@ -1402,7 +1465,7 @@ namespace Barotrauma } else { - if (humanAI.ObjectiveManager.GetActiveObjective()?.Enemy == attacker) + if (humanAI.ObjectiveManager.GetLastActiveObjective()?.Enemy == attacker) { // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; @@ -1587,7 +1650,7 @@ namespace Barotrauma hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.IsProtectedFromPressure; + needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure; return needsAir || needsSuit; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) @@ -1604,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. @@ -1837,18 +1900,22 @@ 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. /// public static void PropagateHullSafety(Character character, Hull hull) { - DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull)); + 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); } @@ -1916,7 +1983,7 @@ namespace Barotrauma private static bool AddTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { bool targetAdded = false; - DoForEachCrewMember(caller, humanAI => + DoForEachBot(caller, humanAI => { if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } var objective = humanAI.ObjectiveManager.GetObjective(); @@ -1933,7 +2000,7 @@ namespace Barotrauma public static void RemoveTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { - DoForEachCrewMember(caller, humanAI => + DoForEachBot(caller, humanAI => humanAI.ObjectiveManager.GetObjective()?.ReportedTargets.Remove(target)); } @@ -2108,7 +2175,7 @@ namespace Barotrauma if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. - if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + return sameTeam || me.TeamID != CharacterTeamType.None; } else { @@ -2120,9 +2187,11 @@ namespace Barotrauma (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) { Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; + //NPCs that allow some campaign interaction are not turned hostile by low reputation if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } - if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI) + + if (npc.AIController is HumanAIController npcAI) { return !npcAI.IsInHostileFaction(); } @@ -2134,6 +2203,7 @@ namespace Barotrauma public bool IsInHostileFaction() { if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; } + if (Character.IsEscorted) { return false; } Identifier npcFaction = Character.Faction; Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; @@ -2145,8 +2215,7 @@ namespace Barotrauma } if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + if (campaign.CurrentLocation is { IsFactionHostile: true }) { return true; } @@ -2154,71 +2223,89 @@ namespace Barotrauma return false; } - public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; + public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious; - public static bool IsTrueForAllCrewMembers(Character character, Func predicate) + public static bool IsTrueForAllBotsInTheCrew(Character character, Func predicate) { if (character == null) { return false; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c)) + if (!IsBotInTheCrew(character, c)) { continue; } + if (!predicate(c.AIController as HumanAIController)) { - if (!predicate(c.AIController as HumanAIController)) - { - return false; - } + return false; } - } + } return true; } - public static bool IsTrueForAnyCrewMember(Character character, Func predicate) + public static bool IsTrueForAnyBotInTheCrew(Character character, Func predicate) { if (character == null) { return false; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c)) + if (!IsBotInTheCrew(character, c)) { continue; } + if (predicate(c.AIController as HumanAIController)) { - if (predicate(c.AIController as HumanAIController)) - { - return true; - } + return true; } } return false; } - public static int CountCrew(Character character, Func predicate = null, bool onlyActive = true, bool onlyBots = false) + public static int CountBotsInTheCrew(Character character, Func predicate = null) { if (character == null) { return 0; } int count = 0; foreach (var other in Character.CharacterList) { - if (onlyActive && !IsActive(other)) + if (!IsBotInTheCrew(character, other)) { continue; } + if (predicate == null || predicate(other.AIController as HumanAIController)) { - continue; - } - if (onlyBots && other.IsPlayer) - { - continue; - } - if (FilterCrewMember(character, other)) - { - if (predicate == null || predicate(other.AIController as HumanAIController)) - { - count++; - } + count++; } } return count; } - public static void DoForEachCrewMember(Character character, Action action, float range = float.PositiveInfinity) + /// + /// Including the player characters in the same team. + /// + public bool IsTrueForAnyCrewMember(Func predicate, bool onlyActive = true, bool onlyConnectedSubs = false) + { + foreach (var c in Character.CharacterList) + { + if (!IsActive(c)) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (onlyActive && c.IsIncapacitated) { continue; } + if (onlyConnectedSubs) + { + if (Character.Submarine == null) + { + if (c.Submarine != null) + { + return false; + } + } + else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine)) + { + return false; + } + } + if (predicate(c)) + { + return true; + } + } + return false; + } + + private static void DoForEachBot(Character character, Action action, float range = float.PositiveInfinity) { if (character == null) { return; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) + if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range)) { action(c.AIController as HumanAIController); } @@ -2238,7 +2325,7 @@ namespace Barotrauma } } - private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); + private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController; public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) { @@ -2283,10 +2370,9 @@ namespace Barotrauma bool isOrder = IsOrderedToOperateThis(Character.AIController); foreach (Character c in Character.CharacterList) { + if (!IsActive(c)) { continue; } if (c == Character) { continue; } - if (c.Removed) { continue; } if (c.TeamID != Character.TeamID) { continue; } - if (c.IsIncapacitated) { continue; } if (c.IsPlayer) { if (c.SelectedItem == target.Item) @@ -2354,9 +2440,9 @@ namespace Barotrauma bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); foreach (var c in Character.CharacterList) { + if (!IsActive(c)) { continue; } if (c == Character) { continue; } if (c.TeamID != Character.TeamID) { continue; } - if (c.IsIncapacitated) { continue; } other = c; if (c.IsPlayer) { @@ -2370,7 +2456,7 @@ namespace Barotrauma { var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective(); if (repairItemsObjective == null) { continue; } - if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target) + if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target) { // Not targeting the same item. continue; @@ -2405,11 +2491,10 @@ namespace Barotrauma } #region Wrappers - public bool IsFriendly(Character other) => IsFriendly(Character, other); - public void DoForEachCrewMember(Action action) => DoForEachCrewMember(Character, action); - public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); - public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); - public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); + public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam); + public bool IsTrueForAnyBotInTheCrew(Func predicate) => IsTrueForAnyBotInTheCrew(Character, predicate); + public bool IsTrueForAllBotsInTheCrew(Func predicate) => IsTrueForAllBotsInTheCrew(Character, predicate); + public int CountBotsInTheCrew(Func predicate = null) => CountBotsInTheCrew(Character, predicate); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index a98233d5d..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,66 +126,28 @@ 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) { - bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished; + 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,15 +157,33 @@ namespace Barotrauma if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; + if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp) + { + 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 && 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) + float maxDist = 200; + if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist) + { + needsNewPath = true; + } + } + } 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; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); - bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; + bool useNewPath = needsNewPath; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). @@ -234,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() { @@ -312,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; } @@ -328,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()); @@ -353,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)) @@ -380,56 +391,76 @@ namespace Barotrauma } } } - var collider = character.AnimController.Collider; - if (character.IsClimbing && !useLadders) - { - character.StopClimbing(); - } if (character.IsClimbing && useLadders) { - bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; - if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) + if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item) { - //climbing ladders -> don't move horizontally - diff.X = 0.0f; + // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. + NextNode(!doorsChecked); } - //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) + else { - 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)) + bool nextLadderSameAsCurrent = currentLadder == nextLadder; + if (currentLadder != null && nextLadder != null) { - character.StopClimbing(); + //climbing ladders -> don't move horizontally + diff.X = 0.0f; } - else if (nextLadder != null && !nextLadderSameAsCurrent) + //at the same height as the waypoint + float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); + float colliderHeight = collider.Height / 2 + collider.Radius; + float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); + if (heightDiff < colliderHeight * 1.25f) { - // Try to change the ladder (hatches between two submarines) - if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) + if (nextLadder != null && !nextLadderSameAsCurrent) { - if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) + // Try to change the ladder (hatches between two submarines) + if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) + { + if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) + { + NextNode(!doorsChecked); + } + } + } + bool isAboveFloor; + if (diff.Y < 0) + { + // 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(); + } } } - if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)) + else if (currentLadder != null && currentPath.NextNode != null) { - NextNode(!doorsChecked); - } - } - else if (nextLadder != null) - { - //if the current node is below the character and the next one is above (or vice versa) - //and both are on ladders, we can skip directly to the next one - //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above - if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) - { - NextNode(!doorsChecked); + if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) + { + //if the current node is below the character and the next one is above (or vice versa) + //and both are on ladders, we can skip directly to the next one + //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above + NextNode(!doorsChecked); + } } } return ConvertUnits.ToSimUnits(diff); @@ -440,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); @@ -459,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. @@ -486,9 +515,12 @@ namespace Barotrauma } } float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (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) @@ -507,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; } @@ -605,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 3a16cf84d..dd3b79d0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -506,15 +506,31 @@ namespace Barotrauma } } - protected static bool CanEquip(Character character, Item item) + public virtual void SpeakAfterOrderReceived() { } + + protected static bool CanEquip(Character character, Item item, bool allowWearing) { - bool canEquip = item != null; - if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any)) + if (item == null) { return false; } + bool canEquip = false; + if (item.AllowedSlots.Contains(InvSlotType.Any)) + { + if (character.Inventory.IsAnySlotAvailable(item)) + { + canEquip = true; + } + } + if (!canEquip) { - canEquip = false; var inv = character.Inventory; foreach (var allowedSlot in item.AllowedSlots) { + if (!allowWearing) + { + if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand)) + { + continue; + } + } foreach (var slotType in inv.SlotTypes) { if (!allowedSlot.HasFlag(slotType)) { continue; } @@ -530,18 +546,9 @@ namespace Barotrauma } } } - return canEquip; - } - protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet identifiersOrTags) - { - if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; } - foreach (var identifier in identifiersOrTags) - { - if (item.HasTag(identifier)) { return true; } - } - return false; + return canEquip && character.Inventory.CanBePut(item); } - protected bool CanEquip(Item item) => CanEquip(character, item); + protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index dfc2efe6d..d9c0c85ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -64,7 +64,6 @@ namespace Barotrauma if (subObjectives.Any()) { return; } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { - itemIndex = 0; if (suitableContainer != null) { bool equip = item.GetComponent() != null || @@ -112,10 +111,7 @@ namespace Barotrauma Abandon = true; } } - else - { - objectiveManager.GetObjective().Wander(deltaTime); - } + objectiveManager.GetObjective().Wander(deltaTime); } protected override bool CheckObjectiveSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 1b2f390e2..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(); @@ -121,7 +125,7 @@ namespace Barotrauma { return true; } - return CanEquip(character, item); + return CanEquip(character, item, allowWearing: false); } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 066b2b9a1..a1b25992a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -170,13 +170,33 @@ namespace Barotrauma return Priority; } } - float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f); - Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100); - if (Priority > 0) + if (TargetEliminated) { - if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) + Priority = 0; + } + else + { + // 91-100 + float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; + float maxPriority = AIObjectiveManager.MaxObjectivePriority; + float priorityScale = maxPriority - minPriority; + float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) { - Priority = 0; + xDist /= 2; + yDist /= 2; + } + float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5); + float devotion = CumulatedDevotion / 100; + float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1)); + Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority); + if (Priority > 0) + { + if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) + { + Priority = 0; + } } } return Priority; @@ -312,12 +332,10 @@ namespace Barotrauma } else { - AskHelp(); Retreat(deltaTime); } break; case CombatMode.Retreat: - AskHelp(); Retreat(deltaTime); break; default: @@ -352,7 +370,7 @@ namespace Barotrauma Weapon = null; continue; } - if (WeaponComponent.IsNotEmpty(character)) + if (!WeaponComponent.IsEmpty(character)) { // All good, the weapon is loaded break; @@ -470,7 +488,7 @@ namespace Barotrauma // Not in the inventory anymore or cannot find the weapon component return false; } - if (!WeaponComponent.IsNotEmpty(character)) + if (WeaponComponent.IsEmpty(character)) { // Try reloading (and seek ammo) if (!Reload(seekAmmo)) @@ -541,7 +559,7 @@ namespace Barotrauma priority /= 2; } } - if (!weapon.IsNotEmpty(character)) + if (weapon.IsEmpty(character)) { if (weapon is RangedWeapon && !isAllowedToSeekWeapons) { @@ -554,7 +572,6 @@ namespace Barotrauma priority /= 2; } } - if (Enemy.Params.Health.StunImmunity) { if (weapon.Item.HasTag("stunner")) @@ -750,7 +767,7 @@ namespace Barotrauma private bool Equip() { if (character.LockHands) { return false; } - if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) + if (WeaponComponent.IsEmpty(character)) { return false; } @@ -783,6 +800,10 @@ namespace Barotrauma private void Retreat(float deltaTime) { + if (!Enemy.IsHuman) + { + SpeakRetreating(); + } RemoveFollowTarget(); RemoveSubObjective(ref seekAmmunitionObjective); if (retreatObjective != null && retreatObjective.Target != retreatTarget) @@ -793,6 +814,7 @@ namespace Barotrauma { // Swim away SteeringManager.Reset(); + character.ReleaseSecondaryItem(); SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition)); SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); return; @@ -819,7 +841,8 @@ namespace Barotrauma { TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager) { - UsePathingOutside = false + UsePathingOutside = false, + SpeakIfFails = false }, onAbandon: () => { @@ -861,6 +884,7 @@ namespace Barotrauma { if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range)) { + character.ReleaseSecondaryItem(); // Swim towards the target SteeringManager.Reset(); SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10); @@ -882,7 +906,8 @@ namespace Barotrauma UsePathingOutside = false, IgnoreIfTargetDead = true, TargetName = Enemy.DisplayName, - AlwaysUseEuclideanDistance = false + AlwaysUseEuclideanDistance = false, + SpeakIfFails = false }, onAbandon: () => { @@ -966,14 +991,22 @@ namespace Barotrauma item.GetComponent() != null) { item.Drop(character); - character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot); + character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot); } } } - 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); @@ -1028,54 +1061,43 @@ namespace Barotrauma if (Weapon.OwnInventory == null) { return true; } // Eject empty ammo HumanAIController.UnequipEmptyItems(Weapon); - RelatedItem item = null; - Item ammunition = null; ImmutableHashSet ammunitionIdentifiers = null; if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) { - ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); - if (ammunition != null) - { - // Ammunition still remaining - return true; - } - item = requiredItem; + if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; } ammunitionIdentifiers = requiredItem.Identifiers; + break; } } else if (WeaponComponent is MeleeWeapon meleeWeapon) { ammunitionIdentifiers = meleeWeapon.PreferredContainedItems; } - // No ammo - if (ammunition == null) + if (ammunitionIdentifiers != null) { - if (ammunitionIdentifiers != null) + // Try reload ammunition from inventory + static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); + if (ammunition != null) { - // Try reload ammunition from inventory - static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); - ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); - if (ammunition != null) + var container = Weapon.GetComponent(); + if (!container.Inventory.TryPutItem(ammunition, user: character)) { - var container = Weapon.GetComponent(); - if (!container.Inventory.TryPutItem(ammunition, null)) + if (ammunition.ParentInventory == character.Inventory) { - if (ammunition.ParentInventory == character.Inventory) - { - ammunition.Drop(character); - } + ammunition.Drop(character); } } } } - if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) + if (!WeaponComponent.IsEmpty(character)) { return true; } - else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) + else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) { SeekAmmunition(ammunitionIdentifiers); } @@ -1270,7 +1292,7 @@ namespace Barotrauma } private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); + private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); private void Speak(Identifier textIdentifier, float delay, float minDuration) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 4c1d874d2..244b66a89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -109,7 +109,7 @@ namespace Barotrauma private bool CheckItem(Item item) { - return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); + return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); } protected override void Act(float deltaTime) @@ -156,15 +156,15 @@ namespace Barotrauma Inventory originalInventory = ItemToContain.ParentInventory; var slots = originalInventory?.FindIndices(ItemToContain); - static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) + bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) { if (targetSlot.HasValue) { - return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null); + return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character); } else { - return inventory.TryPutItem(itemToContain, user: null); + return inventory.TryPutItem(itemToContain, user: character); } } @@ -198,11 +198,11 @@ 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(), - endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index aee20f6ef..cec282b90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -66,7 +66,7 @@ namespace Barotrauma else { float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } } return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 7bbe5e18b..a81367582 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -28,7 +28,7 @@ namespace Barotrauma if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. - return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; + return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100; } protected override AIObjective ObjectiveConstructor(Character target) @@ -37,8 +37,7 @@ namespace Barotrauma var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + if (campaign.CurrentLocation is { IsFactionHostile: true }) { combatObjective.holdFireCondition = () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index a87e5cd61..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 }; }, @@ -79,7 +80,7 @@ namespace Barotrauma { if (mask != targetItem) { - character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot); + character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 221067c28..a35ec388d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -52,17 +52,14 @@ namespace Barotrauma objectiveManager.HasOrder(o => o.Priority > 0) || objectiveManager.HasActiveObjective() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; + && ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10; } else { if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || - (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && - (needsSuit ? - !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : - !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))) + NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) { - Priority = 100; + Priority = AIObjectiveManager.MaxObjectivePriority; } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) @@ -75,11 +72,11 @@ namespace Barotrauma { Priority = 0; } - Priority = MathHelper.Clamp(Priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority); if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { // Boost the priority while seeking the diving gear - Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100)); + Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority)); } } return Priority; @@ -111,7 +108,7 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; - if (currenthullSafety >= 100) + if (currenthullSafety >= 100 && !character.IsLowInOxygen) { // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. Priority = 0; @@ -122,7 +119,7 @@ namespace Barotrauma float dangerFactor = (100 - currenthullSafety) / 100; Priority += dangerFactor * priorityIncrease * deltaTime; } - Priority = MathHelper.Clamp(Priority, 0, 100); + Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority); } } @@ -138,7 +135,7 @@ namespace Barotrauma { if (resetPriority) { return; } var currentHull = character.CurrentHull; - bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); + bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure; bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { @@ -200,16 +197,11 @@ namespace Barotrauma UpdateSimpleEscape(deltaTime); return; } - searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; currentSafeHull = potentialSafeHull; - - cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); - if (currentSafeHull == null) - { - currentSafeHull = previousSafeHull; - } + cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull); + currentSafeHull ??= previousSafeHull; if (currentSafeHull != null && currentSafeHull != currentHull) { if (goToObjective?.Target != currentSafeHull) @@ -219,6 +211,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { + SpeakIfFails = false, AllowGoingOutside = character.IsProtectedFromPressure || character.CurrentHull == null || @@ -300,6 +293,7 @@ namespace Barotrauma //only move if we haven't reached the edge of the room if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) { + character.ReleaseSecondaryItem(); character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); } else @@ -349,7 +343,6 @@ namespace Barotrauma if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; } - //sort the hulls based on distance and which sub they're in //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive @@ -413,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; @@ -493,5 +486,18 @@ namespace Barotrauma cannotFindDivingGear = false; cannotFindSafeHull = false; } + + private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0) + { + if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; } + if (needsSuit) + { + return !HumanAIController.HasDivingSuit(character, minOxygen); + } + else + { + return !HumanAIController.HasDivingGear(character, minOxygen); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index dd0b1e20b..f08a264c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -37,40 +37,56 @@ namespace Barotrauma { Priority = 0; Abandon = true; + return Priority; } - else if (HumanAIController.IsTrueForAnyCrewMember( - other => other != HumanAIController && - other.Character.IsBot && - other.ObjectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && - fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak))) + float coopMultiplier = 1; + foreach (var c in Character.CharacterList) { - Priority = 0; + if (!HumanAIController.IsActive(c)) { continue; } + if (c.TeamID != character.TeamID) { continue; } + if (c == character) { continue; } + if (c.IsPlayer) { continue; } + if (c.AIController is HumanAIController otherAI ) + { + if (otherAI.ObjectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeak fixLeak) + { + if (fixLeak.Leak == Leak) + { + // Ignore leaks that others are already targeting + Priority = 0; + return Priority; + } + if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull) + { + // Reduce the priority of leaks that others should be targeting + coopMultiplier = 0.1f; + break; + } + } + } + } + float reduction = isPriority ? 1 : 2; + float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; + if (operateObjective != null && objectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + { + // Prioritize leaks that we are already fixing + Priority = maxPriority; } else { - float reduction = isPriority ? 1 : 2; - float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; - if (operateObjective != null && objectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); + if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) { - // Prioritize leaks that we are already fixing - Priority = maxPriority; - } - else - { - float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); - float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). - // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); - if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) - { - // Double the distance when the leak can be accessed from the current hull. - distanceFactor *= 2; - } - float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + // Double the distance when the leak can be accessed from the current hull. + distanceFactor *= 2; } + float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1)); } return Priority; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 9094affbe..d9c8ff24a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -38,9 +38,9 @@ namespace Barotrauma protected override float TargetEvaluation() { - int totalLeaks = Targets.Count(); + int totalLeaks = Targets.Count; if (totalLeaks == 0) { return 0; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true); + int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && c.Character.Submarine == character.Submarine); bool anyFixers = otherFixers > 0; if (objectiveManager.IsOrder(this)) { @@ -52,7 +52,7 @@ namespace Barotrauma int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); int leaks = totalLeaks - secondaryLeaks; float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1; - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 2f3e75a45..aaee84fa1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -1,9 +1,11 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Immutable; using System.Collections.Generic; using System.Linq; +using System.Diagnostics; namespace Barotrauma { @@ -31,14 +33,15 @@ namespace Barotrauma private ISpatialEntity moveToTarget; private bool isDoneSeeking; public Item TargetItem => targetItem; - private int currSearchIndex; + private int currentSearchIndex; public ImmutableHashSet ignoredContainerIdentifiers; public ImmutableHashSet ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; - public static float DefaultReach = 100; + public const float DefaultReach = 100; + public const float MaxReach = 150; public bool AllowToFindDivingGear { get; set; } = true; public bool MustBeSpecificItem { get; set; } @@ -76,7 +79,7 @@ namespace Barotrauma public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - currSearchIndex = -1; + currentSearchIndex = 0; Equip = equip; originalTarget = targetItem; this.targetItem = targetItem; @@ -89,7 +92,7 @@ namespace Barotrauma public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { - currSearchIndex = -1; + currentSearchIndex = 0; Equip = equip; this.spawnItemIfNotFound = spawnItemIfNotFound; this.checkInventory = checkInventory; @@ -125,7 +128,7 @@ namespace Barotrauma public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) { - return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach); } private bool CheckInventory() @@ -246,7 +249,7 @@ namespace Barotrauma else { character.SelectCharacter(c); - canInteract = character.CanInteractWith(c, maxDist: DefaultReach); + canInteract = character.CanInteractWith(c); character.DeselectCharacter(); } } @@ -268,7 +271,7 @@ namespace Barotrauma Inventory itemInventory = targetItem.ParentInventory; var slots = itemInventory?.FindIndices(targetItem); - if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true)) + if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags)) { if (TakeWholeStack && slots != null) { @@ -298,8 +301,11 @@ namespace Barotrauma if (!Equip) { // Try equipping and wearing the item - Wear = true; Equip = true; + if (!objectiveManager.HasActiveObjective() && !objectiveManager.HasActiveObjective()) + { + Wear = true; + } return; } #if DEBUG @@ -342,6 +348,10 @@ namespace Barotrauma } } + private Stopwatch sw; + private Stopwatch StopWatch => sw ??= new Stopwatch(); + private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>(); + private List itemList; private void FindTargetItem() { if (IdentifiersOrTags == null) @@ -349,13 +359,16 @@ namespace Barotrauma if (targetItem == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red); + DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined."); #endif Abandon = true; } return; } - + if (HumanAIController.DebugAI) + { + StopWatch.Restart(); + } float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); if (!CheckPathForEachItem) { @@ -363,15 +376,25 @@ 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; - bool hasCalledPathFinder = false; - int itemsPerFrame = (int)priority; - for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++) + // Reset if the character has switched subs. + if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true)) { - currSearchIndex++; - var item = Item.ItemList[currSearchIndex]; + currentSearchIndex = 0; + } + if (currentSearchIndex == 0) + { + itemCandidates.Clear(); + itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true); + } + int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority)); + int checkedItems = 0; + for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++) + { + checkedItems++; + var item = itemList[currentSearchIndex]; Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; if (itemSub == null) { continue; } Submarine mySub = character.Submarine; @@ -395,8 +418,6 @@ namespace Barotrauma if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } } - // Don't allow going into another sub, unless it's connected and of the same team and type. - if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; } if (character.IsItemTakenBySomeoneElse(item)) { continue; } if (item.ParentInventory is ItemInventory itemInventory) { @@ -411,11 +432,14 @@ namespace Barotrauma if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(character)) { continue; } - if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } - //the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool) - if (ownerItem != item.Container) + if (ownerItem != item) { - itemPriority *= 0.1f; + if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } + //the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool) + if (ownerItem != item.Container) + { + itemPriority *= 0.1f; + } } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; @@ -463,22 +487,69 @@ namespace Barotrauma { itemPriority *= item.Condition / item.MaxCondition; } + if (checkPath) + { + itemCandidates.Add((item, itemPriority)); + } // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } - if (!hasCalledPathFinder && PathSteering != null && checkPath) + if (EvaluateCombatPriority && itemPriority <= 0) { - hasCalledPathFinder = true; - var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) { continue; } + // Not good enough + continue; } currItemPriority = itemPriority; targetItem = item; moveToTarget = rootInventoryOwner ?? item; } - if (currSearchIndex >= Item.ItemList.Count - 1) + if (currentSearchIndex >= itemList.Count - 1) { isDoneSeeking = true; - if (targetItem == null) + } + if (checkedItems > 0) + { + if (isDoneSeeking && itemCandidates.Any()) + { + itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority)); + } + if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2) + { + var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; + if (StopWatch.ElapsedMilliseconds > 5) + { + DebugConsole.ThrowError(msg); + } + else + { + // An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue. + DebugConsole.AddWarning(msg); + } + } + } + if (isDoneSeeking) + { + if (PathSteering == null) + { + itemCandidates.Clear(); + } + if (itemCandidates.Any()) + { + if (itemCandidates.FirstOrDefault() is { } itemCandidate) + { + 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. + itemCandidates.Remove(itemCandidate); + } + else + { + // The path was valid -> we are done. + itemCandidates.Clear(); + } + } + } + if (targetItem == null && itemCandidates.None()) { if (spawnItemIfNotFound) { @@ -569,11 +640,11 @@ namespace Barotrauma { if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; - if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } + if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } - if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; } - return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; } + return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); } public override void Reset() @@ -591,7 +662,7 @@ namespace Barotrauma targetItem = originalTarget; moveToTarget = targetItem?.GetRootInventoryOwner(); isDoneSeeking = false; - currSearchIndex = 0; + currentSearchIndex = 0; currItemPriority = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index e3a5efe07..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 { @@ -257,302 +289,290 @@ namespace Barotrauma } } } - else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false)) + else if (HumanAIController.HasValidPath(requireUnfinished: false)) { 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; - if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true)) - { - 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); + }); } - } + }, + 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.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) || - (rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1); - 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 && HumanAIController.UseIndoorSteeringOutside) - { - nodeFilter = n => n.Waypoint.Submarine == null; - } - - if (!isInside && !UsePathingOutside) - { - 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 { - 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) { @@ -560,19 +580,41 @@ namespace Barotrauma } else { - SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); + character.ReleaseSecondaryItem(); + 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) { if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; } SteeringManager.Reset(); + character.ReleaseSecondaryItem(); character.CursorPosition = targetWorldPos; if (character.Submarine != null) { @@ -580,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)) @@ -659,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; } @@ -772,6 +814,16 @@ namespace Barotrauma { StopMovement(); HumanAIController.FaceTarget(Target); + if (Target is WayPoint { Ladders: null }) + { + // 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. + if (character.IsClimbing && character.AnimController.IsAboveFloor) + { + character.StopClimbing(); + } + } base.OnCompleted(); } @@ -781,6 +833,10 @@ namespace Barotrauma findDivingGear = null; seekGapsTimer = 0; TargetGap = null; + if (SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 3625f7a1e..bb0c590c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -163,15 +163,22 @@ namespace Barotrauma character.SelectedItem = null; - CleanupItems(deltaTime); + if (!character.IsClimbing) + { + 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; } - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) + bool currentTargetIsInvalid = + currentTarget == null || + IsForbidden(currentTarget) || + (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + + if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull)) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -251,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 @@ -271,7 +279,7 @@ namespace Barotrauma return; } character.AIController.SelectTarget(currentTarget.AiTarget); - PathSteering.SetPath(path); + PathSteering.SetPath(targetPos, path); SetTargetTimerNormal(); searchingNewHull = false; } @@ -305,12 +313,8 @@ namespace Barotrauma { if (character.IsClimbing) { - if (character.AnimController.GetHeightFromFloor() < 0.1f) - { - character.AnimController.Anim = AnimController.Animation.None; - character.SelectedSecondaryItem = null; - } - return; + PathSteering.Reset(); + character.StopClimbing(); } var currentHull = character.CurrentHull; if (!character.AnimController.InWater && currentHull != null) @@ -362,6 +366,7 @@ namespace Barotrauma } else { + character.ReleaseSecondaryItem(); PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff)); } return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index ec2a5ad30..118849867 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -24,7 +24,7 @@ namespace Barotrauma private ImmutableHashSet ValidContainableItemIdentifiers { get; } private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); - private int itemIndex = 0; + private int itemIndex; private AIObjectiveDecontainItem decontainObjective; private readonly HashSet ignoredItems = new HashSet(); private Item targetItem; @@ -219,9 +219,8 @@ namespace Barotrauma return Priority; } - public override void Update(float deltaTime) + protected override void Act(float deltaTime) { - base.Update(deltaTime); if (targetItem == null) { if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) @@ -233,6 +232,7 @@ namespace Barotrauma } targetItem = item; } + objectiveManager.GetObjective().Wander(deltaTime); float GetPriority(Item item) { try @@ -256,11 +256,7 @@ namespace Barotrauma } } } - } - - protected override void Act(float deltaTime) - { - if (targetItem != null) + else { if(decontainObjective == null && !IsValidContainable(targetItem)) { @@ -290,10 +286,6 @@ namespace Barotrauma Reset(); }); } - else - { - objectiveManager.GetObjective().Wander(deltaTime); - } } private bool IsValidContainable(Item item) @@ -310,7 +302,7 @@ namespace Barotrauma if (parentItem.HasTag("donttakeitems")) { return false; } } if (!item.HasAccess(character)) { return false; } - if (!character.HasItem(item) && !CanEquip(item)) { return false; } + if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index c54980f2e..eeee31b23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -63,7 +63,7 @@ namespace Barotrauma { if (item == null || item.Removed) { return false; } if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } - if (!(item.GetComponent() is ItemContainer container)) { return false; } + if ((item.GetComponent() is not ItemContainer container)) { return false; } if (container.Inventory == null) { return false; } if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 38aa4b5d5..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) @@ -89,12 +91,7 @@ namespace Barotrauma var target = objective.Key; if (!Targets.Contains(target)) { - var subObjective = objective.Value; - if (CurrentSubObjective == subObjective) - { - CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted; - } - subObjectives.Remove(subObjective); + subObjectives.Remove(objective.Value); } } SyncRemovedObjectives(Objectives, GetList()); @@ -157,6 +154,11 @@ namespace Barotrauma else { float max = AIObjectiveManager.LowestOrderPriority - 1; + if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character)) + { + // Allow higher prio + max = AIObjectiveManager.EmergencyObjectivePriority; + } float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); Priority = MathHelper.Lerp(0, max, value); } @@ -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 142a57783..af05c7095 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma MaxValue = 2 } + public const float MaxObjectivePriority = 100; + public const float EmergencyObjectivePriority = 90; public const float HighestOrderPriority = 70; public const float LowestOrderPriority = 60; public const float RunPriority = 50; @@ -125,11 +127,21 @@ namespace Barotrauma { CoroutineManager.StopCoroutines(delayedObjective.Value); } + + var prevIdleObjective = GetObjective(); + DelayedObjectives.Clear(); Objectives.Clear(); FailedAutonomousObjectives = false; AddObjective(new AIObjectiveFindSafety(character, this)); - AddObjective(new AIObjectiveIdle(character, this)); + var newIdleObjective = new AIObjectiveIdle(character, this); + if (prevIdleObjective != null) + { + newIdleObjective.TargetHull = prevIdleObjective.TargetHull; + newIdleObjective.Behavior = prevIdleObjective.Behavior; + prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t)); + } + AddObjective(newIdleObjective); int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { @@ -155,7 +167,7 @@ namespace Barotrauma AddObjective(objective, delay: Rand.Value() / 2); objectiveCount++; } - } + } _waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount); } @@ -410,12 +422,12 @@ namespace Barotrauma newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { CloseEnough = Rand.Range(80f, 100f), - CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), + CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, - IsFollowOrderObjective = true, + IsFollowOrder = true, Mimic = character.IsOnPlayerTeam, DialogueIdentifier = "dialogcannotreachplace".ToIdentifier() }; @@ -423,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": @@ -633,10 +649,11 @@ namespace Barotrauma public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; - /// - /// Returns the last active objective of the specific type. - /// - public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + public T GetLastActiveObjective() where T : AIObjective + => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + + public T GetFirstActiveObjective() where T : AIObjective + => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T; /// /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 06305e134..163b7bee2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -122,7 +122,7 @@ namespace Barotrauma else if (!isOrder) { var steering = component?.Item.GetComponent(); - if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain))) + if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true))) { // Ignore if already set to autopilot or if there's a captain onboard Priority = 0; @@ -204,7 +204,7 @@ namespace Barotrauma } if (operateTarget != null) { - if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) + if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) { // Another crew member is already targeting this entity (leak). Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index de55f4035..b46f1f2e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -69,16 +69,6 @@ namespace Barotrauma if (subObjective != null && subObjective.IsCompleted) { Priority = 0; - items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character)); - if (items.None()) - { - Abandon = true; - - } - else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character)))) - { - Reset(); - } } return Priority; } @@ -162,25 +152,25 @@ namespace Barotrauma }; } if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, - onCompleted: () => - { - if (KeepActiveWhenReady) + onCompleted: () => { - if (getSingleItemObjective != null) + if (KeepActiveWhenReady) { - var item = getSingleItemObjective?.TargetItem; - if (item?.IsOwnedBy(character) != null) + if (getSingleItemObjective != null) { - items.Add(item); + var item = getSingleItemObjective?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + items.Add(item); + } } } - } - else - { - IsCompleted = true; - } - }, - onAbandon: () => Abandon = true)) + else + { + IsCompleted = true; + } + }, + onAbandon: () => Abandon = true)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index c2539d899..dbb0a68a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -102,7 +102,7 @@ namespace Barotrauma // Don't stop fixing until completely done return 100; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); + int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective()); int items = Targets.Count; if (items == 0) { @@ -116,7 +116,7 @@ namespace Barotrauma } else { - if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) + if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f)) { // Enough fixers return 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 5a93ceb26..cba0a97f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -293,6 +293,11 @@ namespace Barotrauma currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); + //allow taking items from the target's inventory too if the target is unconscious + if (matchingItem == null && targetCharacter.IsIncapacitated) + { + matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); + } if (matchingItem != null) { bestItem = matchingItem; @@ -379,24 +384,24 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (character != targetCharacter && character.IsOnPlayerTeam) + if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); + SpeakCannotTreat(); } }); } else if (cprSuitability <= 0) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; + SpeakCannotTreat(); } } } else if (!targetCharacter.IsUnconscious) { - //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; + //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) + SpeakCannotTreat(); return; } if (character != targetCharacter) @@ -414,6 +419,14 @@ namespace Barotrauma } } + private void SpeakCannotTreat() + { + LocalizedString msg = character == targetCharacter ? + TextManager.Get("dialogcannottreatself") : + TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No); + character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); + } + private void ApplyTreatment(Affliction affliction, Item item) { item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction)); @@ -433,50 +446,36 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed) + if (!IsAllowed || targetCharacter == null) { Priority = 0; Abandon = true; return Priority; } - if (character.CurrentHull == null) + if (character.CurrentHull != null) { - if (!objectiveManager.HasOrder()) + if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { + // Don't go into rooms that have enemies Priority = 0; Abandon = true; return Priority; } } - else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); + if (character.Submarine?.Info is { IsRuin: false }) { - // Don't go into rooms that have enemies - Priority = 0; - Abandon = true; - return Priority; + verticalDistance *= 2; } - if (targetCharacter == null) + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); + if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull) { - Priority = 0; - Abandon = true; - } - else - { - float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); - if (character.Submarine?.Info is { IsRuin: false }) - { - verticalDistance *= 2; - } - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); - if (targetCharacter.CurrentHull == character.CurrentHull) - { - distanceFactor = 1; - } - float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; - float devotion = CumulatedDevotion / 100; - Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); + distanceFactor = 1; } + float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100; + float devotion = CumulatedDevotion / 100; + Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1)); return Priority; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 50f03a240..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; @@ -42,15 +61,16 @@ namespace Barotrauma if (Targets.None()) { return 100; } if (!objectiveManager.IsOrder(this)) { - if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) + if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsMedic, onlyActive: true, onlyConnectedSubs: true)) { - // Don't do anything if there's a medic on board and we are not a medic + // Don't do anything if there's a medic on board actively treating and we are not a medic return 100; } } float worstCondition = Targets.Min(t => GetVitalityFactor(t)); if (Targets.Contains(character)) { + // Targeting self -> higher prio if (character.Bleeding > 10) { // Enforce the highest priority when bleeding out. @@ -67,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; @@ -83,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; @@ -101,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; } @@ -111,13 +138,13 @@ namespace Barotrauma { if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { - return false; + return false; } if (!humanAI.ObjectiveManager.HasOrder()) { if (!character.IsMedic && target != character) { - // Don't allow to treat others autonomously + // Don't allow to treat others autonomously, unless we are a medic return false; } // Ignore unsafe hulls, unless ordered @@ -126,6 +153,10 @@ namespace Barotrauma return false; } } + if (requireTreatableAfflictions && GetTreatableAfflictions(target).None()) + { + return false; + } } else { @@ -136,17 +167,17 @@ namespace Barotrauma // Don't allow going into another sub, unless it's connected and of the same team and type. if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } } - else + else if (target.Submarine != null) { - return target.Submarine == null; + // We are outside, but the target is inside. + return false; } if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { - // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters + // Ignore all concious targets that are currently fighting, fleeing, or treating characters if (targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective() || - targetAI.ObjectiveManager.HasActiveObjective() || - targetAI.ObjectiveManager.HasActiveObjective()) + targetAI.ObjectiveManager.HasActiveObjective()) { return false; } @@ -158,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/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 88db60899..338568707 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -228,8 +228,8 @@ namespace Barotrauma { continue; } - //optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one - if (node.TempDistance < 1.0f) + //optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one + if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach)) { if (IsValidStartNode(node)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index 402d3ed94..ba29bdbb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +8,7 @@ namespace Barotrauma { class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem { - public override float RedundantIssueModifier => 0.65f; + public override float RedundantIssueModifier => 0.8f; private readonly List targetingImportances = new List(); public override bool AllowEasySwitching => true; @@ -17,43 +18,52 @@ namespace Barotrauma float GetTargetingImportance(Entity entity) { float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition); - - float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f); - if (TargetItem.Submarine != null) + if (currentDistanceToEnemy > Sonar.DefaultSonarRange) { return 0.0f; } + float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MaxImportance * 0.1f, MaxImportance * 0.5f); + if (TargetItem.Submarine != null && importance > 0.0f) { - Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition; - Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition; - if (Vector2.Dot(dir, submarineDir) < 0) + if (TargetItemComponent is Turret turret) { - //direction from the weapon to the target is opposite to the direction from the sub to the weapon - // = the turret is most likely on the wrong side of the sub, reduce importance - importance *= 0.1f; + if (!turret.CheckTurretAngle(entity.WorldPosition)) + { + importance *= 0.1f; + } } + else + { + Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition; + Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition; + if (Vector2.Dot(dir, submarineDir) < 0) + { + //direction from the weapon to the target is opposite to the direction from the sub to the weapon + // = the turret is most likely on the wrong side of the sub, reduce importance + importance *= 0.1f; + } + } + } return importance; } public override void CalculateImportanceSpecific() { - if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) - { - //operate (= recharge the turrets) with low priority if they're out of power - //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority - Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f; - return; - } - targetingImportances.Clear(); foreach (Character character in shipCommandManager.EnemyCharacters) { targetingImportances.Add(GetTargetingImportance(character)); } // there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing - if (targetingImportances.Any(i => i > 0)) { targetingImportances.Sort(); - Importance = targetingImportances.TakeLast(3).Sum(); + Importance = Math.Max(targetingImportances.TakeLast(3).Sum(), ShipCommandManager.MinimumIssueThreshold); + } + if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) + { + //operate (= recharge the turrets) with low priority if they're out of power + //if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority + Importance = Math.Max(ShipCommandManager.MinimumIssueThreshold / RedundantIssueModifier, Importance); + return; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 06be740df..2ac885b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -256,7 +256,7 @@ namespace Barotrauma float bestValue = 0f; Character bestCharacter = null; - if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold) + if (mostImportantIssue != null && mostImportantIssue.Importance >= MinimumIssueThreshold) { IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true); 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 f78151840..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 { @@ -1259,7 +1259,7 @@ namespace Barotrauma if (ladder.Item.Prefab.Triggers.None()) { - character.SelectedSecondaryItem = null; + character.ReleaseSecondaryItem(); return; } @@ -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/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 2a9aa8211..5991498e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -78,6 +78,11 @@ namespace Barotrauma } } + /// + /// Attacks are used to deal damage to characters, structures and items. + /// They can be defined in the weapon components of the items or the limb definitions of the characters. + /// The limb attacks can also be used by the player, when they control a monster or have some appendage, like a husk stinger. + /// partial class Attack : ISerializableEntity { [Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable] @@ -123,7 +128,7 @@ namespace Barotrauma set => _damageRange = value; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float MinRange { get; private set; } [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] @@ -138,22 +143,22 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float CoolDownRandomFactor { get; private set; } = 0; - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable] public bool FullSpeedAfterAttack { get; private set; } private float _structureDamage; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float StructureDamage { get => _structureDamage * DamageMultiplier; set => _structureDamage = value; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable] public bool EmitStructureDamageParticles { get; private set; } private float _itemDamage; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage { get =>_itemDamage * DamageMultiplier; @@ -178,16 +183,16 @@ namespace Barotrauma /// public float ImpactMultiplier { get; set; } = 1; - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable] public bool Ranged { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable] + [Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable] public float RequiredAngle { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] @@ -205,16 +210,13 @@ namespace Barotrauma [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] public float SwayFrequency { get; set; } - /// - /// Legacy support. Use Afflictions. - /// - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")] public float Stun { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; set; } - [Serialize("", IsPropertySaveable.Yes), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable] public string ApplyForceOnLimbs { get @@ -240,20 +242,20 @@ namespace Barotrauma [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldStart { get; private set; } - + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldMiddle { get; private set; } - + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldEnd { get; private set; } - - [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable] + + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable] public TransitionMode RootTransitionEasing { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] public float Torque { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable] public bool ApplyForcesOnlyOnce { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] @@ -279,10 +281,10 @@ namespace Barotrauma //public float StickChance { get; set; } public float StickChance => 0f; - [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } - [Serialize(false, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable] public bool Blink { get; private set; } public IEnumerable StatusEffects @@ -298,7 +300,7 @@ namespace Barotrauma private set; } = new Dictionary(); - //the indices of the limbs Force is applied on + //the indices of the limbs Force is applied on //(if none, force is applied only to the limb the attack is attached to) public readonly List ForceOnLimbIndices = new List(); @@ -309,6 +311,10 @@ namespace Barotrauma /// public List Conditionals { get; private set; } = new List(); + /// + /// StatusEffects to apply when the attack triggers. + /// StatusEffect types of 'OnUse' are executed always, 'OnFailure' only when the attack doesn't deal damage and 'OnSuccess' executes when some damage is dealt. + /// private readonly List statusEffects = new List(); public void SetUser(Character user) @@ -322,7 +328,7 @@ namespace Barotrauma // used for talents/ability conditions public Item SourceItem { get; set; } - + public List GetMultipliedAfflictions(float multiplier) { List multipliedAfflictions = new List(); @@ -428,7 +434,13 @@ namespace Barotrauma } break; case "conditional": - Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + Conditionals.Add(new PropertyConditional(attribute)); + } + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index a03315f66..6042aecee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -11,6 +11,7 @@ using FarseerPhysics.Dynamics; using Barotrauma.Extensions; using System.Collections.Immutable; using Barotrauma.Abilities; +using System.Diagnostics; #if SERVER using System.Text; #endif @@ -964,6 +965,7 @@ namespace Barotrauma /// The secondary selected item. It's an item other than a device (see ), e.g. a ladder or a chair. /// public Item SelectedSecondaryItem { get; set; } + public void ReleaseSecondaryItem() => SelectedSecondaryItem = null; /// /// Has the characters selected a primary or a secondary item? /// @@ -1063,7 +1065,7 @@ namespace Barotrauma { get { - return SelectedItem == null || (SelectedItem.GetComponent()?.AllowAiming ?? false); + return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled; } } @@ -1126,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 { @@ -2432,6 +2435,8 @@ namespace Barotrauma return true; } + private Stopwatch sw; + private Stopwatch StopWatch => sw ??= new Stopwatch(); private float _selectedItemPriority; private Item _foundItem; /// @@ -2445,14 +2450,20 @@ namespace Barotrauma IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null) { + if (HumanAIController.DebugAI) + { + StopWatch.Restart(); + } if (itemIndex == 0) { _foundItem = null; _selectedItemPriority = 0; } - for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++) + int itemsPerFrame = IsOnPlayerTeam ? 100 : 10; + int checkedItemCount = 0; + for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++) { - itemIndex++; + checkedItemCount++; var item = Item.ItemList[itemIndex]; if (!item.IsInteractable(this)) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } @@ -2492,7 +2503,21 @@ namespace Barotrauma } } targetItem = _foundItem; - return itemIndex >= Item.ItemList.Count - 1; + bool completed = itemIndex >= Item.ItemList.Count - 1; + if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1) + { + var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}"; + if (StopWatch.ElapsedMilliseconds > 5) + { + DebugConsole.ThrowError(msg); + } + else + { + // An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue. + DebugConsole.AddWarning(msg); + } + } + return completed; } public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null; @@ -2512,7 +2537,7 @@ namespace Barotrauma } } - return checkVisibility ? CanSeeCharacter(c) : true; + return !checkVisibility || CanSeeCharacter(c); } public bool CanInteractWith(Item item, bool checkLinked = true) @@ -2679,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.FindContainingInLocalCoordinates(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; } @@ -2863,7 +2867,7 @@ namespace Barotrauma } #endif } - else if (!IsClimbing) + else { #if CLIENT if (Controlled == this) @@ -2906,14 +2910,14 @@ namespace Barotrauma else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent() == null && (focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect)) { - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); #if CLIENT CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && SelectedItem != null) + else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) { - SelectedItem = null; + SelectedItem = SelectedSecondaryItem = null; } else if (focusedItem != null) { @@ -3246,7 +3250,7 @@ namespace Barotrauma } if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem)) { - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); } if (!IsDead) { LockHands = false; } @@ -3460,14 +3464,19 @@ namespace Barotrauma despawnTimer += deltaTime * despawnPriority; if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } - if (IsHuman) + Identifier despawnContainerId = + IsHuman ? + "despawncontainer".ToIdentifier() : + Params.DespawnContainer; + if (!despawnContainerId.IsEmpty) { var containerPrefab = - ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ?? + MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ?? + ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ?? (MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab); if (containerPrefab == null) { - DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red); + DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red); } else { @@ -3591,7 +3600,7 @@ namespace Barotrauma { if (character == this) { continue; } if (character.TeamID != TeamID) { continue; } - if (!(character.AIController is HumanAIController)) { continue; } + if (character.AIController is not HumanAIController) { continue; } if (!HumanAIController.IsActive(character)) { continue; } foreach (var currentOrder in character.CurrentOrders) { @@ -4562,11 +4571,6 @@ namespace Barotrauma SetStun(0.0f, true); isDead = false; - if (info != null) - { - info.CauseOfDeath = null; - } - foreach (LimbJoint joint in AnimController.LimbJoints) { var revoluteJoint = joint.revoluteJoint; @@ -4590,7 +4594,10 @@ namespace Barotrauma limb.IsSevered = false; } - GameMain.GameSession?.ReviveCharacter(this); + if (GameMain.GameSession != null) + { + GameMain.GameSession.ReviveCharacter(this); + } } public override void Remove() @@ -4627,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) @@ -4713,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()) @@ -4966,6 +4982,8 @@ namespace Barotrauma #region Talents private readonly List characterTalents = new List(); + public IReadOnlyCollection CharacterTalents => characterTalents; + public void LoadTalents() { List toBeRemoved = null; @@ -5062,7 +5080,7 @@ namespace Barotrauma public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) { - foreach (var characterTalent in characterTalents) + foreach (CharacterTalent characterTalent in CharacterTalents) { characterTalent.CheckTalent(abilityEffectType, abilityObject); } @@ -5358,7 +5376,7 @@ namespace Barotrauma public void StopClimbing() { AnimController.StopClimbing(); - SelectedSecondaryItem = null; + ReleaseSecondaryItem(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 9d0379987..fde9954bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -905,10 +905,19 @@ namespace Barotrauma Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (talentIdentifier == Identifier.Empty) { continue; } + if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab)) + { + foreach (TalentMigration migration in prefab.Migrations) + { + migration.TryApply(version, this); + } + } + UnlockedTalents.Add(talentIdentifier); } } } + LoadHeadAttachments(); } @@ -1886,6 +1895,21 @@ namespace Barotrauma } } + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier) + { + float statValue = GetSavedStatValue(statType, statIdentifier); + + if (GameMain.NetworkMember is null) { return statValue; } + + foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + { + int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier); + statValue = Math.Max(statValue, botStatValue); + } + + return statValue; + } + public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) { if (!SavedStatValues.ContainsKey(statType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index e6c09cc3b..ffbf96858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -179,7 +179,7 @@ namespace Barotrauma float currVitalityDecrease = MathHelper.Lerp( currentEffect.MinVitalityDecrease, currentEffect.MaxVitalityDecrease, - currentEffect.GetStrengthFactor(this)); + currentEffect.GetStrengthFactor(strength)); if (currentEffect.MultiplyByMaxVitality) { @@ -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 87b22795b..3b65cc320 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -117,7 +117,7 @@ namespace Barotrauma public readonly bool CauseSpeechImpediment; /// - /// If not set to true, affected characters will no longer require air + /// If set to false, affected characters will no longer require air /// once the affliction reaches the active stage. /// public readonly bool NeedsAir; @@ -247,7 +247,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No, description: "If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " + - "vilatily, with 1 meaning 100%, instead of the same amount for all species.")] + "vitality, with 1 meaning 100%, instead of the same amount for all species.")] public bool MultiplyByMaxVitality { get; private set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")] @@ -457,16 +457,22 @@ namespace Barotrauma /// Returns 0 if affliction.Strength is MinStrength, /// 1 if affliction.Strength is MaxStrength /// - public float GetStrengthFactor(Affliction affliction) + public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength); + + /// + /// Returns 0 if affliction.Strength is MinStrength, + /// 1 if affliction.Strength is MaxStrength + /// + public float GetStrengthFactor(float strength) => MathUtils.InverseLerp( MinStrength, MaxStrength, - affliction.Strength); + strength); } /// - /// Description element can be used to define descriptions for the affliction that are shown at specific conditions. - /// For example a description that only shows to other players or only at certain strength levels. + /// The description element can be used to define descriptions for the affliction which are shown under specific conditions; + /// for example a description that only shows to other players or only at certain strength levels. /// /// /// @@ -692,8 +698,7 @@ namespace Barotrauma public readonly bool ShowBarInHealthMenu; /// - /// If set to true, this affliction's icon will be hidden from the HUD - /// after 5 seconds. + /// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds. /// public readonly bool HideIconAfterDelay; @@ -701,37 +706,37 @@ namespace Barotrauma /// How high the strength has to be for the affliction to take effect /// public readonly float ActivationThreshold = 0.0f; - + /// /// How high the strength has to be for the affliction icon to be shown in the UI /// public readonly float ShowIconThreshold = 0.05f; - + /// /// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface /// public readonly float ShowIconToOthersThreshold = 0.05f; - + /// /// The maximum strength this affliction can have. /// public readonly float MaxStrength = 100.0f; /// - /// The strength of the radiation grain effect to apply - /// when the strength of this affliction increases. + /// The strength of the radiation grain effect to apply when the strength of this affliction increases. /// public readonly float GrainBurst; /// /// 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 + /// 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 @@ -758,13 +763,12 @@ namespace Barotrauma /// public readonly float DamageOverlayAlpha; - /// - /// Steam achievement given when the controlled character receives the affliction + /// Steam achievement given when the controlled character receives the affliction. /// public readonly Identifier AchievementOnReceived; /// - /// Steam achievement given when the affliction is removed from the controlled character + /// Steam achievement given when the affliction is removed from the controlled character. /// public readonly Identifier AchievementOnRemoved; @@ -775,10 +779,8 @@ namespace Barotrauma public readonly Color[] IconColors; /// - /// If set to true and the affliction has an AfflictionOverlay element, - /// the overlay's opacity will be strictly proportional to its strength. - /// Otherwise, the overlay's opacity will be determined based on its - /// activation threshold and effects. + /// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength. + /// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects. /// public readonly bool AfflictionOverlayAlphaIsLinear; @@ -788,16 +790,18 @@ namespace Barotrauma public readonly bool ResetBetweenRounds; /// - /// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage". + /// Should damage particles be emitted when a character receives this affliction? + /// Only relevant if the affliction is of the type "bleeding" or "damage". /// public readonly bool DamageParticles; /// /// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target. - /// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile. - /// If the affliction is of type buff, the skill is increased only when the target is friendly. + /// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile. + /// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly. /// public readonly float MedicalSkillGain; + /// /// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target. /// The skill is increased only when the target is hostile. @@ -827,14 +831,13 @@ namespace Barotrauma private readonly ConstructorInfo constructor; /// - /// Icon that's used in UI to represent this affliction. + /// An icon that’s used in the UI to represent this affliction. /// public readonly Sprite Icon; /// /// A sprite that covers the affected player's entire screen when this affliction is active. - /// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier - /// and MaxAfflictionOverlayAlphaMultiplier + /// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier /// public readonly Sprite AfflictionOverlay; @@ -844,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); @@ -912,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/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index ba88edee5..9289a90d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -119,6 +119,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] public float AISlowTurretPriority { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable] + public Identifier DespawnContainer { get; private set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -228,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/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs index 4fe2c1c86..c78b2c66b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -6,11 +6,13 @@ namespace Barotrauma.Abilities { private readonly ItemTalentStats stat; private readonly float value; + private readonly bool stackable; public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); + stackable = abilityElement.GetAttributeBool("stackable", true); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) @@ -25,7 +27,7 @@ namespace Barotrauma.Abilities { if (abilityObject is not IAbilityItem ability) { return; } - ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent); + ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 6c4022968..4185832c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -9,12 +9,14 @@ namespace Barotrauma.Abilities private readonly ItemTalentStats stat; private readonly float value; private readonly ImmutableHashSet tags; + private readonly bool stackable; public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + stackable = abilityElement.GetAttributeBool("stackable", true); } public override void InitializeAbility(bool addingFirstTime) @@ -41,7 +43,7 @@ namespace Barotrauma.Abilities { if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { - item.StatManager.ApplyStat(stat, value, CharacterTalent); + item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); } } } 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/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index bb6a2ac0a..6b59a5825 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -217,6 +217,11 @@ namespace Barotrauma.Abilities public static StatTypes ParseStatType(string statTypeString, string debugIdentifier) { + //backwards compatibility + if (statTypeString.Equals("MedicalItemDurationMultiplier", StringComparison.OrdinalIgnoreCase)) + { + statTypeString = "BuffItemApplyingMultiplier"; + } if (!Enum.TryParse(statTypeString, true, out StatTypes statType)) { DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 4cc374e79..0cb53857e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -71,6 +71,29 @@ namespace Barotrauma } } + private static readonly HashSet checkedNonStackableTalents = new(); + + /// + /// Checks talents for a given AbilityObject taking into account non-stackable talents. + /// + public static void CheckTalentsForCrew(IEnumerable crew, AbilityEffectType type, AbilityObject abilityObject) + { + checkedNonStackableTalents.Clear(); + foreach (Character character in crew) + { + foreach (CharacterTalent characterTalent in character.CharacterTalents) + { + if (!characterTalent.Prefab.AbilityEffectsStackWithSameTalent) + { + if (checkedNonStackableTalents.Contains(characterTalent.Prefab.Identifier)) { continue; } + checkedNonStackableTalents.Add(characterTalent.Prefab.Identifier); + } + + characterTalent.CheckTalent(type, abilityObject); + } + } + } + public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject) { if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs new file mode 100644 index 000000000..b185d09cd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentMigration.cs @@ -0,0 +1,109 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + internal abstract class TalentMigration + { + private readonly Version version; + + private delegate TalentMigration TalentMigrationCtor(Version version, ContentXElement element); + + private static readonly Dictionary migrationTemplates = + new() + { + [new Identifier("AddStat")] = + static (version, element) => new TalentMigrationAddStat(version, element), + + [new Identifier("UpdateStatIdentifier")] = + static (version, element) => new TalentMigrationUpdateStatIdentifier(version, element) + }; + + public bool TryApply(Version savedVersion, CharacterInfo info) + { + if (version <= savedVersion) { return false; } + Apply(info); + return true; + } + + protected abstract void Apply(CharacterInfo info); + + protected TalentMigration(Version targetVersion) + { + version = targetVersion; + } + + public static TalentMigration FromXML(ContentXElement element) + { + Version? version = XMLExtensions.GetAttributeVersion(element, "version", null); + + if (version is null) + { + throw new Exception("Talent migration version not defined."); + } + + Identifier name = element.Name.ToString().ToIdentifier(); + + if (!migrationTemplates.TryGetValue(name, out TalentMigrationCtor? ctor)) + { + throw new Exception($"Unknown talent migration type: {name}."); + } + + return ctor(version, element); + } + } + + /// + /// Migration that adds a missing permanent stat to the character. + /// + internal sealed class TalentMigrationAddStat : TalentMigration + { + [Serialize(StatTypes.None, IsPropertySaveable.Yes)] + public StatTypes StatType { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier StatIdentifier { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes)] + public float Value { get; set; } + + [Serialize(false, IsPropertySaveable.Yes)] + public bool RemoveOnDeath { get; set; } + + public TalentMigrationAddStat(Version targetVersion, ContentXElement element) : base(targetVersion) + => SerializableProperty.DeserializeProperties(this, element); + + protected override void Apply(CharacterInfo info) + { + info.ChangeSavedStatValue(StatType, Value, StatIdentifier, RemoveOnDeath); + } + } + + /// + /// Migration that updates permanent stat identifiers. + /// + internal class TalentMigrationUpdateStatIdentifier : TalentMigration + { + [Serialize("", IsPropertySaveable.Yes, "The old identifier to update.")] + public Identifier Old { get; set; } + + [Serialize("", IsPropertySaveable.Yes, "What to change the old identifier to.")] + public Identifier New { get; set; } + + public TalentMigrationUpdateStatIdentifier(Version targetVersion, ContentXElement element) : base(targetVersion) + => SerializableProperty.DeserializeProperties(this, element); + + protected override void Apply(CharacterInfo info) + { + foreach (SavedStatValue statValue in info.SavedStatValues.Values.SelectMany(static s => s)) + { + if (statValue.StatIdentifier != Old) { continue; } + + statValue.StatIdentifier = New; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index d3cc70e56..bd105e729 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,4 +1,7 @@ -#if CLIENT +using System; +using System.Collections.Immutable; +using System.Xml.Linq; +#if CLIENT using Microsoft.Xna.Framework; #endif @@ -12,6 +15,11 @@ namespace Barotrauma public LocalizedString Description { get; private set; } + /// + /// When set to false the AbilityEffects of multiple of the same talent will not be checked and only the first one. + /// + public bool AbilityEffectsStackWithSameTalent; + public readonly Sprite Icon; #if CLIENT @@ -20,6 +28,8 @@ namespace Barotrauma public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); + public readonly ImmutableHashSet Migrations; + public ContentXElement ConfigElement { get; @@ -32,6 +42,8 @@ namespace Barotrauma DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); + AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true); + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); if (!nameIdentifier.IsEmpty) { @@ -48,6 +60,8 @@ namespace Barotrauma : Option.None(); #endif + var migrations = ImmutableHashSet.CreateBuilder(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -60,9 +74,25 @@ namespace Barotrauma TextManager.ConstructDescription(ref tempDescription, subElement); Description = tempDescription; break; + case "migrations": + foreach (var migrationElement in subElement.Elements()) + { + try + { + var migration = TalentMigration.FromXML(migrationElement); + migrations.Add(migration); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e); + } + } + break; } } + Migrations = migrations.ToImmutable(); + if (element.GetAttribute("description") != null) { string description = element.GetAttributeString("description", string.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index b4b26579b..01cd9319c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using System.Xml.Linq; @@ -22,7 +23,7 @@ namespace Barotrauma : string.Empty); } - public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0); + public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2); public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( @@ -176,7 +177,7 @@ namespace Barotrauma } } - public Md5Hash CalculateHash(bool logging = false) + public Md5Hash CalculateHash(bool logging = false, string? name = null, string? modVersion = null) { using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); @@ -203,7 +204,14 @@ namespace Barotrauma break; } } - + + string selectedName = name ?? Name; + if (!selectedName.IsNullOrEmpty()) + { + incrementalHash.AppendData(Encoding.UTF8.GetBytes(selectedName)); + } + incrementalHash.AppendData(Encoding.UTF8.GetBytes(modVersion ?? ModVersion)); + var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset()); if (logging) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 8ea7cb235..3f3249990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -120,7 +120,7 @@ namespace Barotrauma { if (contentPackages.AtLeast(2, cp2 => cp == cp2)) { - throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); + throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 0cb3e03cf..b7c419430 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1878,6 +1878,7 @@ 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)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index e20724d51..813524345 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -104,7 +104,7 @@ namespace Barotrauma /// OnClose = 20, /// - /// Executes when the entity spawns. Only valid for doors. + /// Executes when the entity spawns. Valid for items and characters. /// OnSpawn = 21, /// 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/CheckSelectedItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs deleted file mode 100644 index 6de01c0e8..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedItemAction.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Barotrauma.Extensions; -using System.Collections.Generic; - -namespace Barotrauma -{ - class CheckSelectedAction : BinaryOptionAction - { - public enum SelectedItemType { Primary, Secondary, Any }; - - [Serialize("", IsPropertySaveable.Yes)] - public Identifier CharacterTag { get; set; } - - [Serialize("", IsPropertySaveable.Yes)] - public Identifier TargetTag { get; set; } - - [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] - public SelectedItemType ItemType { get; set; } - - public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - - protected override bool? DetermineSuccess() - { - Character character = null; - if (!CharacterTag.IsEmpty) - { - foreach (var t in ParentEvent.GetTargets(CharacterTag)) - { - if (t is Character c) - { - character = c; - break; - } - } - } - 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."); - return false; - } - if (!TargetTag.IsEmpty) - { - 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."); - 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; - } - if (IsSelected(targetItem)) - { - return true; - } - } - return false; - - bool IsSelected(Item item) - { - return ItemType switch - { - SelectedItemType.Any => character.IsAnySelectedItem(item), - SelectedItemType.Primary => character.SelectedItem == item, - SelectedItemType.Secondary => character.SelectedSecondaryItem == item, - _ => false - }; - } - } - else - { - return ItemType switch - { - SelectedItemType.Any => !character.HasSelectedAnyItem, - SelectedItemType.Primary => character.SelectedItem == null, - SelectedItemType.Secondary => character.SelectedSecondaryItem == null, - _ => false - }; - } - } - - private string GetEventName() - { - return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event"; - } - } -} \ No newline at end of file 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/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 47d8f4ddf..b0655f113 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -225,7 +225,7 @@ namespace Barotrauma } } - while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id) + while (QueuedEventsForNextRound.TryDequeue(out var id)) { var eventPrefab = EventSet.GetEventPrefab(id); if (eventPrefab == null) @@ -879,9 +879,9 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; } + if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; } - if (character.AIController is EnemyAIController enemyAI) + if (character.AIController is EnemyAIController enemyAI) { if (!enemyAI.AIParams.StayInAbyss) { @@ -889,7 +889,7 @@ namespace Barotrauma monsterStrength += enemyAI.CombatStrength; } - if (character.CurrentHull?.Submarine?.Info != null && + if (character.CurrentHull?.Submarine?.Info != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) { @@ -902,10 +902,11 @@ namespace Barotrauma // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. enemyDanger += enemyAI.CombatStrength / 5000.0f; } - } + } else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { - if (character.Submarine != null && + if (character.Submarine != null && + character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } && Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) { //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character), 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/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index c0f125362..5da1a323a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -401,7 +401,7 @@ namespace Barotrauma int reward = GetReward(sub); IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier); crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); finalReward = (int)(reward * missionMoneyGainMultiplier.Value); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index ecc29fcb6..d52095f5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -19,6 +19,8 @@ namespace Barotrauma private float missionDifficulty; private int alternateReward; + private Identifier factionIdentifier; + private Submarine enemySub; private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); @@ -140,8 +142,8 @@ namespace Barotrauma missionDifficulty = level?.Difficulty ?? 0; XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier); - alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); + factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty); string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } @@ -170,11 +172,11 @@ namespace Barotrauma submarineInfo = new SubmarineInfo(contentFile.Path.Value); } - private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) + private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) { return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble())); } - private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand) + private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand) { return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount); } @@ -254,6 +256,7 @@ namespace Barotrauma } } enemySub.ImmuneToBallastFlora = true; + enemySub.EnableFactionSpecificEntities(factionIdentifier); } private void InitPirates() @@ -444,7 +447,7 @@ namespace Barotrauma private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m)); - private bool DeadOrCaptured(Character character) + private static bool DeadOrCaptured(Character character) { return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 3a0b83978..7138946c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -16,8 +16,8 @@ namespace Barotrauma public Item Item; /// - /// Note that the integer values matter here: the state of the target can't go back to a smaller value, - /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// Note that the integer values matter here: + /// a larger or equal value than the RequiredRetrievalState means the item counts as retrieved /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) /// public enum RetrievalState @@ -167,6 +167,8 @@ namespace Barotrauma private readonly List targets = new List(); + public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get @@ -361,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; } } @@ -382,23 +384,46 @@ namespace Barotrauma switch (target.State) { case Target.RetrievalState.None: - if (target.Interacted) { - TrySetRetrievalState(Target.RetrievalState.Interact); - } - var root = target.Item?.GetRootContainer() ?? target.Item; - if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) - { - TrySetRetrievalState(Target.RetrievalState.PickedUp); + if (target.Interacted) + { + TrySetRetrievalState(Target.RetrievalState.Interact); + } + var root = target.Item?.RootContainer ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) + { + TrySetRetrievalState(Target.RetrievalState.PickedUp); + } } + break; case Target.RetrievalState.PickedUp: - Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; - if (parentSub != null) + case Target.RetrievalState.RetrievedToSub: { - if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost) + Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; + + bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; + bool inPlayerInventory = false; + bool playerInFriendlySub = false; + if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1) { - TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + inPlayerInventory = true; + if (character.Submarine != null) + { + playerInFriendlySub = + character.IsInFriendlySub || + (character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true }); + } + } + + if (inPlayerSub || (inPlayerInventory && playerInFriendlySub)) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } + else + { + target.State = Target.RetrievalState.PickedUp; } } break; @@ -406,8 +431,8 @@ namespace Barotrauma void TrySetRetrievalState(Target.RetrievalState retrievalState) { - if (retrievalState < target.State) { return; } - bool wasRetrieved = false; + if (retrievalState < target.State || target.State == retrievalState) { return; } + bool wasRetrieved = target.Retrieved; target.State = retrievalState; //increment the mission state if the target became retrieved if (!wasRetrieved && target.Retrieved) { State = i + 1; } 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 1f3535788..f14e1d6f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -416,7 +416,7 @@ namespace Barotrauma } } #endif - return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)); + return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList(); } public static IReadOnlyCollection FindAllItemsOnPlayerAndSub(Character character) @@ -440,7 +440,7 @@ namespace Barotrauma 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 { } rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -610,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/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 7205df632..50e9ff216 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { @@ -15,7 +16,7 @@ namespace Barotrauma /// /// Maximum amount of reputation loss you can get from damaging outpost NPCs per round /// - public const float MaxReputationLossFromNPCDamage = 10.0f; + public const float MaxReputationLossFromNPCDamage = 20.0f; /// /// Maximum amount of reputation loss you can get from damaging outpost walls per round /// @@ -71,45 +72,50 @@ namespace Barotrauma public float GetReputationChangeMultiplier(float reputationChange) { - if (reputationChange > 0f) + return reputationChange switch { - float reputationGainMultiplier = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); - reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; - } - return reputationGainMultiplier; - } - else if (reputationChange < 0f) + > 0f => GetMultiplierForStatType(StatTypes.ReputationGainMultiplier, Identifier), + < 0f => GetMultiplierForStatType(StatTypes.ReputationLossMultiplier, Identifier), + _ => 1.0f + }; + + static float GetMultiplierForStatType(StatTypes statTypes, Identifier identifier) { - float reputationLossMultiplier = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + float multiplier = 1f; + var crew = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (crew != null && crew.Any()) { - reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); - reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; + multiplier *= 1f + crew.Max(c => c.GetStatValue(statTypes, includeSaved: false)); + multiplier *= 1f + crew.Max(c => c.Info?.GetSavedStatValue(statTypes, identifier) ?? 0); } - return reputationLossMultiplier; + return multiplier; } - return 1.0f; } public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue) { - float currentValue = Value; - float currentReputationChange = currentValue - ReputationAtRoundStart; - if (Math.Abs(currentReputationChange) >= maxReputationChangePerRound && - Math.Sign(currentReputationChange) == Math.Sign(reputationChange)) - { - return; + float prevValue = Value; + //if we're already over the limit, do nothing (assuming the change is in the "same direction" that we've gone over the limit) + if (doesReputationChangeGoOverLimit(prevValue, reputationChange)) + { + return; } + float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange); - if (Math.Abs(newValue - ReputationAtRoundStart) > maxReputationChangePerRound && - Math.Sign(newValue - currentValue) == Math.Sign(newValue - ReputationAtRoundStart)) + if (doesReputationChangeGoOverLimit(newValue, newValue - prevValue)) { newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange); } + Value = newValue; + + bool doesReputationChangeGoOverLimit(float newValue, float change) + { + float totalReputationChange = newValue - ReputationAtRoundStart; + return + Math.Abs(totalReputationChange) > maxReputationChangePerRound && + Math.Sign(totalReputationChange) == Math.Sign(change); + } } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 81b26d81f..9ff3c9aa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -95,8 +95,6 @@ namespace Barotrauma public SubmarineInfo PendingSubmarineSwitch; public bool TransferItemsOnSubSwitch { get; set; } - public bool SwitchedSubsThisRound { get; private set; } - protected Map map; public Map Map { @@ -129,6 +127,8 @@ namespace Barotrauma } } + public Location CurrentLocation => Map?.CurrentLocation; + public Wallet Bank; public LevelData NextLevel @@ -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; } @@ -291,7 +293,6 @@ namespace Barotrauma PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); - SwitchedSubsThisRound = false; } public static int GetHullRepairCost() @@ -739,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 @@ -777,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 @@ -931,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"); @@ -986,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; } @@ -1205,26 +1213,15 @@ 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)); - } - - protected void LoadEvents(XElement element) - { - TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); - TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); - } - - protected XElement SaveEvents() - { - return new XElement("events", - new XAttribute(nameof(EventManager.QueuedEventsForNextRound).ToLowerInvariant(), - string.Join(',', GameMain.GameSession.EventManager.QueuedEventsForNextRound))); + new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels), + new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown)); } public void LogState() @@ -1310,7 +1307,6 @@ namespace Barotrauma TransferItemsBetweenSubs(); } RefreshOwnedSubmarines(); - SwitchedSubsThisRound = true; PendingSubmarineSwitch = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2f5d725da..728dafe88 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -275,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/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 4d0a5186a..a2a9ea63b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -11,6 +11,9 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private const int RateLimitMaxRequests = 20, + RateLimitExpiry = 5; + public enum NetworkHeader { REQUEST_AFFLICTIONS, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index d644c8267..b1effb055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -24,7 +24,7 @@ namespace Barotrauma } - public static readonly List anySlot = new List() { InvSlotType.Any }; + public static readonly List AnySlot = new List() { InvSlotType.Any }; protected bool[] IsEquipped; @@ -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)) { @@ -226,7 +240,7 @@ namespace Barotrauma if (item.AllowedSlots.Contains(InvSlotType.Any)) { var wearable = item.GetComponent(); - if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1) + if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item)) { return false; } @@ -336,7 +350,7 @@ namespace Barotrauma //try to place the item in a LimbSlot.Any slot if that's allowed if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any)) { - int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot); + int freeIndex = GetFreeAnySlot(item, inWrongSlot); if (freeIndex > -1) { PutItem(item, freeIndex, user, true, createNetworkEvent); @@ -393,7 +407,9 @@ namespace Barotrauma return placedInSlot > -1; } - public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot) + public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1; + + private int GetFreeAnySlot(Item item, bool inWrongSlot) { //attempt to stack first for (int i = 0; i < capacity; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 3c26d393f..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; @@ -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 517c3d8f5..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 } } @@ -383,6 +389,8 @@ namespace Barotrauma.Items.Components return; } + + bool isClosing = false; if ((!IsStuck && !IsJammed) || !isOpen) { @@ -521,8 +529,11 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); - if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); } + convexHull = new ConvexHull(doorRect, IsHorizontal, item); + if (Window != Rectangle.Empty) + { + convexHull2 = new ConvexHull(doorRect, IsHorizontal, item); + } UpdateConvexHulls(); #endif } @@ -556,6 +567,12 @@ namespace Barotrauma.Items.Components gap.ConnectedDoor = null; } } + + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture); + OutsideSubmarineFixture = null; + } //no need to remove the gap if we're unloading the whole submarine //otherwise the gap will be removed twice and cause console warnings 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 6620a203c..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); @@ -700,7 +704,8 @@ namespace Barotrauma.Items.Components } Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true); Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine; - int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int currentlyAttachedCount = Item.ItemList.Count( i => i.Submarine == attachSubmarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) @@ -811,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/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 9751820c2..950e8cfae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Projectile.GetSpreadFromPool(); + float spread = GetSpread(character) * projectile.GetSpreadFromPool(); var lastProjectile = LastProjectile; if (lastProjectile != projectile) @@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool()); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool()); } Item.RemoveContained(projectile.Item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 10143e307..71d2510a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] public bool RepairMultiple { get; set; } + [Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")] + public bool RepairMultipleWalls { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } @@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components //stop the ray if it already hit a door/wall and is now about to hit some other type of entity if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; } } + if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent() != null)) { break; } Character hitCharacter = null; if (body.UserData is Limb limb) 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 4f0491cde..b70f89e16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -237,7 +237,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")] public float CombatPriority { get; private set; } /// @@ -435,7 +435,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) @@ -682,9 +682,10 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } - public bool IsNotEmpty(Character user, bool checkContainedItems = true) => - HasRequiredContainedItems(user, addMessage: false) && - (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); + /// + /// Shorthand for !HasRequiredContainedItems() + /// + public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false); public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { @@ -716,20 +717,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; } @@ -765,23 +796,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a16f4572e..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; } @@ -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); @@ -511,25 +528,28 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; + targets.Clear(); + bool wearing = item.GetComponent() is Wearable { IsActive: true }; if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); + targets.AddRange(item.AllPropertyObjects); } if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { - effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + targets.AddRange(contained.AllPropertyObjects); } if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) { - effect.Apply(ActionType.OnContaining, deltaTime, item, character); + targets.Add(character); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - targets.Clear(); effect.AddNearbyTargets(item.WorldPosition, targets); - effect.Apply(ActionType.OnActive, deltaTime, item, targets); } + effect.Apply(ActionType.OnActive, deltaTime, item, targets); + effect.Apply(ActionType.OnContaining, deltaTime, item, targets); + if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); } } } @@ -629,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 50dce4efd..1ad742343 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -14,18 +14,18 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { - const int SpreadCounterWrapAround = 256; - private static readonly ImmutableArray spreadPool; static Projectile() { MTRandom random = new MTRandom(0); - spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); } - public static float GetSpreadFromPool() + public static byte SpreadCounter { get; private set; } + + public static void ResetSpreadCounter() { - return spreadPool[SpreadCounter]; + SpreadCounter = 0; } struct HitscanResult @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components private bool removePending; - public static byte SpreadCounter { get; private set; } + private byte spreadIndex; //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -299,7 +299,8 @@ namespace Barotrauma.Items.Components return; } - SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + spreadIndex = SpreadCounter; + SpreadCounter++; InitProjSpecific(element); } @@ -328,6 +329,12 @@ namespace Barotrauma.Items.Components originalCollisionTargets = item.body.CollidesWith; } + public float GetSpreadFromPool() + { + spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length); + return spreadPool[spreadIndex]; + } + private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f) { if (Item.body == null) { return; } @@ -380,7 +387,7 @@ namespace Barotrauma.Items.Components { #if SERVER launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1))); #endif } } @@ -396,7 +403,6 @@ namespace Barotrauma.Items.Components for (int i = 0; i < HitScanCount; i++) { float launchAngle; - if (StaticSpread) { launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread; @@ -405,7 +411,7 @@ namespace Barotrauma.Items.Components { launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool()); } - SpreadCounter++; + spreadIndex++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) @@ -703,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) @@ -711,7 +717,7 @@ namespace Barotrauma.Items.Components DisableProjectileCollisions(); Unstick(); } - base.Drop(dropper); + base.Drop(dropper, setTransform); } public override void Update(float deltaTime, Camera cam) @@ -885,17 +891,8 @@ namespace Barotrauma.Items.Components return false; } - Vector2 normalizedVel; - Vector2 dir; - if (item.body.LinearVelocity.LengthSquared() < 0.001f) - { - normalizedVel = Vector2.Zero; - dir = contact.Manifold.LocalNormal; - } - else - { - normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity); - } + Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? + contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); //do a raycast in the sub's coordinate space to see if it hit a structure var wallBody = Submarine.PickBody( @@ -904,7 +901,7 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionWall); if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) + Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) @@ -942,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 b73b7cbf5..6f0e8a1cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -29,7 +29,7 @@ namespace Barotrauma.Items.Components private readonly Item item; public readonly bool IsOutput; - + public readonly List Effects; public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires; @@ -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; @@ -292,6 +295,7 @@ namespace Barotrauma.Items.Components public void SendSignal(Signal signal) { + LastSentSignal = signal; enumeratingWires = true; foreach (var wire in wires) { @@ -302,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) { 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 09563de0d..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 { @@ -333,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; @@ -563,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 263bb9f9a..02f0807b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -56,11 +56,9 @@ namespace Barotrauma.Items.Components private float resetUserTimer; - private float aiTargetingGraceTimer; - private float aiFindTargetTimer; private ISpatialEntity currentTarget; - private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAiFindTargetMaxInterval = 1.0f; private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -299,7 +297,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] + [Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] public float AIRange { get; @@ -422,10 +420,7 @@ namespace Barotrauma.Items.Components // Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it. if (lc?.Parent == this) { - if (lightComponents == null) - { - lightComponents = new List(); - } + lightComponents ??= new List(); lightComponents.Add(lc); } } @@ -439,6 +434,8 @@ namespace Barotrauma.Items.Components light.Parent = null; light.Rotation = Rotation - item.RotationRad; light.Light.Rotation = -rotation; + //turret lights are high-prio (don't want the lights to disappear when you're fighting something) + light.Light.PriorityMultiplier *= 10.0f; } } #endif @@ -590,10 +587,6 @@ namespace Barotrauma.Items.Components angularVelocity *= -0.5f; } - if (aiTargetingGraceTimer > 0f) - { - aiTargetingGraceTimer -= deltaTime; - } if (aiFindTargetTimer > 0.0f) { aiFindTargetTimer -= deltaTime; @@ -620,10 +613,18 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime); + private bool isUseBeingCalled; + public override bool Use(float deltaTime, Character character = null) { if (!characterUsable && character != null) { return false; } - return TryLaunch(deltaTime, character); + //prevent an infinite loop if launching triggers a StatusEffect that Uses this item + if (isUseBeingCalled) { return false; } + + isUseBeingCalled = true; + bool wasSuccessful = TryLaunch(deltaTime, character); + isUseBeingCalled = false; + return wasSuccessful; } public float GetPowerRequiredToShoot() @@ -844,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; } } @@ -885,21 +891,9 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - - Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition()); - - //check if there's some other sub between the turret's origin and the launch pos, - //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub - Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true, - customPredicate: (Fixture f) => - { - return f.Body.UserData is not Submarine sub || sub != item.Submarine; - }); - if (pickedBody != null) - { - launchPos = Submarine.LastPickedPosition; - } - projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); + projectile.SetTransform( + ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), + -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -929,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); @@ -1027,6 +1021,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (dist > closestDist) { continue; } if (dist > shootDistance * shootDistance) { continue; } + if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } target = targetItem; closestDist = dist / priority; @@ -1142,9 +1137,12 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead").Value, - identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), - minDurationBetweenSimilar: 10.0f); + if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character) + { + character.Speak(TextManager.Get("DialogTurretTargetDead").Value, + identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), + minDurationBetweenSimilar: 5.0f); + } character.AIController.SelectTarget(null); } @@ -1277,18 +1275,27 @@ namespace Barotrauma.Items.Components Vector2? targetPos = null; float maxDistance = 10000; float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; - // use full range only if we're actively firing - if (aiTargetingGraceTimer <= 0f) - { - shootDistance *= 0.75f; - } - float closestDistance = maxDistance * maxDistance; - bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (!IsValidTarget(currentTarget)) + bool isValidTarget = IsValidTarget(currentTarget); + if (isValidTarget) + { + float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition); + if (dist > closestDistance) + { + isValidTarget = false; + } + else if (currentTarget is Item targetItem) + { + if (!IsTargetItemCloseEnough(targetItem, dist)) + { + isValidTarget = false; + } + } + } + if (!isValidTarget) { currentTarget = null; aiFindTargetTimer = CrewAIFindTargetMinInverval; @@ -1304,11 +1311,17 @@ namespace Barotrauma.Items.Components if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } - if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.Submarine != null) + { + if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + if (enemy.Submarine.Info.IsOutpost) { continue; } + } } // 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) @@ -1330,6 +1343,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (dist > closestDistance) { continue; } if (dist > shootDistance * shootDistance) { continue; } + if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; } if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } targetPos = targetItem.WorldPosition; closestDistance = dist / priority; @@ -1337,14 +1351,7 @@ namespace Barotrauma.Items.Components closestEnemy = null; currentTarget = targetItem; } - if (currentTarget == null) - { - aiFindTargetTimer = CrewAIFindTargetMinInverval; - } - else - { - aiFindTargetTimer = CrewAiFindTargetMaxInterval; - } + aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval; } else if (currentTarget != null) { @@ -1358,6 +1365,10 @@ namespace Barotrauma.Items.Components if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) { targetPos = targetCharacter.CurrentHull.WorldPosition; + if (closestDistance > maxDistance * maxDistance) + { + ResetTarget(); + } } else { @@ -1377,12 +1388,17 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot. - currentTarget = null; - closestEnemy = null; - targetPos = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; + ResetTarget(); } } + void ResetTarget() + { + // Not close enough to shoot. + currentTarget = null; + closestEnemy = null; + targetPos = null; + } } else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { @@ -1404,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; } @@ -1532,10 +1548,11 @@ namespace Barotrauma.Items.Components } character.SetInput(InputType.Shoot, true, true); } - aiTargetingGraceTimer = 5f; return false; } + private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance); + /// /// Turret doesn't consume grid power, directly takes from the batteries on its grid instead. /// @@ -1565,6 +1582,10 @@ namespace Barotrauma.Items.Components { return false; } + if (targetItem.ParentInventory != null) + { + return false; + } } return true; } @@ -1580,6 +1601,7 @@ namespace Barotrauma.Items.Components { if (item.Submarine != null) { + if (item.Submarine.Info.IsOutpost) { return false; } // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; } @@ -1672,7 +1694,7 @@ namespace Barotrauma.Items.Components return angle >= minRotation && angle <= maxRotation; } - private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); protected override void RemoveComponentSpecific() { @@ -1874,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 5a08c3838..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); @@ -742,13 +742,13 @@ namespace Barotrauma stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)) && (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || - existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)); + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent)); } else { swapSuccessful = (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || - existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)) + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.AnySlot, createNetworkEvent)) && stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); @@ -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 b4b5ef815..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) @@ -1587,6 +1650,17 @@ namespace Barotrauma return tags.Contains(tag) || base.Prefab.Tags.Contains(tag); } + public bool HasIdentifierOrTags(IEnumerable identifiersOrTags) + { + if (identifiersOrTags == null) { return false; } + if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; } + foreach (Identifier tag in identifiersOrTags) + { + if (HasTag(tag)) { return true; } + } + return false; + } + public void ReplaceTag(string tag, string newTag) { ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier()); @@ -1616,7 +1690,7 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { - if (string.IsNullOrEmpty(conditional.TargetItemComponent)) + if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) { if (!conditional.Matches(this)) { return false; } } @@ -1624,7 +1698,7 @@ namespace Barotrauma { foreach (ItemComponent component in components) { - if (component.Name != conditional.TargetItemComponent) { continue; } + if (component.Name != conditional.TargetItemComponentName) { continue; } if (!conditional.Matches(component)) { return false; } } } @@ -1770,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(); @@ -1930,7 +2019,7 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) + if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) { bool shouldBeActive = true; foreach (var conditional in ic.IsActiveConditionals) @@ -1994,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); @@ -2023,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 @@ -2036,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(); @@ -2854,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); } @@ -2915,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) { @@ -3550,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; @@ -3727,6 +3809,7 @@ namespace Barotrauma ItemList.Remove(this); dangerousItems.Remove(this); repairableItems.Remove(this); + sonarVisibleItems.Remove(this); cleanableItems.Remove(this); } @@ -3750,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/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 3130414a0..010a675b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -71,9 +71,9 @@ namespace Barotrauma { public EventType EventType => EventType.ItemStat; - public readonly Dictionary Stats; + public readonly Dictionary Stats; - public SetItemStatEventData(Dictionary stats) + public SetItemStatEventData(Dictionary stats) { Stats = stats; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 5c1c5c573..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,6 +830,12 @@ 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) { @@ -1434,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/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs index c6091915f..ba7aef6a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -5,29 +5,48 @@ using System.Collections.Generic; namespace Barotrauma { + [NetworkSerialize] + internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId) : INetSerializableStruct + { + /// + /// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist. + /// + public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId) + => new(stat, talentIdentifier, Option.Some(characterId)); + + /// + /// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied. + /// will always use the highest value for unstackable stats. + /// + public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier) + => new(stat, talentIdentifier, Option.None); + } + internal sealed class ItemStatManager { - private Item item; - - public ItemStatManager(Item item) - { - this.item = item; - } - - [NetworkSerialize] - public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct; - private readonly Dictionary talentStats = new(); + private readonly Item item; - public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent) + public ItemStatManager(Item item) => this.item = item; + + public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent) { if (talent.Character?.ID is not { } characterId || - talent.Prefab?.Identifier is not { } talentIdentifier) + talent.Prefab?.Identifier is not { } talentIdentifier) { return; } + + var identifier = stackable + ? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId) + : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier); + + if (!stackable) { - return; + if (talentStats.TryGetValue(identifier, out float existingValue)) + { + // Always use the highest value for non-stackable stats + if (existingValue > value) { return; } + } } - TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId); talentStats[identifier] = value; #if SERVER @@ -38,21 +57,19 @@ namespace Barotrauma #endif } - // Used for getting the value value from network packet - public void ApplyStat(TalentStatIdentifier identifier, float value) - { - talentStats[identifier] = value; - } + /// + /// Used for setting the value value from network packet; bypassing all validity checks. + /// + public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value; public float GetAdjustedValue(ItemTalentStats stat, float originalValue) { float total = originalValue; + foreach (var (key, value) in talentStats) { - if (key.Stat == stat) - { - total *= value; - } + if (key.Stat != stat) { continue; } + total *= value; } return total; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 59721d257..b1f0392c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -290,6 +290,7 @@ namespace Barotrauma dictionary.Clear(); Hull.EntityGrids.Clear(); Spawner?.Reset(); + Items.Components.Projectile.ResetSpreadCounter(); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 2ce9bd0e8..91797b6ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -10,37 +10,169 @@ using System.Linq; namespace Barotrauma { + /// + /// Explosions are area of effect attacks that can damage characters, items and structures. + /// + /// + /// + /// Used to enable all particle effects without having to specify them one by one. + /// + /// partial class Explosion { public readonly Attack Attack; + /// + /// How much force the explosion applies to the characters. + /// private readonly float force; - private readonly float cameraShake, cameraShakeRange; + /// + /// Intensity of the screen shake effect. + /// + /// + /// + /// 10% of the range if showEffects is true, 0 otherwise. + /// + /// + private readonly float cameraShake; + /// + /// How far away does the camera shake effect reach. + /// + /// + /// + /// Same as attack range if showEffects is true, 0 otherwise. + /// + /// + private readonly float cameraShakeRange; + + /// + /// Color tint to apply to the player's screen when in range of the explosion. + /// private readonly Color screenColor; - private readonly float screenColorRange, screenColorDuration; - private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; + /// + /// How far away can the screen color effect be seen. + /// + /// + /// + /// 10% of the range if showEffects is true, 0 otherwise. + /// + /// + private readonly float screenColorRange; + + /// + /// How long the screen color effect lasts. + /// + private readonly float screenColorDuration; + + /// + /// Whether a spark particle effect is created when the explosion happens. + /// + private bool sparks; + + /// + /// Whether a shockwave particle effect is created when the explosion happens. + /// + private bool shockwave; + + /// + /// Whether a flame particle effect is created when the explosion happens. + /// + private bool flames; + + /// + /// Whether a smoke particle effect is created when the explosion happens. + /// + private bool smoke; + + /// + /// Whether a flash effect is created when the explosion happens. + /// + private bool flash; + + /// + /// Whether a underwater bubble particle effect is created when the explosion happens. + /// + private bool underwaterBubble; + + /// + /// Color of the light source created by the explosion. + /// private readonly Color flashColor; + + /// + /// Whether the explosion plays a tinnitus sound to players who get hit by it. + /// private readonly bool playTinnitus; + + /// + /// Whether the explosion executes 'OnFire' status effects on the items it hits. + /// + /// + /// + /// true if showEffects is true and flames haven't been explicitly set to false, false otherwise. + /// + /// private readonly bool applyFireEffects; + + /// + /// List of item tags that the explosion ignores when applying fire effects. + /// private readonly string[] ignoreFireEffectsForTags; + + /// + /// When set to true, the explosion don't deal less damage when the target is behind a solid object. + /// private readonly bool ignoreCover; + + /// + /// How long the light source created by the explosion lasts. + /// private readonly float flashDuration; + + /// + /// How large the light source created by the explosion is. + /// private readonly float? flashRange; + + /// + /// Identifier of the decal the explosion creates on the background structure it explodes over. + /// Set to empty string to disable. + /// private readonly string decal; + + /// + /// Relative size of the decal created by the explosion. + /// private readonly float decalSize; - private readonly bool applyToSelf; - public bool OnlyInside, OnlyOutside; + /// + /// Whether the explosion only affects characters inside a submarine. + /// + public bool OnlyInside; + /// + /// Whether the explosion only affects characters outside a submarine. + /// + public bool OnlyOutside; + + /// + /// How much the explosion repairs items. + /// private readonly float itemRepairStrength; public readonly HashSet IgnoredSubmarines = new HashSet(); + /// + /// Strength of the EMP effect created by the explosion. + /// public float EmpStrength { get; set; } - + + /// + /// How much damage the explosion does to ballast flora. + /// public float BallastFloraDamage { get; set; } public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f) @@ -66,8 +198,6 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - applyToSelf = element.GetAttributeBool("applytoself", true); - //the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects") bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true); sparks = element.GetAttributeBool("sparks", showEffects); @@ -196,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 @@ -231,7 +363,7 @@ namespace Barotrauma return; } - DamageCharacters(worldPosition, Attack, force, damageSource, attacker, applyToSelf); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -284,8 +416,8 @@ namespace Barotrauma } partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - - private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, bool applyToSelf) + + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } @@ -300,7 +432,6 @@ namespace Barotrauma { continue; } - //if (c == attacker && !applyToSelf) { continue; } if (OnlyInside && c.Submarine == null) { @@ -513,21 +644,28 @@ namespace Barotrauma for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; } + + bool inRange = false; foreach (var cell in destructibleWall.Cells) { if (cell.IsPointInside(worldPosition)) { - destructibleWall.AddDamage(levelWallDamage, worldPosition); - continue; + inRange = true; + break; } foreach (var edge in cell.Edges) { if (MathUtils.LineSegmentToPointDistanceSquared((edge.Point1 + cell.Translation).ToPoint(), (edge.Point2 + cell.Translation).ToPoint(), worldPosition.ToPoint()) < worldRange * worldRange) { - destructibleWall.AddDamage(levelWallDamage, worldPosition); + inRange = true; break; } } + if (inRange) { break; } + } + if (inRange) + { + destructibleWall.AddDamage(levelWallDamage, worldPosition); } } } 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 ed866eed9..77b7a1e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -59,6 +59,8 @@ namespace Barotrauma private Body outsideCollisionBlocker; private float outsideColliderRaycastTimer; + private bool wasRoomToRoom; + public float Open { get { return open; } @@ -205,7 +207,10 @@ namespace Barotrauma outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; -#endif +# endif + + wasRoomToRoom = IsRoomToRoom; + RefreshOutsideCollider(); DebugConsole.Log("Created gap (" + ID + ")"); } @@ -214,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)) { @@ -222,7 +227,7 @@ namespace Barotrauma return; } - base.Move(amount); + base.Move(amount, ignoreContacts); if (!DisableHullRechecks) { FindHulls(); } } @@ -313,27 +318,26 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false); - if (hulls[i] == null) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } + if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } - if (hulls[0] != null || hulls[1] != null) - { - if (hulls[0] == null && hulls[1] != null) - { - (hulls[1], hulls[0]) = (hulls[0], hulls[1]); - } + if (hulls[0] == null && hulls[1] == null) { return; } - flowTargetHull = hulls[0]; - - for (int i = 0; i < 2; i++) - { - if (hulls[i] == null) { continue; } - linkedTo.Add(hulls[i]); - if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); } - } + if (hulls[0] == null && hulls[1] != null) + { + Hull temp = hulls[0]; + hulls[0] = hulls[1]; + hulls[1] = temp; } - RefreshOutsideCollider(); + flowTargetHull = hulls[0]; + + for (int i = 0; i < 2; i++) + { + if (hulls[i] == null) { continue; } + linkedTo.Add(hulls[i]); + if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); + } } private int updateCount; @@ -359,9 +363,14 @@ namespace Barotrauma updateCount = 0; flowForce = Vector2.Zero; - outsideColliderRaycastTimer -= deltaTime; + if (IsRoomToRoom != wasRoomToRoom) + { + RefreshOutsideCollider(); + wasRoomToRoom = IsRoomToRoom; + } + if (open == 0.0f || linkedTo.Count == 0) { lerpedFlowForce = Vector2.Zero; @@ -664,7 +673,7 @@ namespace Barotrauma if (outsideColliderRaycastTimer <= 0.0f) { - UpdateOutsideColliderPos((Hull)linkedTo[0]); + UpdateOutsideColliderState((Hull)linkedTo[0]); outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ? OutsideColliderRaycastIntervalHighPrio : OutsideColliderRaycastIntervalLowPrio; @@ -673,7 +682,7 @@ namespace Barotrauma return outsideCollisionBlocker.Enabled; } - private void UpdateOutsideColliderPos(Hull hull) + private void UpdateOutsideColliderState(Hull hull) { if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; } @@ -710,7 +719,7 @@ namespace Barotrauma if (blockingBody.UserData == Submarine) { return; } outsideCollisionBlocker.Enabled = true; Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition; - float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2; + float colliderRotation = MathUtils.VectorToAngle(Submarine.LastPickedNormal) - MathHelper.PiOver2; outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); } else 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 eb1e44499..720f13874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -429,7 +429,7 @@ namespace Barotrauma public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost; /// - /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1) + /// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reputation into account. /// public static bool IsLoadedFriendlyOutpost => loaded?.Type == LevelData.LevelType.Outpost && @@ -468,8 +468,7 @@ namespace Barotrauma (StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) { - var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation; - return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold; + return GameMain.GameSession.Campaign?.CurrentLocation is not { IsFactionHostile: true }; } return false; } @@ -508,6 +507,8 @@ namespace Barotrauma StartLocation = startLocation; EndLocation = endLocation; + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16); @@ -535,7 +536,6 @@ namespace Barotrauma List sites = new List(); Voronoi voronoi = new Voronoi(1.0); - Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); #if CLIENT renderer = new LevelRenderer(this); @@ -838,14 +838,6 @@ namespace Barotrauma foreach (var pathCell in tunnel.Cells) { MarkEdges(pathCell, tunnel.Type); - foreach (GraphEdge edge in pathCell.Edges) - { - var adjacent = edge.AdjacentCell(pathCell); - if (adjacent != null) - { - MarkEdges(adjacent, tunnel.Type); - } - } if (!pathCells.Contains(pathCell)) { pathCells.Add(pathCell); @@ -1462,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; } @@ -1558,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; @@ -1846,9 +1838,10 @@ namespace Barotrauma bool createCave = //force at least one abyss cave (i == islandCount - 1 && createdCaves == 0) || - Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability; + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < GenerationParams.AbyssIslandCaveProbability; if (!createCave) { + //just create a chunk with no cave float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); Vector2 position = islandArea.Center.ToVector2(); @@ -2808,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; } } @@ -3188,7 +3181,7 @@ 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(); @@ -3513,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/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 6d13e794e..296df273b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -95,6 +95,8 @@ namespace Barotrauma public Reputation Reputation => Faction?.Reputation; + public bool IsFactionHostile => Faction?.Reputation.NormalizedValue < Reputation.HostileThreshold; + public int TurnsInRadiation { get; set; } #region Store @@ -177,29 +179,30 @@ namespace Barotrauma } } + public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo) + { + int quantity = PriceInfo.DefaultAmount; + if (priceInfo.MaxAvailableAmount > 0) + { + quantity = + priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount ? + Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1) : + priceInfo.MaxAvailableAmount; + } + else if (priceInfo.MinAvailableAmount > 0) + { + quantity = priceInfo.MinAvailableAmount; + } + return new PurchasedItem(itemPrefab, quantity, buyer: null); + } + public List CreateStock() { var stock = new List(); foreach (var prefab in ItemPrefab.Prefabs) { if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; } - int quantity = PriceInfo.DefaultAmount; - if (priceInfo.MaxAvailableAmount > 0) - { - if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) - { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); - } - else - { - quantity = priceInfo.MaxAvailableAmount; - } - } - else if (priceInfo.MinAvailableAmount > 0) - { - quantity = priceInfo.MinAvailableAmount; - } - stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); + stock.Add(CreateInitialStockItem(prefab, priceInfo)); } return stock; } @@ -304,6 +307,7 @@ namespace Barotrauma if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); + price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all"))); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); @@ -1277,25 +1281,31 @@ namespace Barotrauma } var stock = new List(store.Stock); var stockToRemove = new List(); - foreach (var item in stock) + + foreach (var itemPrefab in ItemPrefab.Prefabs) { - if (item.ItemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) + var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab); + if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) { - item.Quantity += 1; - if (priceInfo.MaxAvailableAmount > 0) + if (existingStock == null) { - item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + //can be bought from the location, but not in stock - some new item added by an update or mod? + stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo)); } else { - item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + existingStock.Quantity = + Math.Min( + existingStock.Quantity + 1, + priceInfo.MaxAvailableAmount > 0 ? priceInfo.MaxAvailableAmount : CargoManager.MaxQuantity); } } - else + else if (existingStock != null) { - stockToRemove.Add(item); + stockToRemove.Add(existingStock); } } + stockToRemove.ForEach(i => stock.Remove(i)); store.Stock.Clear(); store.Stock.AddRange(stock); 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 dd9c99176..09fcedaf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -312,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; @@ -449,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,7 +507,9 @@ namespace Barotrauma 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)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index c0f436f27..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; } @@ -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; @@ -1440,27 +1435,7 @@ namespace Barotrauma private static void EnableFactionSpecificEntities(Submarine sub, Location location) { - foreach (MapEntity me in MapEntity.mapEntityList) - { - if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; } - - var layerAsIdentifier = me.Layer.ToIdentifier(); - if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) - { - me.HiddenInGame = - location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; -#if CLIENT - //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that - if (me.HiddenInGame && me is Item item) - { - foreach (var lightComponent in item.GetComponents()) - { - lightComponent.Light.Enabled = false; - } - } -#endif - } - } + sub.EnableFactionSpecificEntities(location?.Faction?.Prefab.Identifier ?? Identifier.Empty); } private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) @@ -1615,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 d27dd4471..535ade368 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 @@ -106,9 +106,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; } @@ -120,7 +122,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; @@ -191,14 +193,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; @@ -369,7 +371,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)) { @@ -377,7 +379,7 @@ namespace Barotrauma return; } - base.Move(amount); + base.Move(amount, ignoreContacts); for (int i = 0; i < Sections.Length; i++) { @@ -444,13 +446,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; } } @@ -459,29 +459,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()) @@ -1576,16 +1575,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; } @@ -1634,12 +1633,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), @@ -1649,6 +1648,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 178edc186..57d2d0424 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1,16 +1,14 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.ComponentModel; -using Barotrauma.IO; +using System.Diagnostics; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using System.Xml.Linq; using Voronoi2; @@ -259,7 +257,7 @@ namespace Barotrauma { if (outpost.exitPoints.Any()) { - Rectangle worldBorders = Borders; + Rectangle worldBorders = GetDockedBorders(); worldBorders.Location += WorldPosition.ToPoint(); foreach (var exitPoint in outpost.exitPoints) { @@ -425,18 +423,25 @@ namespace Barotrauma } } + private static readonly HashSet checkSubmarineBorders = new HashSet(); + /// - /// Returns a rect that contains the borders of this sub and all subs docked to it + /// Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts /// - public Rectangle GetDockedBorders(List checkd = null) + public Rectangle GetDockedBorders(bool allowDifferentTeam = true) { - if (checkd == null) { checkd = new List(); } - checkd.Add(this); + checkSubmarineBorders.Clear(); + return GetDockedBordersRecursive(allowDifferentTeam); + } + private Rectangle GetDockedBordersRecursive(bool allowDifferentTeam) + { Rectangle dockedBorders = Borders; - - var connectedSubs = DockedTo.Where(s => !checkd.Contains(s) && !s.Info.IsOutpost).ToList(); - + checkSubmarineBorders.Add(this); + 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 @@ -445,7 +450,7 @@ namespace Barotrauma Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - Rectangle dockedSubBorders = dockedSub.GetDockedBorders(checkd); + Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive(allowDifferentTeam); dockedSubBorders.Location += MathUtils.ToPoint(expectedLocation.Value); dockedBorders.Y = -dockedBorders.Y; @@ -457,28 +462,27 @@ 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) { - if (subs.Contains(dockedSub)) continue; - + if (subs.Contains(dockedSub)) { continue; } subs.Add(dockedSub); dockedSub.GetConnectedSubsRecursive(subs); } @@ -1041,8 +1045,34 @@ namespace Barotrauma #endif } + public void EnableFactionSpecificEntities(Identifier factionIdentifier) + { + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } + + var layerAsIdentifier = me.Layer.ToIdentifier(); + if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) + { + me.HiddenInGame = factionIdentifier != layerAsIdentifier; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } + } + } + public void Update(float deltaTime) { + RefreshConnectedSubs(); + if (Info.IsWreck) { WreckAI?.Update(deltaTime); @@ -1328,38 +1358,22 @@ namespace Barotrauma } /// - /// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level: - /// only use this if the position is in a submarine's local coordinate space! + /// Finds the sub whose borders contain the position /// - public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f) + public static Submarine FindContaining(Vector2 position) { foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; - subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height); - subBorders.Inflate(inflate, inflate); - if (subBorders.Contains(position)) { return sub; } + subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); + + subBorders.Inflate(500.0f, 500.0f); + + if (subBorders.Contains(position)) return sub; } return null; } - - /// - /// Finds the sub whose world borders contain the position. - /// - public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f) - { - foreach (Submarine sub in Loaded) - { - Rectangle worldBorders = sub.Borders; - worldBorders.Location += sub.WorldPosition.ToPoint(); - worldBorders.Inflate(inflate, inflate); - if (RectContains(worldBorders, worldPosition)) { return sub; } - } - return null; - } - - public static Rectangle GetBorders(XElement submarineElement) { Vector4 bounds = Vector4.Zero; @@ -1385,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; @@ -1481,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; @@ -1589,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) @@ -1704,9 +1735,30 @@ namespace Barotrauma } } + Dictionary savedEntities = new Dictionary(); foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { if (!e.ShouldBeSaved) { continue; } + + if (e.Removed) + { + GameAnalyticsManager.AddErrorEventOnce( + "Submarine.SaveToXElement:Removed" + e.Name, + GameAnalyticsManager.ErrorSeverity.Error, + $"Attempted to save a removed entity (\"{e.Name}\"). Duplicate ID: {savedEntities.ContainsKey(e.ID)}"); + DebugConsole.ThrowError($"Error while saving the submarine. Attempted to save a removed entity (\"{e.Name} ({e.ID})\"). The entity will not be saved to avoid corrupting the submarine file."); + continue; + } + if (savedEntities.TryGetValue(e.ID, out MapEntity duplicateEntity)) + { + GameAnalyticsManager.AddErrorEventOnce( + "Submarine.SaveToXElement:DuplicateId" + e.Name, + GameAnalyticsManager.ErrorSeverity.Error, + $"Attempted to save an entity with a duplicate ID ({e.Name}, {duplicateEntity.Name})."); + DebugConsole.ThrowError($"Error while saving the submarine. The entity \"{e.Name}\" has the same ID as \"{duplicateEntity.Name}\" ({e.ID}). The entity will not be saved to avoid corrupting the submarine file."); + continue; + } + if (e is Item item) { if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } @@ -1717,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 { @@ -1726,6 +1777,7 @@ namespace Barotrauma } e.Save(element); + savedEntities.Add(e.ID, e); } Info.CheckSubsLeftBehind(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 4bf6ca6e8..064687270 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -204,6 +204,7 @@ namespace Barotrauma if (item.Submarine != submarine) { continue; } Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); + if (sub.FlippedX) { simPos.X = -simPos.X; } if (item.GetComponent() is Door door) { door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith); @@ -220,11 +221,6 @@ namespace Barotrauma float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); - if (sub.FlippedX) - { - simPos.X = -simPos.X; - } - if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith)); @@ -404,7 +400,7 @@ namespace Barotrauma if (totalForce.Y > 0) { ContactEdge contactEdge = Body?.FarseerBody?.ContactList; - while (contactEdge?.Next != null) + while (contactEdge?.Contact != null) { if (contactEdge.Contact.Enabled && contactEdge.Other.UserData is Submarine otherSubmarine && @@ -412,10 +408,10 @@ namespace Barotrauma contactEdge.Contact.IsTouching) { contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2 points); - if (points[0].Y > Body.SimPosition.Y && + if (points[0].Y > Body.SimPosition.Y && !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID)) { - otherSubmarine.SubBody.forceUpwardsTimer += deltaTime; + otherSubmarine.GetConnectedSubs().ForEach(s => s.SubBody.forceUpwardsTimer += deltaTime); break; } } @@ -471,8 +467,6 @@ namespace Barotrauma } } - totalForcePerFrame = Vector2.Zero; - UpdateDepthDamage(deltaTime); forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay); @@ -558,12 +552,9 @@ namespace Barotrauma return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); } - - private Vector2 totalForcePerFrame; public void ApplyForce(Vector2 force) { Body.ApplyForce(force); - totalForcePerFrame += force; } public void SetPosition(Vector2 position) @@ -701,7 +692,7 @@ namespace Barotrauma } //if all the bodies of a wall have been disabled, we don't need to care about gaps (can always pass through) - if (!(contact.FixtureA.UserData is Structure wall) || !wall.AllSectionBodiesDisabled()) + if (contact.FixtureA.UserData is not Structure wall || !wall.AllSectionBodiesDisabled()) { var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); @@ -820,7 +811,7 @@ namespace Barotrauma } } - private IEnumerable GetLevelContacts(PhysicsBody body) + private static IEnumerable GetLevelContacts(PhysicsBody body) { ContactEdge contactEdge = body.FarseerBody.ContactList; while (contactEdge?.Contact != null) 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/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs index f28300c70..7398a7f21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs @@ -94,6 +94,7 @@ namespace Barotrauma { List bytes = new List(); byte currentByte; + do { if (inc.BitPosition >= inc.LengthBits) 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 d4ed2e781..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 = 2400, + 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; } 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/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 2e3444126..c88eb0204 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -25,8 +25,7 @@ namespace Barotrauma { None = 0, Transparent = 1, - Opaque = 2, - BlockOutsideView = 3 + Opaque = 2 } public enum VoiceMode @@ -53,13 +52,19 @@ namespace Barotrauma { Config config = new Config { +#if SERVER + //server defaults to English, clients get a prompt to select a language Language = TextManager.DefaultLanguage, +#else + Language = LanguageIdentifier.None, +#endif SubEditorUndoBuffer = 32, MaxAutoSaves = 8, AutoSaveIntervalSeconds = 300, SubEditorBackground = new Color(13, 37, 69, 255), EnableSplashScreen = true, PauseOnFocusLost = true, + RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, EnableMouseLook = true, @@ -101,11 +106,18 @@ namespace Barotrauma Config retVal = fallback ?? GetDefault(); retVal.DeserializeElement(element); +#if SERVER + //server defaults to English, clients get a prompt to select a language if (retVal.Language == LanguageIdentifier.None) { 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 @@ -141,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/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index cd9a41b1b..514e207b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,246 +1,71 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Collections.Immutable; +using System; using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma { - /// - /// Conditionals are used by some in-game mechanics to require one - /// or more conditions to be met for those mechanics to be active. - /// For example, some StatusEffects use Conditionals to only trigger - /// if the affected character is alive. - /// - sealed class PropertyConditional + // TODO: This class should be refactored: + // - Use XElement instead of XAttribute in the constructor + // - Simplify, remove unnecessary conversions + // - Improve the flow so that the logic is undestandable. + // - Maybe add some test cases for the operators? + class PropertyConditional { - // TODO: Make this testable and add tests - - /// - /// Category of properties to check against - /// public enum ConditionType { - /// - /// Depending on what's available, check against either one - /// of the target object's properties or the strength of an - /// affliction. - /// - /// The target object's available properties depend on how that - /// object is defined in the [source code](https://github.com/Regalis11/Barotrauma). - /// - /// This is not applicable if the element contains the attribute - /// `SkillRequirement="true"`. - /// - /// - /// - /// - /// - PropertyValueOrAffliction, - - /// - /// Check against the target character's skill with the same name as the attribute. - /// - /// This is only applicable if the element contains the attribute - /// `SkillRequirement="true"`. - /// - /// - /// - /// - /// - SkillRequirement, - - /// - /// Check against the name of the target. - /// + Uncertain, + PropertyValue, Name, - - /// - /// Check against the species identifier of the target. Only works on characters. - /// SpeciesName, - - /// - /// Check against the species group of the target. Only works on characters. - /// SpeciesGroup, - - /// - /// Check against the target's tags. Only works on items. - /// - /// Several tags can be checked against by using a comma-separated list. - /// HasTag, - - /// - /// Check against the tags of the target's active status effects. - /// - /// Several tags can be checked against by using a comma-separated list. - /// HasStatusTag, - - /// - /// Check against the target's specifier tags. In the vanilla game, these are the head index - /// and gender. See human.xml for more details. - /// - /// Several tags can be checked against by using a comma-separated list. - /// HasSpecifierTag, - - /// - /// Check against the target's entity type. - /// - /// The currently supported values are "character", "limb", "item", "structure" and "null". - /// + Affliction, EntityType, - - /// - /// Check against the target's limb type. See . - /// - LimbType + LimbType, + SkillRequirement } - public enum LogicalOperatorType + public enum Comparison { And, Or } - /// - /// There are several ways to compare properties to values. The comparison operator - /// to use can be specified by placing one of the following before the value to compare - /// against. - /// - public enum ComparisonOperatorType + public enum OperatorType { None, - - /// - /// Require that the property being checked equals the given value. - /// - /// This is the default operator used if none is specified. - /// Equals, - - /// - /// Require that the property being checked doesn't equal the given value. - /// NotEquals, - - /// - /// Require that the property being checked is less than the given value. - /// - /// This can only be used to compare with numeric object properties, - /// affliction strengths and skill levels. - /// LessThan, - - /// - /// Require that the property being checked is less than or equal to the given value. - /// - /// This can only be used to compare with numeric object properties, - /// affliction strengths and skill levels. - /// LessThanEquals, - - /// - /// Require that the property being checked is greater than the given value. - /// - /// This can only be used to compare with numeric object properties, - /// affliction strengths and skill levels. - /// GreaterThan, - - /// - /// Require that the property being checked is greater than or equal to the given value. - /// - /// This can only be used to compare with numeric object properties, - /// affliction strengths and skill levels. - /// GreaterThanEquals } public readonly ConditionType Type; - public readonly ComparisonOperatorType ComparisonOperator; + public readonly OperatorType Operator; public readonly Identifier AttributeName; public readonly string AttributeValue; - public readonly ImmutableArray AttributeValueAsTags; + public readonly string[] SplitAttributeValue; public readonly float? FloatValue; - /// - /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. - /// Only works on items. - /// - public readonly string TargetItemComponent; + public readonly string TargetItemComponentName; - /// - /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character - /// + // Only used by attacks public readonly bool TargetSelf; - /// - /// If set to true, the conditionals defined by this element check against the entity containing the target. - /// + // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) public readonly bool TargetContainer; - - /// - /// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container. - /// + // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. public readonly bool TargetGrandParent; - /// - /// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items. - /// public readonly bool TargetContainedItem; - public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) - { - var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); - var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); - var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false); - var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false); - var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false); - - ConditionType? overrideConditionType = null; - if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false)) - { - overrideConditionType = ConditionType.SkillRequirement; - } - - foreach (var attribute in element.Attributes()) - { - if (!IsValid(attribute)) { continue; } - if (predicate != null && !predicate(attribute)) { continue; } - - var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); - if (string.IsNullOrWhiteSpace(attributeValueString)) - { - DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); - continue; - } - - var conditionType = overrideConditionType ?? - (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) - ? type - : ConditionType.PropertyValueOrAffliction); - - yield return new PropertyConditional( - attributeName: attribute.NameAsIdentifier(), - comparisonOperator: comparisonOperator, - attributeValue: attributeValueString, - targetItemComponent: targetItemComponent, - targetSelf: targetSelf, - targetContainer: targetContainer, - targetGrandParent: targetGrandParent, - targetContainedItem: targetContainedItem, - conditionType: conditionType); - } - } - - private static bool IsValid(XAttribute attribute) + // Remove this after refactoring + public static bool IsValid(XAttribute attribute) { switch (attribute.Name.ToString().ToLowerInvariant()) { @@ -257,63 +82,60 @@ namespace Barotrauma } } - private PropertyConditional( - Identifier attributeName, - ComparisonOperatorType comparisonOperator, - string attributeValue, - string targetItemComponent, - bool targetSelf, - bool targetContainer, - bool targetGrandParent, - bool targetContainedItem, - ConditionType conditionType) + // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) + public PropertyConditional(XAttribute attribute) { - AttributeName = attributeName; + AttributeName = attribute.NameAsIdentifier(); + string attributeValueString = attribute.Value; + if (string.IsNullOrWhiteSpace(attributeValueString)) + { + DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); + return; + } + string valueString = attributeValueString; + string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } + Operator = GetOperatorType(splitString[0]); - TargetItemComponent = targetItemComponent; - TargetSelf = targetSelf; - TargetContainer = targetContainer; - TargetGrandParent = targetGrandParent; - TargetContainedItem = targetContainedItem; + if (Operator == OperatorType.None) + { + Operator = OperatorType.Equals; + valueString = attributeValueString; + } - Type = conditionType; + TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); + TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); + TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); + TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); + TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); - ComparisonOperator = comparisonOperator; - AttributeValue = attributeValue; - AttributeValueAsTags = AttributeValue.Split(',') - //, options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(s => s.ToIdentifier()) - .ToImmutableArray(); + if (!Enum.TryParse(AttributeName.Value, true, out Type)) + { + Type = ConditionType.Uncertain; + } + + if (attribute.Parent.GetAttributeBool("skillrequirement", false)) + { + Type = ConditionType.SkillRequirement; + } + + AttributeValue = valueString; + SplitAttributeValue = valueString.Split(','); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; } } - public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) - { - str ??= ""; - - ComparisonOperatorType op = ComparisonOperatorType.Equals; - string conditionStr = str; - if (str.IndexOf(' ') is var i and >= 0) - { - op = GetComparisonOperatorType(str[..i]); - if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; } - else { op = ComparisonOperatorType.Equals; } - } - return (op, conditionStr); - } - - public static ComparisonOperatorType GetComparisonOperatorType(string op) + public static OperatorType GetOperatorType(string op) { //thanks xml for not letting me use < or > in attributes :( - switch (op.ToLowerInvariant()) + switch (op) { case "e": case "eq": case "equals": - return ComparisonOperatorType.Equals; + return OperatorType.Equals; case "ne": case "neq": case "notequals": @@ -321,280 +143,311 @@ namespace Barotrauma case "!e": case "!eq": case "!equals": - return ComparisonOperatorType.NotEquals; + return OperatorType.NotEquals; case "gt": case "greaterthan": - return ComparisonOperatorType.GreaterThan; + return OperatorType.GreaterThan; case "lt": case "lessthan": - return ComparisonOperatorType.LessThan; + return OperatorType.LessThan; case "gte": case "gteq": case "greaterthanequals": - return ComparisonOperatorType.GreaterThanEquals; + return OperatorType.GreaterThanEquals; case "lte": case "lteq": case "lessthanequals": - return ComparisonOperatorType.LessThanEquals; + return OperatorType.LessThanEquals; default: - return ComparisonOperatorType.None; + return OperatorType.None; } } - private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals; - public bool Matches(ISerializableEntity? target) + public bool Matches(ISerializableEntity target) { - return TargetContainedItem - ? MatchesContained(target) - : MatchesDirect(target); + return Matches(target, TargetContainedItem); } - private bool MatchesContained(ISerializableEntity? target) + public bool Matches(ISerializableEntity target, bool checkContained) { - var containedItems = target switch + var type = Type; + if (type == ConditionType.Uncertain) { - Item item - => item.ContainedItems, - ItemComponent ic - => ic.Item.ContainedItems, - Character {Inventory: { } characterInventory} - => characterInventory.AllItems, - _ - => Enumerable.Empty() - }; - foreach (var containedItem in containedItems) - { - if (MatchesDirect(containedItem)) { return true; } + type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) + ? ConditionType.Affliction + : ConditionType.PropertyValue; } - return false; - } - - private bool MatchesDirect(ISerializableEntity? target) - { - Character? targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - switch (Type) + + if (checkContained) { - case ConditionType.PropertyValueOrAffliction: - // First try checking for a property belonging to the target - if (target?.SerializableProperties != null - && target.SerializableProperties.TryGetValue(AttributeName, out var property)) + if (target is Item item) + { + foreach (var containedItem in item.ContainedItems) { - return PropertyMatchesRequirement(target, property); + if (Matches(containedItem, checkContained: false)) { return true; } } - // Then try checking for an affliction affecting the target - if (targetChar is { CharacterHealth: { } health }) + return false; + } + else if (target is Items.Components.ItemComponent ic) + { + foreach (var containedItem in ic.Item.ContainedItems) { - var affliction = health.GetAffliction(AttributeName.ToIdentifier()); - float afflictionStrength = affliction?.Strength ?? 0f; + if (Matches(containedItem, checkContained: false)) { return true; } + } + return false; + } + else if (target is Character character) + { + if (character.Inventory == null) { return false; } + foreach (var containedItem in character.Inventory.AllItems) + { + if (Matches(containedItem, checkContained: false)) { return true; } + } + return false; + } + } - return NumberMatchesRequirement(afflictionStrength); - } - return ComparisonOperatorIsNotEquals; - case ConditionType.SkillRequirement: - if (targetChar != null) + switch (type) + { + case ConditionType.PropertyValue: + SerializableProperty property; + if (target?.SerializableProperties == null) { return Operator == OperatorType.NotEquals; } + if (target.SerializableProperties.TryGetValue(AttributeName, out property)) { - float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); - - return NumberMatchesRequirement(skillLevel); + return Matches(target, property); } - return ComparisonOperatorIsNotEquals; + return false; + case ConditionType.Name: + if (target == null) { return Operator == OperatorType.NotEquals; } + return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); case ConditionType.HasTag: - return ItemMatchesTagCondition(target); + if (target == null) { return Operator == OperatorType.NotEquals; } + return MatchesTagCondition(target); case ConditionType.HasStatusTag: - if (target == null) { return ComparisonOperatorIsNotEquals; } - - // TODO: revisit this. As written, the current behavior is: - // - ComparisonOperatorType.Equals: true when any effects have all tags - // - ComparisonOperatorType.NotEquals: true when none of the effects have any of the tags + if (target == null) { return Operator == OperatorType.NotEquals; } int matches = 0; - - foreach (var durationEffect in StatusEffect.DurationList) + foreach (DurationListElement durationEffect in StatusEffect.DurationList) { if (!durationEffect.Targets.Contains(target)) { continue; } - if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { matches++; } + foreach (string tag in SplitAttributeValue) + { + if (durationEffect.Parent.HasTag(tag)) + { + matches++; + } + } } - - foreach (var delayedEffect in DelayedEffect.DelayList) + foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) { if (!delayedEffect.Targets.Contains(target)) { continue; } - if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { matches++; } + foreach (string tag in SplitAttributeValue) + { + if (delayedEffect.Parent.HasTag(tag)) + { + matches++; + } + } } + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + case ConditionType.HasSpecifierTag: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character { Info: { } characterInfo })) { return false; } - return ComparisonOperatorIsNotEquals - ? matches >= StatusEffect.DurationList.Count + DelayedEffect.DelayList.Count - : matches > 0; + return (Operator == OperatorType.Equals) == + SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); + } + case ConditionType.SpeciesName: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character targetCharacter)) { return false; } + return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); + } + case ConditionType.SpeciesGroup: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character targetCharacter)) { return false; } + return (Operator == OperatorType.Equals) == CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Group); + } + case ConditionType.EntityType: + switch (AttributeValue) + { + case "character": + case "Character": + return (Operator == OperatorType.Equals) == target is Character; + case "limb": + case "Limb": + return (Operator == OperatorType.Equals) == target is Limb; + case "item": + case "Item": + return (Operator == OperatorType.Equals) == target is Item; + case "structure": + case "Structure": + return (Operator == OperatorType.Equals) == target is Structure; + case "null": + return (Operator == OperatorType.Equals) == (target == null); + default: + return false; + } + case ConditionType.LimbType: + { + if (!(target is Limb limb)) + { + return false; + } + else + { + return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); + } + } + case ConditionType.Affliction: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + Character targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + if (targetChar != null) + { + var health = targetChar.CharacterHealth; + if (health == null) { return false; } + var affliction = health.GetAffliction(AttributeName.ToIdentifier()); + float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; + + return ValueMatchesRequirement(afflictionStrength); + } + } + return false; + case ConditionType.SkillRequirement: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + if (target is Character targetChar) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return ValueMatchesRequirement(skillLevel); + } + } + return false; default: - bool equals = CheckOnlyEquality(target); - return ComparisonOperatorIsNotEquals - ? !equals - : equals; + return false; } } - private bool CheckOnlyEquality(ISerializableEntity? target) + private bool ValueMatchesRequirement(float testedValue) { - switch (Type) + if (FloatValue.HasValue) { - case ConditionType.Name: - if (target == null) { return false; } - - return target.Name == AttributeValue; - case ConditionType.HasSpecifierTag: + float value = FloatValue.Value; + switch (Operator) { - if (target is not Character {Info: { } characterInfo}) - { - return false; - } - - return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains); - } - case ConditionType.SpeciesName: - { - if (target is not Character targetCharacter) - { - return false; - } - - return targetCharacter.SpeciesName == AttributeValue; - } - case ConditionType.SpeciesGroup: - { - if (target is not Character targetCharacter) - { - return false; - } - - return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group); - } - case ConditionType.EntityType: - return AttributeValue.ToLowerInvariant() switch - { - "character" - => target is Character, - "limb" - => target is Limb, - "item" - => target is Item, - "structure" - => target is Structure, - "null" - => target == null, - _ - => false - }; - case ConditionType.LimbType: - { - return target is Limb limb - && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType) - && attributeLimbType == limb.type; + case OperatorType.Equals: + return testedValue == value; + case OperatorType.GreaterThan: + return testedValue > value; + case OperatorType.GreaterThanEquals: + return testedValue >= value; + case OperatorType.LessThan: + return testedValue < value; + case OperatorType.LessThanEquals: + return testedValue <= value; + case OperatorType.NotEquals: + return testedValue != value; } } return false; } - private bool SufficientTagMatches(int matches) + private bool MatchesTagCondition(ISerializableEntity target) { - return ComparisonOperatorIsNotEquals - ? matches <= 0 - : matches >= AttributeValueAsTags.Length; - } - - private bool ItemMatchesTagCondition(ISerializableEntity? target) - { - if (target is not Item item) { return ComparisonOperatorIsNotEquals; } + if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } int matches = 0; - foreach (var tag in AttributeValueAsTags) + foreach (string tag in SplitAttributeValue) { - if (item.HasTag(tag)) { matches++; } + if (item.HasTag(tag)) + { + matches++; + } } - return SufficientTagMatches(matches); + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - public bool TargetTagMatchesTagCondition(Identifier targetTag) + public bool MatchesTagCondition(Identifier targetTag) { if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; - foreach (var tag in AttributeValueAsTags) + foreach (string tag in SplitAttributeValue) { - if (targetTag == tag) { matches++; } + if (targetTag == tag) + { + matches++; + } } - return SufficientTagMatches(matches); + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) - { - int matches = 0; - foreach (var tag in AttributeValueAsTags) - { - if (statusEffect.HasTag(tag.Value)) { matches++; } - } - return SufficientTagMatches(matches); - } - - private bool NumberMatchesRequirement(float testedValue) - { - if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } - float value = FloatValue.Value; - - return ComparisonOperator switch - { - ComparisonOperatorType.Equals - => MathUtils.NearlyEqual(testedValue, value), - ComparisonOperatorType.NotEquals - => !MathUtils.NearlyEqual(testedValue, value), - ComparisonOperatorType.GreaterThan - => testedValue > value, - ComparisonOperatorType.GreaterThanEquals - => testedValue >= value, - ComparisonOperatorType.LessThan - => testedValue < value, - ComparisonOperatorType.LessThanEquals - => testedValue <= value, - _ - => false - }; - } - - private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property) + // TODO: refactor and add tests + private bool Matches(ISerializableEntity target, SerializableProperty property) { Type type = property.PropertyType; if (type == typeof(float) || type == typeof(int)) { float floatValue = property.GetFloatValue(target); - return NumberMatchesRequirement(floatValue); + switch (Operator) + { + case OperatorType.Equals: + return MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.NotEquals: + return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.GreaterThan: + return floatValue > FloatValue.Value; + case OperatorType.LessThan: + return floatValue < FloatValue.Value; + case OperatorType.GreaterThanEquals: + return floatValue >= FloatValue.Value; + case OperatorType.LessThanEquals: + return floatValue <= FloatValue.Value; + } + return false; } - switch (ComparisonOperator) + switch (Operator) { - case ComparisonOperatorType.Equals: - case ComparisonOperatorType.NotEquals: - bool equals; - if (type == typeof(bool)) - { - bool attributeValueBool = AttributeValue.IsTrueString(); - equals = property.GetBoolValue(target) == attributeValueBool; - } - else + case OperatorType.Equals: { + if (type == typeof(bool)) + { + return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); + } var value = property.GetValue(target); - equals = AreValuesEquivalent(value, AttributeValue); + return Equals(value, AttributeValue); } - - return ComparisonOperatorIsNotEquals - ? !equals - : equals; - default: + case OperatorType.NotEquals: + { + if (type == typeof(bool)) + { + return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); + } + var value = property.GetValue(target); + return !Equals(value, AttributeValue); + } + case OperatorType.GreaterThan: + case OperatorType.LessThanEquals: + case OperatorType.LessThan: + case OperatorType.GreaterThanEquals: DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + "Make sure the type of the value set in the config files matches the type of the property."); - return false; + break; } + return false; - static bool AreValuesEquivalent(object? value, string desiredValue) + static bool Equals(object value, string desiredValue) { if (value == null) { @@ -602,7 +455,7 @@ namespace Barotrauma } else { - return (value.ToString() ?? "").Equals(desiredValue); + return value.ToString().Equals(desiredValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 292976e65..20831e9be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -495,7 +495,7 @@ namespace Barotrauma public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; - private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or; + private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); @@ -516,6 +516,14 @@ namespace Barotrauma /// 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 @@ -674,13 +682,7 @@ namespace Barotrauma private readonly List giveExperiences = new List(); private readonly List giveSkills = new List(); - /// - /// 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. - /// - public readonly float Duration; + public float Duration => duration; /// /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. @@ -735,7 +737,7 @@ namespace Barotrauma AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); Interval = element.GetAttributeFloat("interval", 0.0f); - Duration = element.GetAttributeFloat("duration", 0.0f); + duration = element.GetAttributeFloat("duration", 0.0f); disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); setValue = element.GetAttributeBool("setvalue", false); Stackable = element.GetAttributeBool("stackable", true); @@ -832,7 +834,7 @@ namespace Barotrauma break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) { DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } @@ -847,7 +849,7 @@ namespace Barotrauma } break; case "tags": - if (Duration <= 0.0f || setValue) + 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 @@ -926,7 +928,13 @@ namespace Barotrauma } break; case "conditional": - propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement)); + foreach (XAttribute attribute in subElement.Attributes()) + { + if (PropertyConditional.IsValid(attribute)) + { + propertyConditionals.Add(new PropertyConditional(attribute)); + } + } break; case "affliction": AfflictionPrefab afflictionPrefab; @@ -1063,7 +1071,7 @@ namespace Barotrauma } else { - return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t))); + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); } } @@ -1159,56 +1167,97 @@ namespace Barotrauma { if (conditionals.Count == 0) { return true; } if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } - - bool shortCircuitValue = conditionalLogicalOperator switch + switch (conditionalComparison) { - PropertyConditional.LogicalOperatorType.Or => true, - PropertyConditional.LogicalOperatorType.And => false, - _ => throw new NotImplementedException() - }; - - for (int i = 0; i < conditionals.Count; i++) - { - var pc = conditionals[i]; - if (!pc.TargetContainer || targetingContainer) - { - if (AnyTargetMatches(targets, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } - continue; - } - - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals; - if (comparisonIsNeq == shortCircuitValue) + case PropertyConditional.Comparison.Or: + for (int i = 0; i < conditionals.Count; i++) { - return shortCircuitValue; + var pc = conditionals[i]; + if (pc.TargetContainer && !targetingContainer) + { + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + return true; + } + continue; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) + { + //if we're checking for tags, just check the Item object, not the ItemComponents + if (pc.Matches(container)) { return true; } + } + else + { + if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } + } + } + if (owner is Character character && pc.Matches(character)) { return true; } + } + else + { + if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } + } } - continue; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) + return false; + case PropertyConditional.Comparison.And: + for (int i = 0; i < conditionals.Count; i++) { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (pc.Matches(container) == shortCircuitValue) { return shortCircuitValue; } + var pc = conditionals[i]; + if (pc.TargetContainer && !targetingContainer) + { + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + continue; + } + return false; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) + { + //if we're checking for tags, just check the Item object, not the ItemComponents + if (!pc.Matches(container)) { return false; } + } + else + { + if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } + } + } + if (owner is Character character && !pc.Matches(character)) { return false; } + } + else + { + if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } + } } - else - { - if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } - } - } - if (owner is Character character && pc.Matches(character) == shortCircuitValue) { return shortCircuitValue; } + return true; + default: + throw new NotImplementedException(); } - return !shortCircuitValue; static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) { @@ -1325,13 +1374,13 @@ namespace Barotrauma if (!IsValidTarget(target)) { return; } - if (Duration > 0.0f && !Stackable) + if (duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); if (existingEffect != null) { - existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); return; } } @@ -1370,13 +1419,13 @@ namespace Barotrauma return; } - if (Duration > 0.0f && !Stackable) + if (duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); + existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); return; } } @@ -1570,9 +1619,9 @@ namespace Barotrauma } } - if (Duration > 0.0f) + if (duration > 0.0f) { - DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); + DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); } else { @@ -1611,7 +1660,7 @@ namespace Barotrauma { var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method - if (Duration > 0) { break; } + if (duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in 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/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 be94e2df8..75b4957f2 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,17 +1,250 @@ --------------------------------------------------------------------------------------------------------- -v1.1.4.0 (unstable) +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 +--------------------------------------------------------------------------------------------------------- + +- Fixed "hash mismatch" errors when trying to enable a mod that's been updated in the most recent patch. +- Fixed "the submarine contains entities with duplicate IDs" error message when loading a submarine that contains multiple shuttles/drones. + +--------------------------------------------------------------------------------------------------------- +v1.0.13.1 +--------------------------------------------------------------------------------------------------------- + +- Updated localizations. +- Fixes to Japanese and Russian translations. +- Fixed Azimuth not going up in Silent Running mode. + +--------------------------------------------------------------------------------------------------------- +v1.0.13.0 +--------------------------------------------------------------------------------------------------------- + +Misc changes and improvements: +- NPCs who offer services don't get turned hostile regardless of your reputation. If you've got money, they'll be happy to serve! +- Attacking outpost NPCs can't decrease your reputation by more than 20 points per round. Works as a safeguard against enormous reputation losses e.g. in the case of a trigger-happy griefer or a nuclear mishap. +- Destroying outpost walls can't decrease your reputation by more than 10 points per round. +- Outpost NPCs don't allow players to grab them for longer than 10 seconds to prevent being able to drag them around the outpost. +- Minor visual improvements to biomes: biome-specific outpost levels (instead of all outpost levels looking like Cold Caverns), more level objects in Hydrothermal Wastes and the Great Sea. +- Spawn abyss and combat suits in enemy subs and wrecks instead of normal ones in later biomes. +- Fixed minerals still sometimes spawning on the wrong side of cave walls (when the other side of the wall is outside the boundaries of the level). +- Fixed pressure stabilizer only affecting the player for 100 seconds, instead of the intended 1000 seconds (16 mins). +- Fixed clients not getting assigned the "None" permission preset when using a language other than English (meaning it wasn't possible to customize what permissions clients have by default). +- Fixed enabling cheats not actually disabling Steam achievements for the rest of the campaign (it was possible to re-enable unlocking achievements by saving and reloading). - Fixed Tormsdale mission not completing unless you bring the item to the sub. Now retrieving the item counts as "bringing it to the sub" in friendly outposts. +- Optimized/simplified exosuit and FB3000 status effects. +- Made huskified humans' items move to a duffel bag on death. +- Increased the priority of turret lights to prevent them from getting hidden when using a low light limit. +- Reduced the minimum mass required for a character to be visible with thermal goggles, always show at least the main limb regardless of the mass. Fixes thresher hatchlings being invisible to the goggles. +- Alien power cells can be deconstructed. -WIP countermeasures against multiplayer exploits (feedback appreciated!): +Bots: +- Fixed bots targeting crawler eggs when they are inside an inventory (also in the Broodmother's inventory). +- Fixed bots being allowed to shoot items from very long distances. +- Fixed bots wasting ammunition on crawler eggs that are not dangerously close to the sub. +- Fixed bots using an unintentionally long delay before shooting. +- Fixed bots targeting (but not shooting) enemies inside abandoned outposts. +- Adjusted the delays and the targeting ranges. Remove the "grace" distance modifier. Improves the bots' general usage of turrets. +- Fixed "fight intruders" order causing bots to attack enemies in abandoned outposts again. +- Bots are allowed to use meds from an unconscious patient's inventory. +- Fixed bots falling off the ledge in DockingModule_02_Colony (again). +- Fixed bots not prioritizing the leaks as they should when multiple bots are fixing leaks simultaneously. +- Fixed bots not preferring the items that lie on the ground. +- Fixed bots often not being able to reach small items like battery cells or rifle rounds that lie on the ground. +- Fixed bots sometimes holding the flashlight in their mouth. +- Fixed bots acting weird when picking up items and having their inventory full. +- Fixed bots not being able to find an alternative container for the items they are cleaning up, when the path to the preferred container is blocked. Didn't affect bots getting a specific item, like a diving suit when they need it. +- Bots are no longer allowed to wear the items (other than in hands) when they are cleaning them up. +- Improved bots' abilities to find items. They should be much quicker at it than previously. +- Possibly fixed bots sometimes ignoring targets that they shouldn't ignore. +- Fixed bots not being able to change the oxygen tanks when they are in a safe room with no enemies. +- Fixed bots using both diving mask and the suit simultaneously. +- Fixed bots holding to old paths that shouldn't be valid anymore (= not complying when you order them to follow outside of the submarine). +- Fixed bots not being able to use pathing when the player has just controlled them, leaving them in a ruin, wreck, or a beacon. Only happened when the bot hadn't been AI controlled before during the round. +- Fixed bots using the waypoints not linked to any sub when they should use the waypoints linked to a sub and vice versa. +- Fixed bots trying to use the gaps when they could just use a path to get to the target. +- Fixed missing links between doors and waypoints in Alien_Entrance3. +- Fixed bots sometimes not being able to release from the ladders when they were trying to exit a wreck or beacon to get back to their own submarine. +- Fixed bots sometimes still idling on ladders, which they shouldn't do (There are some unaddressed cases where it might seem like they'd do this, but actually don't. They just can't reach the buttons linked to the door.) +- Fixed bots sometimes failing to climb up the ladder all the way up and falling down just before reaching next floor. Happened only in the idle state. +- Fixed bots sometimes getting stuck on the long ladder in EngineeringModule_02_Colony. +- Fixed waypoints on ladders leading to a hatch on many subs (using old waypoints), which caused the bots to fail to reach the hatch while trying to fix it. +- Fixed bots not being able to operate properly in high pressure levels when they don't have a suit that protects from the pressure (e.g. a regular diving suit later in the campaign). +- Bots now prioritize suits that give a better pressure protection (there's a separate attribute for it in the item definition: called "botpriority", which can and should be taken into account with the modified items). +- Fixed bots not healing theirselves while swimming outside of the submarine (they only do that when the wounds are relatively severe). +- Fixed bots treating theirselves when there's a medic onboard, which is currently unable to treat them (if the medic is able to treat the bot, they should not try to heal theirselves). +- Fixed bots ignoring targets that are currently fixing leaks (which was intentional, but seems to have been a bad decision). +- Added new dialogue for the bots about the lethal pressure levels and insufficient protection for it. +- Fixed some minor buts regarding the dialogue. +- Fixed bots not being able to use harpoon coil rifles properly. +- Fixed bots accepting random items as weapons when they can't find any weapon. Note that many tools, like welding tools and wrenches, can intentionally be used as weapons. +- Fixed prepare for expedition order and another order, like clean items, possibly resulting in looping behavior where the bot can't decide which one to follow. +- Adjustments to how the bots prioritize combat targets under fight intruders objective. +- Fixed bots not reacting when they are non-intentionally damaged by friendly NPCs (e.g. security smashing a player character). + +Multiplayer fixes: +- Changes to make starting a round more robust: fixes various equality check errors ("submarine/mission doesn't match") if starting a multiplayer round takes a long time. +- Fixed dedicated servers' content package info getting truncated to 255 bytes, causing the content package list to just display "unknown" if the server has lots of mods enabled. +- Fixed "input contains duplicate packages" error when trying to join a server that has certain types of mods enabled (more specifically, mods that only contain client-side content, identical content or files of the type "Other", which could result in the MD5 hashes of the mods to be identical). +- Fixed projectile spread not being as random as it should be in multiplayer (successive hitscan rounds usually launched in the same direction). +- Fixed spectators not hearing others if their character is dead before the character despawns. +- Campaign rounds aren't forced to end if there's only one client on the server using freecam. +- Fixed crashing when a traitor missions starts when the host isn't controlling a character. +- Fixed sonar beacon tickbox sometimes flickering on and off in multiplayer. +- Fixed clients not seeing wall damage in outposts if the server has made outpost walls damageable, and the client doesn't have permissions to manage server settings. +- Fixed arc emitter briefly stunning the user client-side. +- Unconscious players can't end the round. + +Countermeasures against multiplayer exploits: - Fixed an exploit that allowed you to equip 2-handed weapons in only one hand. - Added protection against deliberately lagging the server. - - Added option to enable "DoS Protection" in the server settings under "Anti-Griefing". + - Added an option to enable "DoS Protection" in the server settings under "Anti-Griefing". - Enabled by default. - When enabled, the server will automatically kick players who are causing the server to perform poorly. - Added a new "Max Packet Auto-Kick" in the server settings under "Anti-Griefing". - - Enabled and set to 2400 by default. + - Enabled and set to 3000 by default. - Can be disabled by setting the limit to 1200 or below. - When enabled, the server will automatically kick players who are sending a certain amount of network packets in a minute. - Added "Spam Immunity" server permission. @@ -19,171 +252,111 @@ WIP countermeasures against multiplayer exploits (feedback appreciated!): - Added a rate limit to console commands in multiplayer. - Added a rate limit to creating new characters in multiplayer. -Unstable only: -- Fixed frequent "collection was modified" errors when loading/removing/copying structures. -- Fixed light textures being misaligned, causing many areas to look unintentionally bright. -- Fixed artifact transport case not having a background behind the fans, allowing you to see through the edges of the fans. + Added the fans to the inventory icon. -- Fixed character sinking downwards in "steps" when close to the surface of water and when the water level is going down. +Balance: +- Buffed Harpoon Coil Rifle a bit (shorter charge time). +- Reduced Focused Flak Shell penetration slightly. ---------------------------------------------------------------------------------------------------------- -v1.1.3.0 (unstable) ---------------------------------------------------------------------------------------------------------- +Talents: +- Engine Engineer, Helmsman and Affiliation talents no longer stack (allowed gaining insane engine speed boosts and bonuses by having large numbers of crew with the same talent). +- Fixed makeshift shelves no longer being placeable in pre-1.0 saves. +- Fixed some incorrect talent icons. +- Fixed Networking talent only giving a discount for faction-specific items. +- Fixed Machine Maniac requiring 5 repaired items instead 3 like the description says. +- Gene harvester doesn't spawn genetic materials on pets. +- Dying due to a disconnect doesn't trigger talents (like "Revenge Squad") or get recorded as a kill. -Unstable only: -- Fixed broken "dialoglowrepcampaigninteraction" NPC line. -- Fixed messed up water effect and lighting. - ---------------------------------------------------------------------------------------------------------- -v1.1.2.0 (unstable) ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed most StatusEffects doing nothing. - ---------------------------------------------------------------------------------------------------------- -v1.1.1.0 (unstable) ---------------------------------------------------------------------------------------------------------- - -Unstable only: -- Fixed RelatedItem not assigning the Identifiers or ExcludedIdentifiers properties, causing various kinds of crashes in various places. -- Fixed a race condition that sometimes caused "collection was modified" exceptions when rendering lights. - ---------------------------------------------------------------------------------------------------------- -v1.1.0.0 (unstable) ---------------------------------------------------------------------------------------------------------- - -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 LOS setting that only obstructs visibility through the sub's exterior walls. - -Changes and additions: -- Oxygen generator sprite and animation fixes. -- Optimized/simplified exosuit and FB3000 status effects. -- Optimized flashlights (and other "spotlight" type of light sources): previously they'd calculate shadows all around them, even though the light is only visible in front of the light source. -- Partially multithreaded lighting: FindRaycastHits (the heaviest individual part of the lighting algorithm) is now handled in a separate thread. -- Spawn abyss and combat suits in enemy subs and wrecks instead of normal ones in later biomes. -- Outpost NPCs don't allow players to grab them for longer than 10 seconds to prevent being able to drag them around the outpost. -- Unconscious players can't end the round. -- Submarine upgrades that require materials can be purchased with materials in the sub, instead of having to carry the materials on you. -- Added biome-specific outpost level generation parameters (i.e. outpost levels look different in different biomes). -- Minor visual improvements to Hydrothermal Wastes and The Great Sea. -- Faction reputation is reset after finishing the campaign. -- Adjusted FB3000 fabrication recipe (the previous required so many materials they don't fit in the fabricator's input slots). -- Fixed misplaced sonar beacon around the end of the campaign, causing the sonar to display an "emergency signal" outside the level. -- Added an event on the round after rescuing Subra (a sort of escort mission, it's weird if he just disappears after the rescue mission). -- Changed sonar flora sprite depths to prevent them from being obscured by the edge chunk objects. -- Restrict the maximum reputation loss from damaging NPCs and walls to 10 per round. Could use some further adjustment (feedback welcome!), but now a trigger-happy player can't cause too much permanent damage, but 10 is still a sizable penalty. -- Halved the reputation losses from damaging NPCs and walls. The previous values seemed too punishing now that the reputation loss has much more long-lasting consequences. -- Adjusted music intensity ranges to make the actual "music" (as opposed to the intensity tracks) play more frequently. -- Artifact transport cases require batteries to nullify the effects of the artifacts. The batteries last a little over 8 minutes. Also added some animations and lights to the case when powered. -- Allow stealing items in trash cans. -- Made sulphurite shards explode when thrown. -- Outpost NPCs who offer services don't get turned hostile by low reputation (but have some special dialog lines when you interact with them). +Submarines: +- Reworked Typhon (kudos to uberpendragon): replaced legacy items and structures, got rid of double walls, layout adjustments, visual improvements, improvements to the power grid and many smaller changes. +- Fixed waypoint in Herja's airlock not being linked to the door. +- Fixed fabricators not being linked to the cabinet next to them in Azimuth, Berilia, Kastrull, Orca2, Typhon, Typhon2, Winterhalter. +- Fixed some unhulled spaces inside Kastrull, Azimuth and Berilia. +- Fixed improperly wired smoke detectors in Typhon 2. +- Replaced the wall between gunnery and engineering (that every sane person cuts a hole in) with a door in Kastrull. +- Fixed medic not being given proper ID card tags in R-29, preventing them from accessing the toxin cabinet. +- Fixed Venture's exterior airlock door being repairable with a welding tool instead of a wrench. +- Fixed water detector in Dugong's oxygen generator room not being connected to the flood alarm circuit. +- Added "Silent Running" to Azimuth, as well as 1 diving suit and exterior cameras to make it more worthy of tier 2 (despite a lack of guns) Fixes: +- Fixed all shadow-casting lights going through the outpost walls when docked to one. +- Oxygen generator sprite and animation fixes. +- Fixed escorted characters being hostile to you if they belong to a hostile faction. +- Safeguard against getting pinned under flooded enemy subs. The sub can now be pushed off as long as the enemies inside it are dead/incapacitated. +- Fixed enemy subs not affecting monster spawns (meaning monsters could spawn very close to enemy subs, and fighting an enemy sub didn't decrease the probability of monster spawns as it should've). +- Fixed submarines sometimes getting stuck when trying to squeeze through a tight passage in the level. +- Fixed pets being considered hostile in hostile outposts (causing AI crew to report them as intruders). +- Fixed monsters sometimes spawning inside walls during nest missions. +- Fixed paths between biomes sometimes being unlocked from the start on certain map seeds. +- Fixed launching an alien turret (or any modded turret that spawns it's own ammo inside itself) causing a crash. - Fixed crashing when you exit Steam while the Workshop menu is open. -- Fixed crashing with an error message about OutpostGenerationParams.CanHaveCampaignInteraction when some outpost NPCs defined in outpost generation params can't be found. -- Fixed outpost NPCs never attacking you if your reputation is not low. -- Fixed characters holding and eating bananas weirdly. :) -- Fixed inability to hire Jacov Subra if you miss/ignore the event the first time you get it. -- Fixed all lights always going through walls in outposts. -- Fixed water detector in Dugong's oxygen generator room not being connected to the flood alarm circuit. -- Fixes to UI layout on ultrawide resolutions. -- Fixed ragdolls sometimes getting stuck on corners near platforms. -- Fixed characters not falling through holes in the floor below them until they move. -- Fixed inaccurate "Steady Tune" description. -- Fixed autoinjectors doing nothing. -- Fixed characters sometimes dying from barotrauma despite the pressure icon not being visible when in a partially pressurized hull. -- Outpost generator only takes walls with a collider into account when determining the bounds of the modules. Fixes husk modules being placed unnecessarily far from the outpost due to the decorative structures outside the door, leaving a very short hallway between the modules. -- Fixed character interaction texts (like "[H] Heal") not changing when you change the language. -- Fixed arc emitter briefly stunning the user client-side when fired. -- Fixed CustomInterface not showing the labels correctly in the sub editor (displaying the text tag instead of the actual text). -- Fixed operate order category not getting highlighted for the turret objective in the campaign tutorial. -- Fixed characters sometimes becoming immobile for a moment (as if they were briefly stunned) when the surface of the water raises above their chest. -- Fixed outpost NPCs who were supposed to stay in the room they spawn in (e.g. NPCs offering outpost services) occasionally wandering out of the room. -- Fixed pirates / bot crews being unable to swap between weapons and improved how they decide which turret to use (allowing them to swap to a turret with better visibility to the target). -- Fixed monsters sometimes spawning right next to pirate subs. -- Fixed "Gene Harvester" talent spawning genetic materials on pets. -- Fixed rebinding the use key not working in the sub editor. -- Fixed Ctrl+A not selecting connected wires in the sub editor. -- Fixed cancelling fabrication always enabling the amount slider, even if you can only fabricate one. -- Made beds secondary items. Fixes being able to climb ladders while in bed. -- Fabricator input slot tooltips don't show duplicate item names when the item can be crafted from multiple different items with the same name (e.g. petraptor egg can be crafted from 3 different egg items, all called "mudraptor egg"). -- Fixed attacking others with the husk appendage not healing the user. -- Fixed headsets being drawn in front of helmets when you equip the helmet 1st and then the headset. -- Dying due to a disconnect doesn't trigger talents (like "Revenge Squad") or get recorded as a kill. -- Fixed pet name tag getting stuck mid-air when equipped. -- Fixed next round's missions not being displayed in the round summary when leaving a location that has missions (e.g. outpost with a jailbreak mission). -- Fixed turrets being able to launch projectiles inside enemy subs if you poke the turret through the sub's hull. -- Fixed an issue that sometimes caused some levels to always display as unvisited and unlocked (more specifically, the level that happened to generate 1st during the campaign map generation). -- Fixed treatment suggestion for husk infection being shown when wearing zealot robes. -- Fixed all crates and ammo boxes having slightly too large colliders, making them float above ground. -- Fixed misaligned pulse laser sprites. -- Fixed fabricators not being linked to the cabinet next to them in Azimuth, Berilia, Kastrull, Orca 2, Typhon, Typhon 2 and Winterhalter. -- Fixed inability to fabricate high-quality nuclear depth charges. -- Fixed exosuits getting autofilled with batteries despite being powered by fuel rods now. -- Fixed exosuits' lights not turning off when the wearer dies. -- Fixed exosuit not muffling sounds when worn. -- Fixed cultist hood overlapping with the exosuit. -- Fixed escort missions sometimes unlocking in a level leading to an abandoned outpost even if there's an inhabited one available. +- Fixed some of the fonts not working properly in Japanese (but using roughly similar Chinese symbols instead). - Fixed campaign's end boss moving away from you if you attack it with melee weapons. -- Fixed projectile spread not working properly, causing the projectiles to be launched at the same angle too often. -- Fixed some of the fonts not working properly in Japanese, displaying roughly similar Chinese symbols instead of the correct Japanese symbols. -- Fixed pirate captain hats peeking through PUCS's helmet. -- Fixed escorted characters being hostile if they belong to a faction you have a low reputation with. -- Reduce the minimum mass required for a character to be visible with thermal goggles, always show at least the main limb regardless of the mass. Fixes thresher hatchlings being invisible to the goggles. -- Fixed medical clinic sometimes displaying healths as 99% despite the character having seemingly no afflictions. +- Fixed enemy crews not being able to switch between turrets and being very reluctant to operate multiple turrets at the same time. +- Fixed reputation reward text sometimes overflowing in the round summary (e.g. when the mission modifies both the husk cult and clown rep). +- Fixed characters holding and eating bananas weirdly. +- Fixed all friendly characters using the "hostage" dialog and all hostile characters the "bandit" dialog in abandoned outposts. +- Fixed characters sometimes becoming briefly immobilized (as if stunned) when the surface of the water rises up to the character's chest. +- Fixed cultist hood overlapping with the exosuit. +- Fixed exosuit and FB3000 not being tagged as "provocative" (meaning enemies didn't care about the sounds they make or the lights on them). +- Adjusted FB3000 fabrication recipe (the previous required so many materials they don't fit in the fabricator's input slots). +- Fixed PUCS not having an AI target unlike all other diving suits. +- Fixed sonar monitor's "sonar circle" overlapping with the control panel on some resolutions / HUD scales. +- Fixed occasional invisible barriers around alien ruins. +- Fixed protein bars only healing 1/60 of the intended amount. +- Fixed "Engineers_are_special" event no longer appearing. +- Fixed characters sometimes being able pass through level walls by swimming down through the broken floor of a wreck. +- Fixed "acquire a wrench" popup appearing multiple times in the mechanic tutorial. +- Fixed black squares on docking ports and hatches. +- Fixed crashing when trying to increment the version number of a mod whose version number consists of only numbers (i.e. no periods) or is empty. +- Fixed hireable cultists and clowns being the wrong way around (i.e. high clown rep allowed you to hire cultists and vice versa). +- Fixed wikiimage_sub and wikiimage_character crashing the game if the file is being used by another process. +- Fixed wikiimage_sub not sorting the entities the same way as the sub editor and game screen. +- Fixed status monitor displaying very small amounts of water in linked hulls as 1%. +- Faction reputation is reset after finishing the campaign. - Fixed colony docking modules spawning with a bit of water in them. -- Fixed artifacts being slightly off-center and at an incorrect sprite depth in artifact holders. - Fixed minerals sometimes spawning in normal caves in abyss mining missions. Happened if no abyss islands with caves happened to generate - now we always generate a cave in at least one of the islands. - Fixed lights on the items the character is wearing being visible when inside a clown crate. -- Fixed mudraptor eggs (or other items set to be damaged by repair tools) not being damaged by flamer. -- Fixed wikiimage_sub not sorting the entities the same way as the sub editor and game screen, causing e.g. doors to render in front of walls. -- Fixed only main subs's sonar working properly in the end levels. -- Fixed "enablecheats" resetting when you save and reload a campaign save, meaning you could enable cheats, use them to e.g. spawn some weapons, save and reload, and then continue unlocking achievements in that same save. -- Fixed achievements being unlockable in editors. -- Fixed docking ports sometimes becoming impassable when the sub undocks and docks when there's an obstacle right outside it. Happened, for example, with certain kinds of elevators built using linked submarines. -- Fixed orders persisting even if the target no longer exists after a sub switch. -- Fixed reputation reward text sometimes overflowing in the round summary (e.g. when the mission modifies both the husk cult and clown rep). -- Fixed all friendly characters using the "hostage" dialog and all hostile characters the "bandit" dialog in abandoned outposts. -- Fixed "fight intruders" order causing bots to attack enemies in abandoned outposts again. -- A safeguard against getting pinned under a flooded pirate sub: if your sub is under an enemy sub with no living enemies inside it, and heading upwards, the submarine on top will gradually move up. -- Fixed subs sometimes getting stuck if they try to squeeze through a too small passage in the level. -- Fixed monsters sometimes spawning inside the sub during beacon missions. -- Fixed monsters spawned by nest missions still sometimes spawning inside a wall. -- Fixed hanging end of the wire sometimes being "at the wrong end" of the wire after copying entities or saving/reloading. -- Fixed artifacts never spawning randomly in ruins: we tried to spawn them in containers with the tag "ruintreasure", but those were all small chests which couldn't hold artifacts. I made artifacts containable in any alien chest now, and also have a 5% chance to spawning an artifact in a large chest. -- Fixed water moving erratically in rooms with lots of small, connected hulls. -- Fixed Medical Expertise not increasing bandage effectiveness. -- Wearing a clown suit without a mask gives you the "clown" status tag (making you play circus music on instruments and giving you immunity to to banana peels). - -Multiplayer: -- Changes to make starting a round more robust: fixes various equality check errors ("submarine/mission doesn't match") if starting a multiplayer round takes a long time. -- Fixed clients not seeing wall damage in outposts if the server has made outpost walls damageable, and the client doesn't have permissions to manage server settings. -- Fixed dedicated servers' content package info getting truncated to 255 bytes, causing the content package list to just display "unknown" if the server has lots of mods enabled. -- Fixed sonar beacon tickbox flickering on and off when interacted with in multiplayer. -- Fixes to oddities/inconsistencies when trying to heal someone while climbing ladders: dropping off ladders when opening your own health interface in MP, and "heal [H]" hint being visible when focusing on other characters while climbing ladders and the health interface opening for one frame if you attempt to heal. -- Fixed console errors when a client disconnects while a vote is running, and purchases being free if the vote goes through. -- Fixed "none" permission preset not working as it should when the language is set to something else than English (clients would not get assigned the "none" preset by default). -- Fixed 0% condition items in the character's inventory executing the OnBroken effects at the start of a round (i.e. empty stun and fixfoam grenades detonating in your inventory). -- Fixed pre-unlocked talents not being visible client-side until the next round when hiring one of the special faction NPCs. -- Fixed minerals spawning with their rotation set to 0 in multiplayer in mineral missions. -- Fixed ragdolls 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. -- Don't force campaign rounds to stop when the only client on the server is using freecam. -- Fixed spectators not hearing others before their character despawns when dead. -- Fixed light sprites being slightly too small on wrecked items. -- Fixed characters getting removed at the end of the round if they've died and then been revived with the "revive" console command. -- Mission unlock notifications aren't shown to people in the server lobby when a round is running. -- 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 mudraptor eggs (or other items set to be damaged by repair tools) not being damaged by flamers. +- Fixed handheld sonars not working in the end levels. +- Fixed rebinding the Use key not working in the sub editor. +- Fixed Ctrl+A not selecting connected wires in the sub editor. +- Fixed ability to climb ladders while lying in bed. +- Fabricator input slot tooltips don't show duplicate item names when the item can be crafted from multiple different items with the same name (e.g. petraptor egg can be crafted from 3 different egg items, all called "mudraptor egg"). +- Fixed attacking others with the husk appendage not healing the user. +- Fixed pet name tag getting stuck mid-air when equipped. +- Fixed next round's missions not being displayed in the round summary when leaving a location that has missions (e.g. outpost with a jailbreak mission). +- Fixed inability to hire Jacov Subra if you miss/ignore the event the first time you get it. +- Outpost generator only takes walls with a collider into account when determining the bounds of the modules. Fixes husk modules being placed unnecessarily far from the outpost due to the decorative structures outside the door, leaving a very short hallway between the modules. +- Fixed characters sometimes dying from barotrauma despite the pressure icon not being visible. Happened when the pressure was just above the lethal threshold, but not as high as outside the sub. +- Fixed character interact texts (like "[H] Heal") not changing when you change the language. +- Fixed treatment suggestion for husk infection being shown when wearing zealot robes. +- Fixed inability to fabricate high-quality nuclear depth charges. +- Fixed exosuits getting autofilled with batteries. +- Fixed exosuits' lights not turning off when the wearer dies. +- Fixed 0% grenades (stun grenade and fixfoam) detonating if a player has one in their inventory when a mission starts. +- Fixed pre-unlocked talents not being visible client-side on the special faction NPCs hired during the round. +- Fixed escort and cargo missions sometimes leading to an abandoned outpost even if there's an inhabited one available. +- Fixed characters (ragdolls) sometimes getting stuck to the corners near platforms. Modding: -- Fixed ability to "relaunch" a projectile that has already been launched or that's stuck to some target using status effects. -- Fixed LevelTrigger's OtherTrigger type not doing anything. -- Improvements to LevelObject culling to make objects less likely to disappear when they're in view and there's a too large number of objects visible. -- Fixed NearbyItems effects causing a crash if TargetIdentifiers haven't been set. -- Fixed LevelTrigger statuseffects not doing anything when triggered by a submarine. -- Fixed submarine spawning docked to a random docking port of a custom outpost module with multiple ports, even if one of them is marked as a main docking port. +- Fixed thalamus items fading from the prefab color to a darker tint, disregarding the actual sprite color of the item. +- Fixed LevelTrigger statuseffects not doing anything when triggered by a submarine. +- Added a scrollbar to the campaign setup's crew tab to make it work properly when there's more than 3 initial crew members. +- Support for using OnWearing in Containable StatusEffects. +- Fixed ability to "relaunch" a projectile that has already been launched or that's stuck to some target using status effects, which lead to various strange results. +- Fixed ItemContainer StatusEffects working in a different way than other effects, applying the effect separately to each target. Prevents e.g. having an effect that does something to an item if a condition is met on the character wearing it. + +--------------------------------------------------------------------------------------------------------- +v1.0.9.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed crashing when you move from one textbox to another using tab and go past the last available textbox. +- Fixed inability to enter the final levels on some subs that include shuttles/drones. +- Fixed autoinjectors (PUCS, autoinjector headset) wasting the syringe without any effects on the character. +- Fixed bots failing to operate turrets in Typhon 1 due to them being partially inside the ceiling. +- Fixed lights going through walls in outposts. +- Fixed language selection prompt not showing up when launching the game for the first time. +- Fixed bots doing objectives during the roles tutorial, e.g. repairing the leaks for you. --------------------------------------------------------------------------------------------------------- v1.0.8.0 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); }