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