67 Commits

Author SHA1 Message Date
NotAlwaysTrue 61f05d25d0 Sync with upstream 2026-06-16 22:09:05 +08:00
Evil Factory 9794fe50b4 Merge branch 'develop' 2026-06-16 09:44:20 -03:00
Evil Factory c1073ff3d2 Merge remote-tracking branch 'upstream/master' into develop 2026-06-16 09:40:27 -03:00
Joonas Rikkonen 06616535f4 Updated bug report template 2026-06-16 15:37:52 +03:00
Regalis11 1cd0178e0a v1.13.3.1 (Summer Update 2026) 2026-06-16 15:35:36 +03:00
Maplewheels a729269763 added gcthread cleanup on failure. 2026-05-30 22:21:15 -04:00
Maplewheels c452493555 New GC algorithm (untested). 2026-05-30 22:19:37 -04:00
Maplewheels d420e1efbb Added experimental fix for inlining issue in assembly init. 2026-05-23 18:27:34 -03:00
Evil Factory eb22fbddff Make the no packages currently executing warning debug-only 2026-05-22 23:48:34 -03:00
MapleWheels 1f5ee454c0 Increased number of GC iterations by one. 2026-05-18 13:29:10 -04:00
Maplewheels d80ac4a38f Added background gc collection task upon unloading assemblies. 2026-05-17 03:17:46 -04:00
MapleWheels cfde6f3579 - Fixed assembly unloading. However, requires 'plugin_forcerungc' to be run multiple times over ~30 seconds at the main menu. 2026-05-15 18:41:39 -04:00
Joonas Rikkonen 81f57ea3e7 Add unstable version option to bug report template 2026-05-13 13:37:43 +03:00
Maplewheels ada9a09410 Added experimental fix for inlining issue in assembly init. 2026-05-10 23:56:14 -04:00
MapleWheels 66717c7fe7 Several changes to improve unloadability of assemblies. 2026-04-29 10:20:16 -04:00
NotAlwaysTrue 59bc21973a OBT/1.2.0(Spring Update)
Sync with Upstream
2026-04-25 13:25:41 +08:00
Evil Factory 3f199a9500 Remove hard coded string LuaCsForBarotrauma everywhere possible 2026-04-23 22:54:06 -03:00
Evil Factory 798954d182 Fix main menu text alignment 2026-04-21 22:58:42 -03:00
Evil Factory 93bdd03f54 Fix missing SettingControl in global Lua table 2026-04-21 12:58:53 -03:00
Evil Factory 8d90ccb4a3 Merge remote-tracking branch 'upstream/master' into develop 2026-04-21 08:18:49 -03:00
Regalis11 7446dc17c1 v1.12.7.0 (Spring Update 2026 Hotfix 1) 2026-04-21 12:21:41 +03:00
Evil Factory 3dc4548077 Fix texture not multiple of 4 2026-04-20 17:03:45 -03:00
MapleWheels 96906ef89e Always purge cashes for plugin content on unloading. 2026-04-20 15:19:05 -04:00
Evil Factory 1e1fb642f0 Fix UseInternalAccessName 2026-04-20 14:33:49 -03:00
Evil Factory 0a3ca2403e Remove missing Value error, it should just use the default value if it's not specified 2026-04-19 17:49:06 -03:00
Evil Factory 7daefe6f16 Some tweaks to the text 2026-04-19 17:11:10 -03:00
MapleWheels 8a08152e65 Added missing diffs for f61f852a25. 2026-04-19 15:08:21 -04:00
MapleWheels f61f852a25 Added networking tests for configs. 2026-04-19 15:07:25 -04:00
Evil Factory 3c0621c5a0 Missing diffs from bb21a09244 2026-04-19 15:36:33 -03:00
Maplewheels f612112445 Fixed OnUpdate() not passing in deltaTime instead of totalTime. 2026-04-18 22:10:28 -04:00
Evil Factory ab77e9d40c Fix client.packages hook sending normal packages 2026-04-18 17:38:27 -03:00
Evil Factory bb21a09244 New icon for settings and make the top left text more user friendly 2026-04-18 16:36:36 -03:00
Evil Factory f5f7eb7af9 Allow reloadlua to force the state to running 2026-04-18 15:37:49 -03:00
Evil Factory 12e05cb07e Fix LuaCs net messages received during connection initialization to be read incorrectly, happened because we would reset the BitPosition in our harmony patch which would cause the message to be read incorrectly later 2026-04-18 00:00:00 -03:00
MapleWheels 8f33e0af43 - Fixed up the settings menu.
- Made SettingEntry throw an error if "Value" attribute is not found in XML.
- Fixed saved values for settings sometimes not reloading after disabling and re-enabling a package.
2026-04-17 09:00:13 -04:00
Evil Factory 8896377f9c Add alias for human.CPRSuccess and human.CPRFailed 2026-04-15 07:54:51 -03:00
MapleWheels 17003e47cd - Fixed networking sync vars failing to sync initially.
- Fixed lua failing to differentiate overloads ISettingBase.
2026-04-14 12:17:12 -04:00
MapleWheels 38f0055b74 Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox. 2026-04-13 15:36:11 -04:00
MapleWheels a2c67f7af5 Changed csharp script compilation algorithm to be best effort. 2026-04-13 14:19:31 -04:00
MapleWheels 344027d757 Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API. 2026-04-13 13:08:41 -04:00
MapleWheels addd3f7a45 Added legacy overload of AddCommand for mod compat. 2026-04-13 12:02:16 -04:00
Maplewheels a40e54f4e4 Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails. 2026-04-13 02:13:29 -04:00
Evil Factory 666b96f068 Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be) 2026-04-12 11:00:45 -03:00
Maplewheels c6ccc68b2e Fixed "Folder" attribute files not being found. 2026-04-11 23:20:11 -04:00
Evil Factory c5976ccae1 Add safety check so this stops crashing when we look at it the wrong way 2026-04-11 23:32:40 -03:00
Maplewheels 1b8bcf4052 Added configinfo null check to SettingBase.cs 2026-04-11 21:48:33 -04:00
Evil Factory eeef7aff68 Register Voronoi2 stuff 2026-04-11 22:25:04 -03:00
Maplewheels 3605571f71 Added fallback friendly name resolution for ModConfig assembly contents. 2026-04-11 21:00:42 -04:00
Evil Factory f3535e242c Load console commands after executing and possible fix for the not console command permitted 2026-04-11 21:49:12 -03:00
Evil Factory 1703b5a103 Ignore our workshop packages from the game's dependency thing since it doesn't really make sense 2026-04-11 00:08:49 -03:00
Evil Factory 775ae0ef91 Fix server host being prompted twice with the C# popup 2026-04-10 23:49:16 -03:00
Evil Factory ac6147e4a0 Fix Cs prompt showing up more than one between rounds 2026-04-10 22:36:29 -03:00
Evil Factory 3cc7467d99 Fix hide username in logs config not doing anything 2026-04-10 22:10:44 -03:00
Evil Factory f97c00195c Oops, fix NRE crash 2026-04-10 21:56:39 -03:00
Evil Factory df8bfb1807 Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server 2026-04-10 21:40:32 -03:00
MapleWheels ccd0b9f7cc Added caching toggle to PluginManagementService 2026-04-10 18:13:25 -04:00
Evil Factory 5505f20925 PackageId -> PackageName 2026-04-10 16:50:15 -03:00
Evil Factory 1adf76168d Fix garbage network data being read by the game when reading LuaCs network messages 2026-04-10 13:37:01 -03:00
MapleWheels 9250e27953 - Fixed assembly logging style.
- Fixed double logging during execution.
2026-04-10 10:48:55 -04:00
MapleWheels c3a9551683 - Changed event aliases to be case insensitive. 2026-04-10 10:14:53 -03:00
Evil Factory fd324ac67e Fix Hook.Remove not being implemented properly 2026-04-10 09:58:43 -03:00
Evil Factory f6ea4df489 Load languages files even if the package is disabled 2026-04-10 00:36:36 -03:00
Evil Factory a86b8142eb Actually use the PackageId const everywhere we need to refer to our content package 2026-04-10 00:11:33 -03:00
Evil Factory e3b2b34940 Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table 2026-04-09 23:54:21 -03:00
Evil Factory b68ffa2725 Add LuaCsSetup.Lua back for compatibility 2026-04-09 13:56:56 -03:00
Evil Factory a85d489907 Fix modifyChatMessage hook 2026-04-09 12:39:39 -03:00
Joonas Rikkonen 2cfa1e6ffd Update bug-reports.yml 2026-04-09 15:17:26 +03:00
181 changed files with 2411 additions and 3628 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ body:
label: Version 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. 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: options:
- v1.12.6.2 (Spring Update 2026) - v1.13.3.1 (Summer Update 2026)
- Other - Other
validations: validations:
required: true required: true
-2
View File
@@ -61,5 +61,3 @@ Deploy/DeployAll/PrivateKey.*
#Rider #Rider
*.DotSettings.user *.DotSettings.user
.vscode/settings.json .vscode/settings.json
.vscode/launch.json
.vscode/tasks.json
@@ -258,7 +258,9 @@ namespace Barotrauma
//RelativeSpacing = 0.05f //RelativeSpacing = 0.05f
}; };
InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight), GUIFrame left = new(new RectTransform(new Vector2(0.25f, 1f), characterIndicatorArea.RectTransform), style: null);
InventorySlotContainer = new GUICustomComponent(new RectTransform(Vector2.One, left.RectTransform),
(spriteBatch, component) => (spriteBatch, component) =>
{ {
for (int i = 0; i < character.Inventory.Capacity; i++) for (int i = 0; i < character.Inventory.Capacity; i++)
@@ -266,6 +268,10 @@ namespace Barotrauma
if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; } if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; }
if (character.Inventory.HideSlot(i)) { continue; } if (character.Inventory.HideSlot(i)) { continue; }
int width = Character.Inventory.visualSlots[i].Rect.Width;
left.RectTransform.MinSize = new Point(width, left.RectTransform.MinSize.Y);
if (afflictionIconList != null) { afflictionIconList.RectTransform.MinSize = new Point(width, afflictionIconList.RectTransform.MinSize.Y); }
//don't draw the item if it's being dragged out of the slot //don't draw the item if it's being dragged out of the slot
bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn();
@@ -292,8 +298,7 @@ namespace Barotrauma
} }
}); });
cprButton = new GUIButton(new RectTransform(new Vector2(0.75f), left.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
{ {
UserData = UIHighlightAction.ElementId.CPRButton, UserData = UIHighlightAction.ElementId.CPRButton,
OnClicked = (button, userData) => OnClicked = (button, userData) =>
@@ -316,12 +321,11 @@ namespace Barotrauma
return true; return true;
}, },
ToolTip = TextManager.Get("doctor.cprobjective"), ToolTip = TextManager.Get("tutorial.roles.medic.objective.cpr"),
IgnoreLayoutGroups = true,
Visible = false Visible = false
}; };
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.5f, 1.0f), characterIndicatorArea.RectTransform),
(spriteBatch, component) => (spriteBatch, component) =>
{ {
DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true); DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true);
@@ -368,8 +372,6 @@ namespace Barotrauma
CanBeFocused = false CanBeFocused = false
}; };
characterIndicatorArea.Recalculate();
healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null)
{ {
HoverCursor = CursorState.Hand HoverCursor = CursorState.Hand
@@ -2143,7 +2145,7 @@ namespace Barotrauma
if (existingAffliction == null) if (existingAffliction == null)
{ {
existingAffliction = afflictionPrefab.Instantiate(strength); existingAffliction = afflictionPrefab.Instantiate(strength);
afflictions.TryAdd(existingAffliction, limb); afflictions.Add(existingAffliction, limb);
newAdded = true; newAdded = true;
} }
existingAffliction.SetStrength(strength); existingAffliction.SetStrength(strength);
@@ -867,6 +867,19 @@ namespace Barotrauma
}); });
}, isCheat: false)); }, isCheat: false));
commands.Add(new Command("togglespoofeventmanagerid", "togglespoofeventmanagerid: Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.", (string[] args) =>
{
if (GameMain.Client != null)
{
GameMain.Client.SpoofEntityManagerReceivedId = !GameMain.Client.SpoofEntityManagerReceivedId;
DebugConsole.NewMessage(GameMain.Client.SpoofEntityManagerReceivedId ? "Spoofing enabled ": "Spoofing disabled", Color.Green);
}
else
{
DebugConsole.NewMessage("Not connected to server", Color.Red);
}
}));
commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) => commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) =>
{ {
SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid; SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid;
@@ -1388,12 +1401,12 @@ namespace Barotrauma
if (me.SimPosition.Length() > 2000.0f) if (me.SimPosition.Length() > 2000.0f)
{ {
NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange); NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
MapEntity.MapEntityList.Remove(me); MapEntity.MapEntityList.RemoveAt(i);
} }
else if (!me.ShouldBeSaved) else if (!me.ShouldBeSaved)
{ {
NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange); NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
MapEntity.MapEntityList.Remove(me); MapEntity.MapEntityList.RemoveAt(i);
} }
else if (me is Item) else if (me is Item)
{ {
@@ -4454,17 +4467,24 @@ namespace Barotrauma
public static void StartLocalMPSession(int numClients = 2) public static void StartLocalMPSession(int numClients = 2)
{ {
string extraArguments = "-multiclienttestmode";
if (NetConfig.UseLenientHandshake)
{
extraArguments += " -lenienthandshake";
}
try try
{ {
if (Process.GetProcessesByName("DedicatedServer").Length == 0) if (Process.GetProcessesByName("DedicatedServer").Length == 0)
{ {
#if WINDOWS #if WINDOWS
Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode"); Process.Start("DedicatedServer.exe", arguments: extraArguments);
#else #else
Process.Start("./DedicatedServer", arguments: "-multiclienttestmode"); Process.Start("./DedicatedServer", arguments: extraArguments);
#endif #endif
System.Threading.Thread.Sleep(1000); System.Threading.Thread.Sleep(1000);
} }
#if DEBUG #if DEBUG
GameClient.MultiClientTestMode = true; GameClient.MultiClientTestMode = true;
#endif #endif
@@ -4478,10 +4498,13 @@ namespace Barotrauma
for (int i = 2; i <= numClients; i++) for (int i = 2; i <= numClients; i++)
{ {
System.Threading.Thread.Sleep(1000); System.Threading.Thread.Sleep(1000);
string clientArguments = $"-connect server localhost -username client{i} -skipintro";
#if WINDOWS #if WINDOWS
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode"); Process.Start("Barotrauma.exe", arguments: $"{clientArguments} {extraArguments}");
#else #else
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode"); Process.Start("./Barotrauma", arguments: $"{clientArguments} {extraArguments}");
#endif #endif
} }
} }
@@ -28,7 +28,7 @@ namespace Barotrauma
public void DebugDraw(SpriteBatch spriteBatch) public void DebugDraw(SpriteBatch spriteBatch)
{ {
foreach (Event ev in _activeEvents) foreach (Event ev in activeEvents)
{ {
Vector2 drawPos = ev.DebugDrawPos; Vector2 drawPos = ev.DebugDrawPos;
drawPos.Y = -drawPos.Y; drawPos.Y = -drawPos.Y;
@@ -41,7 +41,7 @@ namespace Barotrauma
public void DebugDrawHUD(SpriteBatch spriteBatch, float y) public void DebugDrawHUD(SpriteBatch spriteBatch, float y)
{ {
foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>()) foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
{ {
DrawEventTargetTags(spriteBatch, scriptedEvent); DrawEventTargetTags(spriteBatch, scriptedEvent);
} }
@@ -156,7 +156,7 @@ namespace Barotrauma
{ {
if (isGraphHovered || isGraphSelected) if (isGraphHovered || isGraphSelected)
{ {
foreach (var timeStamp in _timeStamps) foreach (var timeStamp in timeStamps)
{ {
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval)); int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
if (t == order) if (t == order)
@@ -205,7 +205,7 @@ namespace Barotrauma
} }
adjustedYStep = GUI.AdjustForTextScale(12); adjustedYStep = GUI.AdjustForTextScale(12);
foreach (EventSet eventSet in _pendingEventSets) foreach (EventSet eventSet in pendingEventSets)
{ {
if (Submarine.MainSub == null) { break; } if (Submarine.MainSub == null) { break; }
@@ -263,7 +263,7 @@ namespace Barotrauma
y += yStep; y += yStep;
adjustedYStep = GUI.AdjustForTextScale(18); adjustedYStep = GUI.AdjustForTextScale(18);
foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
{ {
GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont); GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont);
@@ -14,6 +14,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma namespace Barotrauma
{ {
@@ -650,11 +651,11 @@ namespace Barotrauma
if (MouseOn != null) if (MouseOn != null)
{ {
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
if (!MouseOn.ToolTip.IsNullOrWhiteSpace()) if (!MouseOn.ToolTip.IsNullOrWhiteSpace())
{ {
MouseOn.DrawToolTip(spriteBatch); MouseOn.DrawToolTip(spriteBatch);
} }
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
} }
if (SubEditorScreen.IsSubEditor()) if (SubEditorScreen.IsSubEditor())
@@ -2323,10 +2324,10 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Creates a 7-segment display. /// Creates a 7-segment display.
/// </summary> /// </summary>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/> or empty.</param> /// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/>.</param>
/// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param> /// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param>
/// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param> /// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param>
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null) public static GUITextBlock CreateDigitalDisplay(RectTransform rect, [NotNullIfNotNull(nameof(leftLabelText))] out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
{ {
GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft) GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft)
{ {
@@ -2337,7 +2338,7 @@ namespace Barotrauma
}; };
leftLabel = null; leftLabel = null;
if (!leftLabelText.IsNullOrEmpty()) if (leftLabelText != null)
{ {
leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight); leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight);
} }
@@ -384,14 +384,14 @@ namespace Barotrauma
CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex;
CalculateCaretPos(); CalculateCaretPos();
ClearSelection(); ClearSelection();
bool wasSelected = selected;
selected = true;
GUI.KeyboardDispatcher.Subscriber = this;
OnSelected?.Invoke(this, Keys.None); OnSelected?.Invoke(this, Keys.None);
if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound) if (!selected && PlaySoundOnSelect && !ignoreSelectSound)
{ {
SoundPlayer.PlayUISound(GUISoundType.Select); SoundPlayer.PlayUISound(GUISoundType.Select);
} }
selected = true;
//set this after we've set selected to true -> otherwise the textbox taking keyboard focus will trigger Select again
GUI.KeyboardDispatcher.Subscriber = this;
} }
public void Deselect() public void Deselect()
@@ -113,7 +113,10 @@ namespace Barotrauma
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState);
if (currentBackgroundTexture.Texture != null)
{
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea); GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
}
overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale);
double noiseT = Timing.TotalTime * 0.02f; double noiseT = Timing.TotalTime * 0.02f;
@@ -278,6 +278,12 @@ namespace Barotrauma
GameClient.MultiClientTestMode = true; GameClient.MultiClientTestMode = true;
} }
#endif #endif
if (ConsoleArguments.Contains("-lenienthandshake"))
{
NetConfig.UseLenientHandshake = true;
}
GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window);
PerformanceCounter = new PerformanceCounter(); PerformanceCounter = new PerformanceCounter();
@@ -867,7 +867,7 @@ namespace Barotrauma
{ {
foreach (var stackedItem in item.GetStackedItems()) foreach (var stackedItem in item.GetStackedItems())
{ {
Item.MarkForDeconstruction(stackedItem); Item.DeconstructItems.Add(stackedItem);
} }
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
} }
@@ -875,7 +875,7 @@ namespace Barotrauma
{ {
foreach (var stackedItem in item.GetStackedItems()) foreach (var stackedItem in item.GetStackedItems())
{ {
Item.UnmarkForDeconstruction(stackedItem); Item.DeconstructItems.Remove(stackedItem);
} }
} }
} }
@@ -128,12 +128,6 @@ namespace Barotrauma
(int)SlotPositions[i].Y, (int)SlotPositions[i].Y,
(int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier)); (int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier));
if (SlotTypes[i] == InvSlotType.HealthInterface &&
character.CharacterHealth?.InventorySlotContainer != null)
{
slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f);
}
ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent<ItemContainer>(); ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent<ItemContainer>();
if (itemContainer != null) if (itemContainer != null)
{ {
@@ -622,6 +616,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++) for (int i = 0; i < capacity; i++)
{ {
if (HideSlot(i)) { continue; }
var item = slots[i].FirstOrDefault(); var item = slots[i].FirstOrDefault();
if (item != null) if (item != null)
{ {
@@ -26,7 +26,7 @@ namespace Barotrauma.Items.Components
} }
else else
{ {
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)); DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)).Fallback(Msg);
} }
CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled); CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled);
@@ -1933,7 +1933,7 @@ namespace Barotrauma.Items.Components
void CalculateDistance() void CalculateDistance()
{ {
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false); pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
if (!path.Unreachable) if (!path.Unreachable)
{ {
@@ -12,9 +12,12 @@ namespace Barotrauma.Items.Components
private partial class PowerGroup private partial class PowerGroup
{ {
private GUIFrame? frame; private GUIFrame? frame;
private GUIFrame? groupContent;
private GUILayoutGroup? nameGroup;
private GUITextBox? nameBox; private GUITextBox? nameBox;
private GUITextBlock? loadDisplayNameLabel;
private GUIScrollBar? ratioSlider; private GUIScrollBar? ratioSlider;
private readonly List<GUITextBlock> powerUnitLabels = new List<GUITextBlock>(); private readonly List<GUITextBlock> powerUnitLabels = [];
private GUIFrame? divider; private GUIFrame? divider;
public bool IsVisible { get; private set; } = true; public bool IsVisible { get; private set; } = true;
@@ -22,9 +25,9 @@ namespace Barotrauma.Items.Components
public void CreateGUI() public void CreateGUI()
{ {
frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null); frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null);
GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null); groupContent = new GUIFrame(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft) nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{ {
Stretch = true Stretch = true
}; };
@@ -37,7 +40,7 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
}; };
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle") nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Screen.Selected == GameMain.SubEditorScreen ? Name : DisplayName.Value, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
{ {
MaxTextLength = MaxNameLength, MaxTextLength = MaxNameLength,
OverflowClip = true, OverflowClip = true,
@@ -48,17 +51,40 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
}; };
nameBox.OnSelected += (tb, _) =>
{
if (tb.Selected) { return; }
tb.Text = Name;
};
nameBox.OnDeselected += (tb, _) => nameBox.OnDeselected += (tb, _) =>
{ {
Name = tb.Text; Name = tb.Text;
tb.CaretIndex = 0;
if (GameMain.Client == null) { return; } if (GameMain.Client == null) { return; }
distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange)); distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange));
}; };
nameBox.GetChild<GUIFrame>().GetChild<GUICustomComponent>().OnDrawToolTip = comp =>
{
if (Screen.Selected != GameMain.SubEditorScreen && !nameBox.Selected)
{
comp.ToolTip = null;
return;
}
LocalizedString localizedText = TextManager.Get(nameBox.Text);
comp.ToolTip = localizedText.IsNullOrEmpty()
? TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", nameBox.Text)
: TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", localizedText);
};
GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) }, GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font); out loadDisplayNameLabel, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString(); loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString();
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider") ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider")
{ {
Step = SupplyRatioStep, Step = SupplyRatioStep,
@@ -78,16 +104,17 @@ namespace Barotrauma.Items.Components
ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height); ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height);
GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft), GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft),
out GUITextBlock? _, out GUITextBlock _, out GUITextBlock? _, out GUITextBlock ratioDisplayUnitLabel,
rightLabelText: "%"); rightLabelText: "%");
ratioDisplay.TextGetter = () => DisplayRatio.ToString(); ratioDisplay.TextGetter = () => DisplayRatio.ToString();
GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) }, GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel, out GUITextBlock? outputDisplayNameLabel, out GUITextBlock outputDisplayUnitLabel,
TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font); TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font);
outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString(); outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString();
powerUnitLabels.Add(loadDisplayUnitLabel); powerUnitLabels.Add(loadDisplayUnitLabel);
powerUnitLabels.Add(ratioDisplayUnitLabel);
powerUnitLabels.Add(outputDisplayUnitLabel); powerUnitLabels.Add(outputDisplayUnitLabel);
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
@@ -111,7 +138,14 @@ namespace Barotrauma.Items.Components
IsVisible = PowerOut.Wires.Count >= 1; IsVisible = PowerOut.Wires.Count >= 1;
frame!.Visible = IsVisible; frame!.Visible = IsVisible;
divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this; divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this;
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); } if (distributor.prevLanguage != GameSettings.CurrentConfig.Language)
{
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
}
} }
} }
@@ -716,13 +716,19 @@ namespace Barotrauma.Items.Components
GetAvailablePower(out float batteryCharge, out float batteryCapacity); GetAvailablePower(out float batteryCharge, out float batteryCapacity);
List<Item> availableAmmo = new List<Item>(); List<Item> availableAmmo = [];
AddAmmoFromContainer(item.GetComponent<ItemContainer>());
foreach (MapEntity e in item.linkedTo) foreach (MapEntity e in item.linkedTo)
{ {
if (!(e is Item linkedItem)) { continue; } if (e is not Item linkedItem) { continue; }
var itemContainer = linkedItem.GetComponent<ItemContainer>(); AddAmmoFromContainer(linkedItem.GetComponent<ItemContainer>());
if (itemContainer == null) { continue; } }
void AddAmmoFromContainer(ItemContainer itemContainer)
{
if (itemContainer == null) { return; }
availableAmmo.AddRange(itemContainer.Inventory.AllItems); availableAmmo.AddRange(itemContainer.Inventory.AllItems);
//add empty slots too
for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++) for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++)
{ {
availableAmmo.Add(null); availableAmmo.Add(null);
@@ -339,6 +339,19 @@ namespace Barotrauma
toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖";
} }
if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; }
if (item.Prefab.UnlockedRecipeInToolTip.Length > 0 && GameMain.GameSession is { } GameSession)
{
if (item.Prefab.UnlockedRecipeInToolTip.All(id => GameSession.HasUnlockedRecipe(Character.Controlled, id)))
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖";
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null)
{ {
colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
@@ -357,18 +370,6 @@ namespace Barotrauma
#if DEBUG #if DEBUG
toolTip += $" ({item.Prefab.Identifier})"; toolTip += $" ({item.Prefab.Identifier})";
#endif #endif
if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession)
{
if (GameSession.HasUnlockedRecipe(Character.Controlled, item.Prefab.UnlockedRecipeInToolTip))
{
toolTip += TextManager.Get("unlockedrecipe.true");
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
if (PlayerInput.KeyDown(InputType.ContextualCommand)) if (PlayerInput.KeyDown(InputType.ContextualCommand))
{ {
toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖"; toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
@@ -1301,7 +1302,6 @@ namespace Barotrauma
} }
} }
SoundPlayer.PlayUISound(GUISoundType.DropItem);
bool removed = false; bool removed = false;
if (Screen.Selected is SubEditorScreen editor) if (Screen.Selected is SubEditorScreen editor)
{ {
@@ -1892,7 +1892,7 @@ namespace Barotrauma
} }
} }
else if (Item.IsMarkedForDeconstruction(item) && else if (Item.DeconstructItems.Contains(item) &&
OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
{ {
DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn); DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
@@ -75,11 +75,11 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
// ReSharper restore FieldCanBeMadeReadOnly.Local // ReSharper restore FieldCanBeMadeReadOnly.Local
private const string SettingsResetButtonText = "LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings"; private const string SettingsResetButtonText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetVisibleSettings";
private const string SettingsResetPromptTitle = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title"; private const string SettingsResetPromptTitle = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Title";
private const string SettingsResetPromptContents = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message"; private const string SettingsResetPromptContents = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Message";
private const string SettingsResetPromptYesText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes"; private const string SettingsResetPromptYesText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Yes";
private const string SettingsResetPromptNoText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No"; private const string SettingsResetPromptNoText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.No";
private event Action OnApplyInstalledModsChanges; private event Action OnApplyInstalledModsChanges;
@@ -100,7 +100,7 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
var menuTitleLayoutGroup = new GUILayoutGroup( var menuTitleLayoutGroup = new GUILayoutGroup(
new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft); new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft);
GUIUtil.Label(menuTitleLayoutGroup, GUIUtil.Label(menuTitleLayoutGroup,
GetLocalizedString("LuaCsForBarotrauma.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"), GetLocalizedString($"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"),
GUIStyle.LargeFont, new Vector2(1f, 1f)); GUIStyle.LargeFont, new Vector2(1f, 1f));
// page contents // page contents
@@ -47,9 +47,9 @@ public class SettingsMenuSystem : ISettingsMenuSystem
_gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance, _gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance,
GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods", GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods",
"LuaCsForBarotrauma.SettingsMenu.ModGameplayButton"); $"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton");
/*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance, /*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance,
"SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton"); "SettingsMenuTab.Controls", $"{LuaCsSetup.PackageName}.SettingsMenu.ModControlsButton");
*/ */
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance); _gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance);
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
class BackgroundCreature : ISteerable class BackgroundCreature : ISteerable, ILevelRenderableObject
{ {
const float MaxDepth = 10000.0f; const float MaxDepth = 10000.0f;
@@ -76,6 +76,8 @@ namespace Barotrauma
set; set;
} }
public Vector3 Position => new Vector3(position.X, position.Y, Depth);
public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position) public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position)
{ {
this.Prefab = prefab; this.Prefab = prefab;
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -15,52 +14,11 @@ namespace Barotrauma
private float checkVisibleTimer; private float checkVisibleTimer;
private readonly List<BackgroundCreature> creatures = new List<BackgroundCreature>(); private readonly List<BackgroundCreature> creatures = [];
private readonly List<BackgroundCreature> visibleCreatures = new List<BackgroundCreature>(); private readonly List<BackgroundCreature> visibleCreatures = [];
public BackgroundCreatureManager() public IEnumerable<BackgroundCreature> VisibleCreatures => visibleCreatures;
{
/*foreach(var file in files)
{
LoadConfig(file.Path);
}*/
}
/*public BackgroundCreatureManager(string path)
{
DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}");
LoadConfig(ContentPath.FromRaw(null, path));
}
private void LoadConfig(ContentPath configPath)
{
try
{
XDocument doc = XMLExtensions.TryLoadXml(configPath);
if (doc == null) { return; }
var mainElement = doc.Root.FromPackage(configPath.ContentPackage);
if (mainElement.IsOverride())
{
mainElement = mainElement.FirstElement();
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple);
}
else if (Prefabs.Any())
{
DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'");
}
foreach (var element in mainElement.Elements())
{
Prefabs.Add(new BackgroundCreaturePrefab(element));
};
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e);
}
}*/
public void SpawnCreatures(Level level, int count, Vector2? position = null) public void SpawnCreatures(Level level, int count, Vector2? position = null)
{ {
@@ -161,14 +119,6 @@ namespace Barotrauma
} }
} }
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in visibleCreatures)
{
creature.Draw(spriteBatch, cam);
}
}
public void DrawLights(SpriteBatch spriteBatch, Camera cam) public void DrawLights(SpriteBatch spriteBatch, Camera cam)
{ {
foreach (BackgroundCreature creature in visibleCreatures) foreach (BackgroundCreature creature in visibleCreatures)
@@ -140,7 +140,7 @@ namespace Barotrauma
public void DrawFront(SpriteBatch spriteBatch, Camera cam) public void DrawFront(SpriteBatch spriteBatch, Camera cam)
{ {
renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager); renderer?.DrawForeground(spriteBatch, cam, backgroundCreatureManager, LevelObjectManager);
} }
public void ClientEventRead(IReadMessage msg, float sendingTime) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
@@ -12,7 +12,7 @@ using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
partial class LevelObject partial class LevelObject : ILevelRenderableObject
{ {
public float SwingTimer; public float SwingTimer;
public float ScaleOscillateTimer; public float ScaleOscillateTimer;
@@ -8,13 +8,20 @@ using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
public interface ILevelRenderableObject
{
public Vector3 Position { get; }
}
partial class LevelObjectManager partial class LevelObjectManager
{ {
// Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower? // Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower?
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>(MaxVisibleObjects); private readonly List<ILevelRenderableObject> visibleObjectsBack = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>(MaxVisibleObjects); private readonly List<ILevelRenderableObject> visibleObjectsMid = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>(MaxVisibleObjects); private readonly List<ILevelRenderableObject> visibleObjectsFront = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly HashSet<LevelObject> allVisibleObjects = new HashSet<LevelObject>(MaxVisibleObjects); private readonly HashSet<ILevelRenderableObject> allVisibleObjects = new HashSet<ILevelRenderableObject>(MaxVisibleObjects);
private double NextRefreshTime; private double NextRefreshTime;
@@ -35,35 +42,44 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam) partial void UpdateProjSpecific(float deltaTime, Camera cam)
{ {
foreach (LevelObject obj in visibleObjectsBack) foreach (ILevelRenderableObject obj in visibleObjectsBack)
{ {
obj.Update(deltaTime, cam); if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
} }
foreach (LevelObject obj in visibleObjectsMid)
{
obj.Update(deltaTime, cam);
} }
foreach (LevelObject obj in visibleObjectsFront) foreach (ILevelRenderableObject obj in visibleObjectsMid)
{ {
obj.Update(deltaTime, cam); if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
foreach (ILevelRenderableObject obj in visibleObjectsFront)
{
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
} }
} }
/// <summary> /// <summary>
/// Returns all visible objects, but not in order, because internally uses a HashSet. /// Returns all visible objects, but not in order, because internally uses a HashSet.
/// </summary> /// </summary>
public IEnumerable<LevelObject> GetAllVisibleObjects() public IEnumerable<ILevelRenderableObject> GetAllVisibleObjects()
{ {
allVisibleObjects.Clear(); allVisibleObjects.Clear();
foreach (LevelObject obj in visibleObjectsBack) foreach (ILevelRenderableObject obj in visibleObjectsBack)
{ {
allVisibleObjects.Add(obj); allVisibleObjects.Add(obj);
} }
foreach (LevelObject obj in visibleObjectsMid) foreach (ILevelRenderableObject obj in visibleObjectsMid)
{ {
allVisibleObjects.Add(obj); allVisibleObjects.Add(obj);
} }
foreach (LevelObject obj in visibleObjectsFront) foreach (ILevelRenderableObject obj in visibleObjectsFront)
{ {
allVisibleObjects.Add(obj); allVisibleObjects.Add(obj);
} }
@@ -73,7 +89,7 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Checks which level objects are in camera view and adds them to the visibleObjects lists /// Checks which level objects are in camera view and adds them to the visibleObjects lists
/// </summary> /// </summary>
private void RefreshVisibleObjects(Rectangle currentIndices, float zoom) private void RefreshVisibleObjects(Rectangle currentIndices, BackgroundCreatureManager backgroundCreatureManager, float zoom)
{ {
visibleObjectsBack.Clear(); visibleObjectsBack.Clear();
visibleObjectsMid.Clear(); visibleObjectsMid.Clear();
@@ -152,6 +168,27 @@ namespace Barotrauma
} }
} }
foreach (var backgroundCreature in backgroundCreatureManager.VisibleCreatures)
{
int drawOrderIndex = 0;
for (int i = 0; i < visibleObjectsBack.Count; i++)
{
if (visibleObjectsBack[i].Position.Z > backgroundCreature.Position.Z)
{
break;
}
else
{
drawOrderIndex = i + 1;
if (drawOrderIndex >= MaxVisibleObjects) { break; }
}
}
if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects)
{
visibleObjectsBack.Insert(drawOrderIndex, backgroundCreature);
}
}
//object grid is sorted in an ascending order //object grid is sorted in an ascending order
//(so we prefer the objects in the foreground instead of ones in the background if some need to be culled) //(so we prefer the objects in the foreground instead of ones in the background if some need to be culled)
//rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists //rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists
@@ -165,28 +202,28 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Draw the objects behind the level walls /// Draw the objects behind the level walls
/// </summary> /// </summary>
public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam) public void DrawObjectsBack(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{ {
DrawObjects(spriteBatch, cam, visibleObjectsBack); DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsBack);
} }
/// <summary> /// <summary>
/// Draw the objects in front of the level walls, but behind characters /// Draw the objects in front of the level walls, but behind characters
/// </summary> /// </summary>
public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam) public void DrawObjectsMid(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{ {
DrawObjects(spriteBatch, cam, visibleObjectsMid); DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsMid);
} }
/// <summary> /// <summary>
/// Draw the objects in front of the level walls and characters /// Draw the objects in front of the level walls and characters
/// </summary> /// </summary>
public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam) public void DrawObjectsFront(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{ {
DrawObjects(spriteBatch, cam, visibleObjectsFront); DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsFront);
} }
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List<LevelObject> objectList) private void DrawObjects(SpriteBatch spriteBatch, Camera cam, BackgroundCreatureManager backgroundCreatureManager, List<ILevelRenderableObject> objectList)
{ {
Rectangle indices = Rectangle.Empty; Rectangle indices = Rectangle.Empty;
indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize);
@@ -207,7 +244,7 @@ namespace Barotrauma
float z = 0.0f; float z = 0.0f;
if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime)) if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime))
{ {
RefreshVisibleObjects(indices, cam.Zoom); RefreshVisibleObjects(indices, backgroundCreatureManager, cam.Zoom);
ForceRefreshVisibleObjects = false; ForceRefreshVisibleObjects = false;
if (cam.Zoom < 0.1f) if (cam.Zoom < 0.1f)
{ {
@@ -216,50 +253,64 @@ namespace Barotrauma
} }
} }
foreach (LevelObject obj in objectList) bool prevObjectHasDeformableSprite = false;
foreach (ILevelRenderableObject obj2 in objectList)
{ {
Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter; Vector2 camDiff = new Vector2(obj2.Position.X, obj2.Position.Y) - cam.WorldViewCenter;
camDiff.Y = -camDiff.Y; camDiff.Y = -camDiff.Y;
Sprite activeSprite = obj.Sprite; bool hasDeformableSprite = false;
if (obj2 is LevelObject levelObject)
{
hasDeformableSprite = levelObject.ActivePrefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
Sprite activeSprite = levelObject.Sprite;
activeSprite?.Draw( activeSprite?.Draw(
spriteBatch, spriteBatch,
new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, new Vector2(levelObject.Position.X, -levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth), Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / levelObject.Prefab.FadeOutDepth),
activeSprite.Origin, activeSprite.Origin,
obj.CurrentRotation, levelObject.CurrentRotation,
obj.CurrentScale, levelObject.CurrentScale,
SpriteEffects.None, SpriteEffects.None,
z); z);
if (obj.ActivePrefab.DeformableSprite != null) if (hasDeformableSprite)
{ {
if (obj.CurrentSpriteDeformation != null) if (levelObject.CurrentSpriteDeformation != null)
{ {
obj.ActivePrefab.DeformableSprite.Deform(obj.CurrentSpriteDeformation); levelObject.ActivePrefab.DeformableSprite.Deform(levelObject.CurrentSpriteDeformation);
} }
else else
{ {
obj.ActivePrefab.DeformableSprite.Reset(); levelObject.ActivePrefab.DeformableSprite.Reset();
} }
obj.ActivePrefab.DeformableSprite?.Draw(cam, levelObject.ActivePrefab.DeformableSprite?.Draw(cam,
new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f), new Vector3(new Vector2(levelObject.Position.X, levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, z * 10.0f),
obj.ActivePrefab.DeformableSprite.Origin, levelObject.ActivePrefab.DeformableSprite.Origin,
obj.CurrentRotation, levelObject.CurrentRotation,
obj.CurrentScale, levelObject.CurrentScale,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f)); Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / 5000.0f));
} }
prevObjectHasDeformableSprite = hasDeformableSprite;
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true); GUI.DrawRectangle(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
if (obj.Triggers == null) { continue; } if (levelObject.Triggers == null) { continue; }
foreach (LevelTrigger trigger in obj.Triggers) foreach (LevelTrigger trigger in levelObject.Triggers)
{ {
if (trigger.PhysicsBody == null) continue; if (trigger.PhysicsBody == null) continue;
GUI.DrawLine(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3); GUI.DrawLine(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
Vector2 flowForce = trigger.GetWaterFlowVelocity(); Vector2 flowForce = trigger.GetWaterFlowVelocity();
if (flowForce.LengthSquared() > 1) if (flowForce.LengthSquared() > 1)
@@ -272,6 +323,24 @@ namespace Barotrauma
} }
} }
}
else if (obj2 is BackgroundCreature backgroundCreature && cam.Zoom > 0.05f)
{
hasDeformableSprite = backgroundCreature.Prefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
backgroundCreature.Draw(spriteBatch, cam);
}
prevObjectHasDeformableSprite = hasDeformableSprite;
z += 0.0001f; z += 0.0001f;
} }
} }
@@ -214,8 +214,9 @@ namespace Barotrauma
//calculate the sum of the forces of nearby level triggers //calculate the sum of the forces of nearby level triggers
//and use it to move the water texture and water distortion effect //and use it to move the water texture and water distortion effect
Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity;
foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects()) foreach (ILevelRenderableObject obj in level.LevelObjectManager.GetAllVisibleObjects())
{ {
if (obj is not LevelObject levelObject) { continue; }
if (levelObject.Triggers == null) { continue; } if (levelObject.Triggers == null) { continue; }
//use the largest water flow velocity of all the triggers //use the largest water flow velocity of all the triggers
Vector2 objectMaxFlow = Vector2.Zero; Vector2 objectMaxFlow = Vector2.Zero;
@@ -274,11 +275,7 @@ namespace Barotrauma
SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null,
cam.Transform); cam.Transform);
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam); backgroundSpriteManager?.DrawObjectsBack(spriteBatch, backgroundCreatureManager, cam);
if (cam.Zoom > 0.05f)
{
backgroundCreatureManager?.Draw(spriteBatch, cam);
}
level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset); level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset);
@@ -292,17 +289,18 @@ namespace Barotrauma
BlendState.NonPremultiplied, BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform); cam.Transform);
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam); backgroundSpriteManager?.DrawObjectsMid(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End(); spriteBatch.End();
} }
public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null) public void DrawForeground(SpriteBatch spriteBatch, Camera cam,
BackgroundCreatureManager backgroundCreatureManager, LevelObjectManager backgroundSpriteManager = null)
{ {
spriteBatch.Begin(SpriteSortMode.Deferred, spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied, BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform); cam.Transform);
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam); backgroundSpriteManager?.DrawObjectsFront(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End(); spriteBatch.End();
} }
@@ -471,11 +471,11 @@ namespace Barotrauma
if (item0 == null && item1 != null) if (item0 == null && item1 != null)
{ {
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false); item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
} }
else if (item0 != null && item1 == null) else if (item0 != null && item1 == null)
{ {
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false); item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
} }
if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1)) if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1))
{ {
@@ -105,7 +105,7 @@ namespace Barotrauma
public static void Draw(SpriteBatch spriteBatch, bool editing = false) public static void Draw(SpriteBatch spriteBatch, bool editing = false)
{ {
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
foreach (MapEntity e in entitiesToRender) foreach (MapEntity e in entitiesToRender)
{ {
@@ -115,7 +115,7 @@ namespace Barotrauma
public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null) public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{ {
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
foreach (MapEntity e in entitiesToRender) foreach (MapEntity e in entitiesToRender)
{ {
@@ -164,7 +164,7 @@ namespace Barotrauma
public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null) public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
{ {
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
depthSortedDamageable.Clear(); depthSortedDamageable.Clear();
@@ -197,7 +197,7 @@ namespace Barotrauma
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null) public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{ {
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
foreach (MapEntity e in entitiesToRender) foreach (MapEntity e in entitiesToRender)
{ {
@@ -217,7 +217,7 @@ namespace Barotrauma
public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null) public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{ {
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList(); var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
foreach (MapEntity e in entitiesToRender) foreach (MapEntity e in entitiesToRender)
{ {
@@ -1,32 +1,12 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Networking; using Barotrauma.Networking;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
namespace Barotrauma namespace Barotrauma
{ {
partial class EntitySpawner : Entity, IServerSerializable partial class EntitySpawner : Entity, IServerSerializable
{ {
/// <summary> public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>();
/// Thread-safe queue for received entity spawn/remove events from the server.
/// </summary>
private readonly ConcurrentQueue<(Entity entity, bool isRemoval)> receivedEventsQueue = new ConcurrentQueue<(Entity entity, bool isRemoval)>();
/// <summary>
/// Gets a thread-safe snapshot of received events.
/// </summary>
public IEnumerable<(Entity entity, bool isRemoval)> GetReceivedEventsSnapshot()
{
return receivedEventsQueue.ToArray();
}
/// <summary>
/// Clears all received events from the queue.
/// </summary>
void ResetReceivedEvents()
{
while (receivedEventsQueue.TryDequeue(out _)) { }
}
public void ClientEventRead(IReadMessage message, float sendingTime) public void ClientEventRead(IReadMessage message, float sendingTime)
{ {
@@ -54,7 +34,7 @@ namespace Barotrauma
{ {
DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found."); DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found.");
} }
receivedEventsQueue.Enqueue((entity, true)); receivedEvents.Add((entity, true));
} }
else else
{ {
@@ -77,7 +57,7 @@ namespace Barotrauma
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier);
} }
} }
receivedEventsQueue.Enqueue((newItem, false)); receivedEvents.Add((newItem, false));
} }
break; break;
case (byte)SpawnableType.Character: case (byte)SpawnableType.Character:
@@ -88,7 +68,7 @@ namespace Barotrauma
} }
else else
{ {
receivedEventsQueue.Enqueue((character, false)); receivedEvents.Add((character, false));
} }
break; break;
default: default:
@@ -2547,7 +2547,7 @@ namespace Barotrauma.Networking
segmentTable.StartNewSegment(ClientNetSegment.SyncIds); segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID); outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID); outmsg.WriteUInt16(SpoofEntityManagerReceivedId ? (ushort)1 : EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID); outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
@@ -3370,6 +3370,12 @@ namespace Barotrauma.Networking
{ {
get { return votingInterface; } get { return votingInterface; }
} }
/// <summary>
/// Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.
/// </summary>
public bool SpoofEntityManagerReceivedId { get; set; }
private VotingInterface votingInterface; private VotingInterface votingInterface;
public bool TypingChatMessage(GUITextBox textBox, string text) public bool TypingChatMessage(GUITextBox textBox, string text)
@@ -3916,7 +3922,7 @@ namespace Barotrauma.Networking
{ {
errorLines.Add(""); errorLines.Add("");
errorLines.Add("EntitySpawner events:"); errorLines.Add("EntitySpawner events:");
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot()) foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
{ {
errorLines.Add( errorLines.Add(
(isRemoval ? "Remove " : "Create ") + (isRemoval ? "Remove " : "Create ") +
@@ -89,6 +89,7 @@ namespace Barotrauma.Networking
{ {
byte queueId = msg.ReadByte(); byte queueId = msg.ReadByte();
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8); float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
bool isRadio = msg.ReadBoolean();
VoipQueue queue = queues.Find(q => q.QueueID == queueId); VoipQueue queue = queues.Find(q => q.QueueID == queueId);
if (queue == null) if (queue == null)
@@ -117,19 +118,21 @@ namespace Barotrauma.Networking
float rangeMultiplier = spectating ? 2.0f : 1.0f; float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent senderRadio = null; WifiComponent senderRadio = null;
var messageType = var messageType = isRadio ? ChatMessageType.Radio : ChatMessageType.Default;
!client.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(client.Character, out senderRadio) &&
(spectating || (ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio)))
? ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f; client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio) if (messageType == ChatMessageType.Radio)
{ {
//If the client cannot establish a radio, use a headsets default range as a fallback to calculate the radio noise.
//This cannot happen in an un-modded setting as CanUseRadio is part of the server side check for isRadio to be true.
ChatMessage.CanUseRadio(client.Character, out senderRadio);
float senderRadioRange = (senderRadio == null) ? 35000.0f : senderRadio.Range;
client.VoipSound.UsingRadio = true; client.VoipSound.UsingRadio = true;
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier); client.VoipSound.SetRange(senderRadioRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadioRange * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating) if (distanceFactor > RangeNear && !spectating)
{ {
//noise starts increasing exponentially after 40% range //noise starts increasing exponentially after 40% range
@@ -533,6 +533,18 @@ namespace Barotrauma
return true; return true;
} }
}; };
new GUITickBox(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 280) },
"Lenient server startup timeouts")
{
Selected = NetConfig.UseLenientHandshake,
ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load.",
OnSelected = (tickBox) =>
{
NetConfig.UseLenientHandshake = tickBox.Selected;
return true;
}
};
#endif #endif
var minButtonSize = new Point(120, 20); var minButtonSize = new Point(120, 20);
@@ -1126,6 +1138,11 @@ namespace Barotrauma
} }
#endif #endif
if (NetConfig.UseLenientHandshake)
{
arguments.Add("-lenienthandshake");
}
var processInfo = new ProcessStartInfo var processInfo = new ProcessStartInfo
{ {
FileName = fileName, FileName = fileName,
@@ -1,19 +1,22 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.IO; using Barotrauma.IO;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Sounds;
using Barotrauma.Steam; using Barotrauma.Steam;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using Steamworks;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs.Events;
using Barotrauma.Sounds;
namespace Barotrauma namespace Barotrauma
{ {
@@ -2044,6 +2047,8 @@ namespace Barotrauma
private bool SaveSubToFile(string name, ContentPackage packageToSaveTo) private bool SaveSubToFile(string name, ContentPackage packageToSaveTo)
{ {
bool remoteStorageWasEnabled = Submarine.MainSub.Info.SaveToRemoteStorage;
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
static string getExistingFilePath(ContentPackage package, string fileName) static string getExistingFilePath(ContentPackage package, string fileName)
@@ -2272,6 +2277,16 @@ namespace Barotrauma
linkedSubBox.AddItem(sub.Name, sub); linkedSubBox.AddItem(sub.Name, sub);
} }
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
if (remoteStorageWasEnabled)
{
Submarine.MainSub.Info.SaveToRemoteStorage = true;
RemoteStorageHelper.TryWrite(
localPath: MainSub.Info.FilePath,
saveAs: MainSub.Info.FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false),
allowOverwrite: true);
}
} }
} }
} }
@@ -3223,6 +3238,42 @@ namespace Barotrauma
previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y)); previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y));
if (SteamManager.IsInitialized)
{
GUILayoutGroup remoteStorageArea = new(
new RectTransform(new Vector2(1f, 0.05f), rightColumn.RectTransform,
minSize: new Point(0, minHeight)),
isHorizontal: true,
childAnchor: Anchor.CenterLeft)
{
Stretch = true,
AbsoluteSpacing = 5
};
new GUITextBlock(new RectTransform(Vector2.One, remoteStorageArea.RectTransform),
TextManager.Get("RemoteStorageToggle.Title"), textAlignment: Alignment.CenterLeft, wrap: true);
new GUITickBox(new RectTransform(Vector2.One, remoteStorageArea.RectTransform), label: "")
{
OnAddedToGUIUpdateList = component =>
{
// Values may change outside of game.
component.Enabled = SteamRemoteStorage.IsCloudEnabledForAccount;
component.ToolTip = !SteamRemoteStorage.IsCloudEnabledForAccount ? TextManager.Get("RemoteStorageToggle.Disabled") : "";
((GUITickBox)component).SetSelected(SteamRemoteStorage.IsCloudEnabled && MainSub.Info.SaveToRemoteStorage, callOnSelected: false);
},
OnSelected = tickBox =>
{
if (tickBox.Selected && !SteamRemoteStorage.IsCloudEnabledForApp)
{
RemoteStorageHelper.AskToEnable(onAccepted: () => MainSub.Info.SaveToRemoteStorage = true);
return false;
}
return MainSub.Info.SaveToRemoteStorage = tickBox.Selected;
}
};
}
var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true); var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true);
GUIButton createTabberBtn(string labelTag) GUIButton createTabberBtn(string labelTag)
@@ -3746,7 +3797,7 @@ namespace Barotrauma
} }
var package = GetLocalPackageThatOwnsSub(subInfo); var package = GetLocalPackageThatOwnsSub(subInfo);
if (package != null) if (package != null || subInfo.IsFromRemoteStorage)
{ {
deleteBtn.Enabled = true; deleteBtn.Enabled = true;
} }
@@ -3771,13 +3822,29 @@ namespace Barotrauma
searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); };
searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; };
var sortedSubs = GetLoadableSubs() List<SubmarineInfo> allSubs = [.. SubmarineInfo.SavedSubmarines];
.OrderBy(s => s.Type) foreach (SteamRemoteStorage.RemoteFile remoteFile in SteamRemoteStorage.Files.Where(file => file.Filename.EndsWith(".sub")))
.ThenBy(s => s.Name) {
.ToList(); if (!remoteFile.TryRead(out byte[] bytes)) { continue; }
using System.IO.MemoryStream stream = new(bytes);
using GZipStream zipStream = new(stream, CompressionMode.Decompress);
if (XMLExtensions.TryLoadXml(zipStream) is not XDocument doc)
{
DebugConsole.ThrowError($"{RemoteStorageHelper.DebugPrefix} Failed to load submarine \"{remoteFile.Filename}\" from remote storage: file is not a valid XML document.");
continue;
}
SubmarineInfo subInfo = new(remoteFile.Filename, element: doc.Root, tryLoad: false) { IsFromRemoteStorage = true };
allSubs.Add(subInfo);
}
IOrderedEnumerable<SubmarineInfo> sortedSubs = allSubs
.OrderBy(kvp => kvp.Type)
.ThenBy(kvp => kvp.Name)
.ThenBy(kvp => kvp.IsFromRemoteStorage);
SubmarineInfo prevSub = null; SubmarineInfo prevSub = null;
foreach (SubmarineInfo sub in sortedSubs) foreach (SubmarineInfo sub in sortedSubs)
{ {
if (prevSub == null || prevSub.Type != sub.Type) if (prevSub == null || prevSub.Type != sub.Type)
@@ -3795,35 +3862,44 @@ namespace Barotrauma
prevSub = sub; prevSub = sub;
} }
string pathWithoutUserName = Path.GetFullPath(sub.FilePath); string displayPath = sub.FilePath;
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); if (sub.IsFromRemoteStorage)
if (pathWithoutUserName.StartsWith(saveFolder))
{ {
pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; displayPath += $" {TextManager.Get("RemoteStorage")}";
} }
else else
{ {
pathWithoutUserName = sub.FilePath; string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
string fullPath = Path.GetFullPath(displayPath);
if (fullPath.StartsWith(saveFolder))
{
displayPath = $"...{fullPath[saveFolder.Length..]}";
}
} }
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, LocalizedString limitedName = ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80);
ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) GUITextBlock textBlock = new(new RectTransform(Vector2.UnitX, subList.Content.RectTransform) { MinSize = new Point(0, 30) }, limitedName)
{ {
UserData = sub, UserData = sub,
ToolTip = pathWithoutUserName ToolTip = displayPath
}; };
if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false)) if (sub.IsFromRemoteStorage)
{
// remote storage
textBlock.OverrideTextColor(RemoteStorageHelper.SteamColor);
}
else if (ContentPackageManager.VanillaCorePackage == null || ContentPackageManager.VanillaCorePackage.Files.None(f => f.Path == sub.FilePath))
{ {
if (GetLocalPackageThatOwnsSub(sub) == null && if (GetLocalPackageThatOwnsSub(sub) == null &&
ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage) ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage)
{ {
//workshop mod // workshop mod
textBlock.OverrideTextColor(Color.MediumPurple); textBlock.OverrideTextColor(Color.MediumPurple);
} }
else else
{ {
//local mod // local mod
textBlock.OverrideTextColor(GUIStyle.TextColorBright); textBlock.OverrideTextColor(GUIStyle.TextColorBright);
} }
} }
@@ -4018,10 +4094,9 @@ namespace Barotrauma
return false; return false;
} }
if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; } if (subList.SelectedComponent?.UserData is not SubmarineInfo selectedSubInfo) { return false; }
var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo); if (!selectedSubInfo.IsFromRemoteStorage && GetLocalPackageThatOwnsSub(selectedSubInfo) is null)
if (ownerPackage is null)
{ {
if (IsVanillaSub(selectedSubInfo)) if (IsVanillaSub(selectedSubInfo))
{ {
@@ -4183,21 +4258,23 @@ namespace Barotrauma
{ {
if (sub == null) { return; } if (sub == null) { return; }
//If the sub is included in a content package that only defines that one sub, // If the sub is included in a content package that only defines that one sub,
//check that it's a local content package and only allow deletion if it is. // check that it's a local content package and only allow deletion if it is.
//(deleting from the Submarines folder is also currently allowed, but this is temporary) // (deleting from the Submarines folder is also currently allowed, but this is temporary)
var subPackage = GetLocalPackageThatOwnsSub(sub); ContentPackage subPackage = GetLocalPackageThatOwnsSub(sub);
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { subPackage = null; }
if (!sub.IsFromRemoteStorage && subPackage == null) { return; }
var msgBox = new GUIMessageBox( GUIMessageBox msgBox = new(TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), [TextManager.Get("Yes"), TextManager.Get("Cancel")]);
TextManager.Get("DeleteDialogLabel"),
TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
msgBox.Buttons[0].OnClicked += (btn, userData) => msgBox.Buttons[0].OnClicked += (btn, userData) =>
{ {
try if (sub.IsFromRemoteStorage)
{ {
if (subPackage != null) RemoteStorageHelper.TryDelete(sub.FilePath);
}
else if (subPackage != null)
{
try
{ {
File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false); File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false);
ModProject modProject = new ModProject(subPackage); ModProject modProject = new ModProject(subPackage);
@@ -4209,16 +4286,16 @@ namespace Barotrauma
MainSub.Info.FilePath = null; MainSub.Info.FilePath = null;
} }
} }
sub.Dispose();
CreateLoadScreen();
}
catch (Exception e) catch (Exception e)
{ {
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e); DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
} }
return true; }
sub.Dispose();
CreateLoadScreen();
return msgBox.Close(btn, userData);
}; };
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += msgBox.Close;
} }
@@ -445,7 +445,7 @@ namespace Barotrauma
{ {
DebugConsole.NewMessage("Missing Localization for property: " + propertyTag); DebugConsole.NewMessage("Missing Localization for property: " + propertyTag);
MissingLocalizations.Add($"sp.{propertyTag}.name|{displayName}"); MissingLocalizations.Add($"sp.{propertyTag}.name|{displayName}");
MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>().Description}"); MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>()?.Description}");
} }
} }
#endif #endif
@@ -467,7 +467,7 @@ namespace Barotrauma
} }
if (toolTip.IsNullOrEmpty()) if (toolTip.IsNullOrEmpty())
{ {
toolTip = property.GetAttribute<Serialize>().Description; toolTip = property.GetAttribute<Serialize>()?.Description;
} }
GUIComponent propertyField = null; GUIComponent propertyField = null;
@@ -503,24 +503,14 @@ namespace Barotrauma.Sounds
mutex = new object(); mutex = new object();
} }
// Use the playingChannels lock to protect both channel assignment AND OpenAL operations.
// This prevents race conditions when multiple threads try to play sounds simultaneously
// (e.g., during Parallel.ForEach in MapEntity.UpdateAll).
int poolIndex = (int)sound.SourcePoolIndex;
object channelsLock = sound.Owner.GetPlayingChannelsLock(sound.SourcePoolIndex);
#if !DEBUG #if !DEBUG
try try
{ {
#endif #endif
lock (channelsLock)
{
if (mutex != null) { Monitor.Enter(mutex); } if (mutex != null) { Monitor.Enter(mutex); }
try if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
{ {
if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances) ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this);
{
ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
} }
if (ALSourceIndex >= 0) if (ALSourceIndex >= 0)
@@ -595,18 +585,18 @@ namespace Barotrauma.Sounds
SetProperties(); SetProperties();
} }
} }
}
finally
{
if (mutex != null) { Monitor.Exit(mutex); }
}
}
#if !DEBUG #if !DEBUG
} }
catch catch
{ {
throw; throw;
} }
finally
{
#endif
if (mutex != null) { Monitor.Exit(mutex); }
#if !DEBUG
}
#endif #endif
void SetProperties() void SetProperties()
@@ -417,15 +417,6 @@ namespace Barotrauma.Sounds
return sourcePools[(int)poolIndex].ALSources[srcInd]; return sourcePools[(int)poolIndex].ALSources[srcInd];
} }
/// <summary>
/// Gets the lock object for the playing channels array for a specific pool.
/// Used to protect OpenAL operations that need to be atomic with channel assignment.
/// </summary>
public object GetPlayingChannelsLock(SourcePoolIndex poolIndex)
{
return playingChannels[(int)poolIndex];
}
public int AssignFreeSourceToChannel(SoundChannel newChannel) public int AssignFreeSourceToChannel(SoundChannel newChannel)
{ {
if (Disabled) { return -1; } if (Disabled) { return -1; }
@@ -436,18 +427,6 @@ namespace Barotrauma.Sounds
lock (playingChannels[poolIndex]) lock (playingChannels[poolIndex])
{ {
return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex);
}
}
/// <summary>
/// Assigns a free source to a channel without locking.
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
/// </summary>
public int AssignFreeSourceToChannelUnsafe(SoundChannel newChannel, int poolIndex)
{
if (Disabled) { return -1; }
for (int i = 0; i < playingChannels[poolIndex].Length; i++) for (int i = 0; i < playingChannels[poolIndex].Length; i++)
{ {
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying) if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
@@ -457,6 +436,7 @@ namespace Barotrauma.Sounds
return i; return i;
} }
} }
}
//we couldn't get a free source to assign to this channel! //we couldn't get a free source to assign to this channel!
return -1; return -1;
@@ -496,25 +476,13 @@ namespace Barotrauma.Sounds
int count = 0; int count = 0;
lock (playingChannels[(int)sound.SourcePoolIndex]) lock (playingChannels[(int)sound.SourcePoolIndex])
{ {
count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex); for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++)
{
if (playingChannels[(int)sound.SourcePoolIndex][i] != null &&
playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename)
{
if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; };
} }
return count;
}
/// <summary>
/// Counts playing instances without locking.
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
/// </summary>
public int CountPlayingInstancesUnsafe(Sound sound, int poolIndex)
{
if (Disabled) { return 0; }
int count = 0;
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
{
if (playingChannels[poolIndex][i] != null &&
playingChannels[poolIndex][i].Sound.Filename == sound.Filename)
{
if (playingChannels[poolIndex][i].IsPlaying) { count++; };
} }
} }
return count; return count;
@@ -0,0 +1,34 @@
#nullable enable
using Steamworks;
using System;
namespace Barotrauma.Steam;
internal static partial class RemoteStorageHelper
{
/// <summary>
/// Asks the user if they wish to enable remote storage. Accepting enables it automatically.
/// </summary>
/// <param name="onAccepted">Invoked when the user accepts enabling remote storage.</param>
/// <param name="onRejected">Invoked when the user rejects enabling remote storage.</param>
/// <remarks>Closes automatically if remote storage was enabled outside of the game, or if remote storage can not be enabled.</remarks>
public static void AskToEnable(Action? onAccepted = null, Action? onRejected = null)
{
GUIMessageBox confirmBox = new GUIMessageBox(
TextManager.Get("RemoteStorageEnablePopup.Header"),
TextManager.Get("RemoteStorageEnablePopup.Text"),
[TextManager.Get("Yes"), TextManager.Get("No")],
autoCloseCondition: () => !SteamRemoteStorage.IsCloudEnabledForAccount || SteamRemoteStorage.IsCloudEnabledForApp);
confirmBox.Buttons[0].OnClicked += (btn, data) =>
{
SteamRemoteStorage.IsCloudEnabledForApp = true;
onAccepted?.Invoke();
return confirmBox.Close(btn, data);
};
confirmBox.Buttons[1].OnClicked += (btn, data) =>
{
onRejected?.Invoke();
return confirmBox.Close(btn, data);
};
}
}
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright> <Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -28,14 +28,11 @@ namespace Barotrauma
} }
} }
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } && if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
causeOfDeath != CauseOfDeathType.Disconnected) causeOfDeath != CauseOfDeathType.Disconnected)
{ {
Client ownerClient = clients.FirstOrDefault(c => c.Character == this); Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
if (ownerClient != null) if (ownerClient != null)
{ {
ownerClient.SpectateOnly = true; ownerClient.SpectateOnly = true;
@@ -54,7 +51,7 @@ namespace Barotrauma
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter)) if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
{ {
var ownerClient = clients.FirstOrDefault(c => c.Character == this); var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (ownerClient != null) if (ownerClient != null)
{ {
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient); (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient);
@@ -65,7 +62,7 @@ namespace Barotrauma
if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this) if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this)
{ {
var owner = clients.FirstOrDefault(c => c.Character == this); var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (owner != null) if (owner != null)
{ {
if (!LuaCsSetup.Instance.Game.overrideTraitors) if (!LuaCsSetup.Instance.Game.overrideTraitors)
@@ -74,11 +71,11 @@ namespace Barotrauma
} }
} }
} }
foreach (Client client in clients) foreach (Client client in GameMain.Server.ConnectedClients)
{ {
if (client.InGame) if (client.InGame)
{ {
client.TryEnqueuePositionUpdate(this); client.PendingPositionUpdates.Enqueue(this);
} }
} }
} }
@@ -486,9 +486,7 @@ namespace Barotrauma
case ControlEventData controlEventData: case ControlEventData controlEventData:
Client owner = controlEventData.Owner; Client owner = controlEventData.Owner;
msg.WriteBoolean(owner == c && owner.Character == this); msg.WriteBoolean(owner == c && owner.Character == this);
// Create snapshot to avoid concurrent access issues during parallel updates msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
var connectedClients = GameMain.Server.ConnectedClients.ToArray();
msg.WriteByte(owner != null && owner.Character == this && connectedClients.Contains(owner) ? owner.SessionId : (byte)0);
msg.WriteBoolean(info is { RenamingEnabled: true }); msg.WriteBoolean(info is { RenamingEnabled: true });
break; break;
case CharacterStatusEventData statusEventData: case CharacterStatusEventData statusEventData:
@@ -744,9 +742,7 @@ namespace Barotrauma
return; return;
} }
// Create snapshot to avoid concurrent access issues during parallel updates Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
var clients = GameMain.Server.ConnectedClients.ToArray();
Client ownerClient = clients.FirstOrDefault(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
if (ownerClient != null) if (ownerClient != null)
{ {
msg.WriteBoolean(true); msg.WriteBoolean(true);
@@ -1799,22 +1799,35 @@ namespace Barotrauma
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{ {
Submarine.MainSub.SetPosition(cursorWorldPos); submarineToTeleport.SetPosition(cursorWorldPos);
} }
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{ {
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); submarineToTeleport.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
} }
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{ {
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); submarineToTeleport.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
} }
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{ {
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
if (Level.Loaded?.EndOutpost == null) if (Level.Loaded?.EndOutpost == null)
{ {
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
@@ -82,13 +82,10 @@ namespace Barotrauma
private bool IsBlockedByAnotherConversation(IEnumerable<Entity> targets, float duration) private bool IsBlockedByAnotherConversation(IEnumerable<Entity> targets, float duration)
{ {
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (targets == null || targets.None()) if (targets == null || targets.None())
{ {
//if the action doesn't target anyone in specific, it's shown to every client //if the action doesn't target anyone in specific, it's shown to every client
foreach (var client in clients) foreach (var client in GameMain.Server.ConnectedClients)
{ {
if (IsBlockedByAnotherConversation(client, duration)) { return true; } if (IsBlockedByAnotherConversation(client, duration)) { return true; }
} }
@@ -98,7 +95,7 @@ namespace Barotrauma
foreach (Entity e in targets) foreach (Entity e in targets)
{ {
if (e is not Character character || !character.IsRemotePlayer) { continue; } if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = clients.FirstOrDefault(c => c.Character == character); Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; } if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; }
} }
} }
@@ -120,16 +117,13 @@ namespace Barotrauma
partial void ShowDialog(Character speaker, Character targetCharacter) partial void ShowDialog(Character speaker, Character targetCharacter)
{ {
targetClients.Clear(); targetClients.Clear();
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!TargetTag.IsEmpty) if (!TargetTag.IsEmpty)
{ {
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag); IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
foreach (Entity e in entities) foreach (Entity e in entities)
{ {
if (e is not Character character || !character.IsRemotePlayer) { continue; } if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = clients.FirstOrDefault(c => c.Character == character); Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (targetClient != null) if (targetClient != null)
{ {
targetClients.Add(targetClient); targetClients.Add(targetClient);
@@ -141,7 +135,7 @@ namespace Barotrauma
} }
else else
{ {
foreach (Client c in clients) foreach (Client c in GameMain.Server.ConnectedClients)
{ {
if (CanClientReceive(c)) if (CanClientReceive(c))
{ {
@@ -9,13 +9,9 @@ namespace Barotrauma;
partial class EventLogAction : EventAction partial class EventLogAction : EventAction
{ {
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText) partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
{ {
if (eventLog == null) { return; } if (eventLog == null) { return; }
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!TargetTag.IsEmpty) if (!TargetTag.IsEmpty)
{ {
List<Client> targetClients = new List<Client>(); List<Client> targetClients = new List<Client>();
@@ -23,7 +19,7 @@ partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
{ {
if (target is Character character) if (target is Character character)
{ {
var ownerClient = clients.FirstOrDefault(c => c.Character == character); var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (ownerClient != null) if (ownerClient != null)
{ {
targetClients.Add(ownerClient); targetClients.Add(ownerClient);
@@ -42,7 +38,7 @@ partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
} }
else else
{ {
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, clients) && ShowInServerLog) if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog)
{ {
Log(targetClients: null); Log(targetClients: null);
} }
@@ -56,5 +52,5 @@ partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText, GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat); ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
} }
} }
} }
@@ -1,5 +1,3 @@
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
partial class EventObjectiveAction : EventAction partial class EventObjectiveAction : EventAction
@@ -15,12 +13,9 @@ namespace Barotrauma
ParentObjectiveId, ParentObjectiveId,
CanBeCompleted); CanBeCompleted);
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (TargetTag.IsEmpty) if (TargetTag.IsEmpty)
{ {
foreach (var client in clients) foreach (var client in GameMain.Server.ConnectedClients)
{ {
if (client.Character == null) { continue; } if (client.Character == null) { continue; }
EventManager.ServerWriteObjective(client, objective); EventManager.ServerWriteObjective(client, objective);
@@ -31,7 +26,7 @@ namespace Barotrauma
foreach (var target in ParentEvent.GetTargets(TargetTag)) foreach (var target in ParentEvent.GetTargets(TargetTag))
{ {
if (target is not Character character) { continue; } if (target is not Character character) { continue; }
var ownerClient = clients.FirstOrDefault(c => c.Character == character); var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (ownerClient == null) { continue; } if (ownerClient == null) { continue; }
EventManager.ServerWriteObjective(ownerClient, objective); EventManager.ServerWriteObjective(ownerClient, objective);
} }
@@ -14,10 +14,8 @@ partial class HighlightAction : EventAction
IEnumerable<Client>? targetClients = null; IEnumerable<Client>? targetClients = null;
if (targetCharacters != null) if (targetCharacters != null)
{ {
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
targetClients = targetCharacters targetClients = targetCharacters
.Select(c => clients.FirstOrDefault(client => client.Character == c)) .Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c))
.Where(c => c != null)!; .Where(c => c != null)!;
} }
GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients)); GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients));
@@ -1,6 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -23,9 +22,7 @@ namespace Barotrauma
private static void NotifyMissionUnlock(Mission mission) private static void NotifyMissionUnlock(Mission mission)
{ {
// Create snapshot to avoid concurrent access issues during parallel updates foreach (Client client in GameMain.Server.ConnectedClients)
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients)
{ {
NotifyMissionUnlock(mission, client); NotifyMissionUnlock(mission, client);
} }
@@ -33,7 +33,7 @@ namespace Barotrauma
byte selectedOption = inc.ReadByte(); byte selectedOption = inc.ReadByte();
bool isIgnore = selectedOption == byte.MaxValue; bool isIgnore = selectedOption == byte.MaxValue;
foreach (Event ev in _activeEvents) foreach (Event ev in activeEvents)
{ {
if (ev is not ScriptedEvent scriptedEvent) { continue; } if (ev is not ScriptedEvent scriptedEvent) { continue; }
@@ -1179,6 +1179,7 @@ namespace Barotrauma
NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg); NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg);
if (GameMain.Server is null) { return; } if (GameMain.Server is null) { return; }
if (transfer.Amount <= 0) { return; }
if (transfer.Sender.TryUnwrap(out var id)) if (transfer.Sender.TryUnwrap(out var id))
{ {
@@ -76,9 +76,7 @@ namespace Barotrauma.Items.Components
{ {
var (msg, deliveryMethod) = PrepareToSend(opcode, data); var (msg, deliveryMethod) = PrepareToSend(opcode, data);
// Create snapshot to avoid concurrent access issues during parallel updates foreach (Client client in GameMain.Server.ConnectedClients)
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients)
{ {
if (predicate is not null && !predicate(client)) { continue; } if (predicate is not null && !predicate(client)) { continue; }
@@ -193,13 +193,6 @@ namespace Barotrauma
(item.PreviousParentInventory == null || (item.PreviousParentInventory == null ||
!sender.Character.CanAccessInventory(item.PreviousParentInventory)); !sender.Character.CanAccessInventory(item.PreviousParentInventory));
// Prevent modified clients from being able to steal items from characters by item swapping with an existing item
// due to drag and drop being enabled
if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null)
{
itemAccessDenied = true;
}
//more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed //more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed
if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied) if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied)
{ {
@@ -211,15 +204,15 @@ namespace Barotrauma
#if DEBUG || UNSTABLE #if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow); DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif #endif
if (item.body != null) if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
{ {
sender.TryEnqueuePositionUpdate(item); sender.PendingPositionUpdates.Enqueue(item);
} }
item.PositionUpdateInterval = 0.0f; item.PositionUpdateInterval = 0.0f;
continue; continue;
} }
} }
TryPutItem(item, slotIndex, true, true, sender.Character, false); TryPutItem(item, slotIndex, allowSwapping: false, allowCombine: false, user: sender.Character, createNetworkEvent: false);
for (int j = 0; j < capacity; j++) for (int j = 0; j < capacity; j++)
{ {
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID)) if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
@@ -29,9 +29,7 @@ namespace Barotrauma
//don't create updates if all clients are very far from the hull //don't create updates if all clients are very far from the hull
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
// Create snapshot to avoid concurrent access issues during parallel updates if (!GameMain.Server.ConnectedClients.Any(c =>
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!clients.Any(c =>
(c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) || (c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) ||
(c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) ) (c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) )
{ {
@@ -8,6 +8,17 @@ namespace Barotrauma.Networking
{ {
partial class ChatMessage partial class ChatMessage
{ {
private static string SanitizeText(Client client, string text)
{
if (!client.HasPermission(ClientPermissions.SpamImmunity))
{
// Prevent clients without spam immunity from being able to use RichString features
text = text.Replace('‖', ' ');
}
return text;
}
public static void ServerRead(IReadMessage msg, Client c) public static void ServerRead(IReadMessage msg, Client c)
{ {
c.KickAFKTimer = 0.0f; c.KickAFKTimer = 0.0f;
@@ -69,8 +80,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString() ?? ""; txt = msg.ReadString() ?? "";
} }
// Sanitize incoming text message from client so they can't use RichString features txt = SanitizeText(c, txt);
txt = txt.Replace('‖', ' ');
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; } if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }
@@ -64,32 +64,6 @@ namespace Barotrauma.Networking
// key = entity, value = NetTime.Now when sending // key = entity, value = NetTime.Now when sending
public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>(); public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>(); public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
private readonly HashSet<Entity> pendingPositionUpdatesSet = new HashSet<Entity>();
/// <summary>
/// Attempts to enqueue a position update for the given entity. Returns true if the entity was added, false if it was already in the queue.
/// Uses HashSet for O(1) lookup instead of Queue.Contains() which is O(n).
/// </summary>
public bool TryEnqueuePositionUpdate(Entity entity)
{
if (pendingPositionUpdatesSet.Add(entity))
{
PendingPositionUpdates.Enqueue(entity);
return true;
}
return false;
}
/// <summary>
/// Dequeues a position update and removes it from the HashSet tracking.
/// </summary>
public Entity DequeuePositionUpdate()
{
if (PendingPositionUpdates.Count == 0) { return null; }
var entity = PendingPositionUpdates.Dequeue();
pendingPositionUpdatesSet.Remove(entity);
return entity;
}
public bool ReadyToStart; public bool ReadyToStart;
@@ -152,7 +126,7 @@ namespace Barotrauma.Networking
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
{ {
syncedKarma = karma; syncedKarma = karma;
GameMain.NetworkMember.IncrementLastClientListUpdateID(); GameMain.NetworkMember.LastClientListUpdateID++;
} }
} }
} }
@@ -379,7 +353,6 @@ namespace Barotrauma.Networking
{ {
NeedsMidRoundSync = false; NeedsMidRoundSync = false;
PendingPositionUpdates.Clear(); PendingPositionUpdates.Clear();
pendingPositionUpdatesSet.Clear();
EntityEventLastSent.Clear(); EntityEventLastSent.Clear();
LastSentEntityEventID = 0; LastSentEntityEventID = 0;
LastRecvEntityEventID = 0; LastRecvEntityEventID = 0;
@@ -174,7 +174,7 @@ namespace Barotrauma.Networking
StartTime = DateTime.Now; StartTime = DateTime.Now;
OnStarted(transfer); OnStarted(transfer);
GameMain.Server.IncrementLastClientListUpdateID(); GameMain.Server.LastClientListUpdateID++;
return transfer; return transfer;
} }
@@ -204,7 +204,7 @@ namespace Barotrauma.Networking
if (numRemoved > 0 || endedTransfers.Count > 0) if (numRemoved > 0 || endedTransfers.Count > 0)
{ {
GameMain.Server.IncrementLastClientListUpdateID(); GameMain.Server.LastClientListUpdateID++;
} }
} }
@@ -327,7 +327,7 @@ namespace Barotrauma.Networking
} }
} }
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
if (newClient.Connection == OwnerConnection && OwnerConnection != null) if (newClient.Connection == OwnerConnection && OwnerConnection != null)
{ {
@@ -742,6 +742,11 @@ namespace Barotrauma.Networking
{ {
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
} }
GameAnalyticsManager.AddErrorEventOnce(
"GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
} }
} }
@@ -899,7 +904,10 @@ namespace Barotrauma.Networking
string subHash = inc.ReadString(); string subHash = inc.ReadString();
CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc); CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); var matchingSub =
ServerSettings.AllowSubVoting ?
Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, connectedClients) :
SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
if (GameStarted) if (GameStarted)
{ {
@@ -2140,7 +2148,7 @@ namespace Barotrauma.Networking
{ {
if (lastSent > NetTime.Now - updateInterval) { continue; } if (lastSent > NetTime.Now - updateInterval) { continue; }
} }
c.TryEnqueuePositionUpdate(otherCharacter); if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
} }
foreach (Submarine sub in Submarine.Loaded) foreach (Submarine sub in Submarine.Loaded)
@@ -2149,7 +2157,7 @@ namespace Barotrauma.Networking
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle) // (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; } if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; } if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
c.TryEnqueuePositionUpdate(sub); if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
} }
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
@@ -2166,7 +2174,7 @@ namespace Barotrauma.Networking
{ {
if (lastSent > NetTime.Now - updateInterval) { continue; } if (lastSent > NetTime.Now - updateInterval) { continue; }
} }
c.TryEnqueuePositionUpdate(item); if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
} }
} }
@@ -2210,7 +2218,7 @@ namespace Barotrauma.Networking
entity.Removed || entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) (entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
{ {
c.DequeuePositionUpdate(); c.PendingPositionUpdates.Dequeue();
continue; continue;
} }
@@ -2232,7 +2240,7 @@ namespace Barotrauma.Networking
outmsg.WritePadBits(); outmsg.WritePadBits();
c.PositionUpdateLastSent[entity] = (float)NetTime.Now; c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
c.DequeuePositionUpdate(); c.PendingPositionUpdates.Dequeue();
} }
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes; positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
@@ -3207,7 +3215,7 @@ namespace Barotrauma.Networking
initiatedStartGame = false; initiatedStartGame = false;
GameMain.ResetFrameTime(); GameMain.ResetFrameTime();
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
roundStartTime = DateTime.Now; roundStartTime = DateTime.Now;
@@ -3506,7 +3514,7 @@ namespace Barotrauma.Networking
{ {
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
//increment the ID to make sure the current server-side name is treated as the "latest", //increment the ID to make sure the current server-side name is treated as the "latest",
//and the client correctly reverts back to the old name //and the client correctly reverts back to the old name
c.NameId++; c.NameId++;
@@ -3520,7 +3528,7 @@ namespace Barotrauma.Networking
if (result != null) if (result != null)
{ {
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
return result.Value; return result.Value;
} }
@@ -3537,14 +3545,14 @@ namespace Barotrauma.Networking
c.Name = newName; c.Name = newName;
c.RejectedName = string.Empty; c.RejectedName = string.Empty;
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
return true; return true;
} }
else else
{ {
//update client list even if the name cannot be changed to the one sent by the client, //update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is //so the client will be informed what their actual name is
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
return false; return false;
} }
} }
@@ -4783,9 +4791,7 @@ namespace Barotrauma.Networking
public static string CharacterLogName(Character character) public static string CharacterLogName(Character character)
{ {
if (character == null) { return "[NULL]"; } if (character == null) { return "[NULL]"; }
// Create snapshot to avoid concurrent access issues during parallel updates Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
var clients = GameMain.Server.ConnectedClients.ToArray();
Client client = clients.FirstOrDefault(c => c.Character == character);
return ClientLogName(client, character.LogName); return ClientLogName(client, character.LogName);
} }
@@ -4796,8 +4802,8 @@ namespace Barotrauma.Networking
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(line, messageType)); LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(line, messageType));
GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType);
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients) foreach (Client client in GameMain.Server.ConnectedClients)
{ {
if (!client.HasPermission(ClientPermissions.ServerLog)) continue; if (!client.HasPermission(ClientPermissions.ServerLog)) continue;
//use sendername as the message type //use sendername as the message type
@@ -4838,7 +4844,7 @@ namespace Barotrauma.Networking
private void UpdateClientLobbies() private void UpdateClientLobbies()
{ {
// Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList() // Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
IncrementLastClientListUpdateID(); LastClientListUpdateID++;
} }
private List<Client> GetPlayingClients() private List<Client> GetPlayingClients()
@@ -163,7 +163,7 @@ namespace Barotrauma
{ {
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma); GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma);
GameMain.NetworkMember.IncrementLastClientListUpdateID(); GameMain.NetworkMember.LastClientListUpdateID++;
} }
else if (existingAffliction != null) else if (existingAffliction != null)
{ {
@@ -373,7 +373,7 @@ namespace Barotrauma.Networking
// in which case we'll wait until the timeout runs out before kicking the client // in which case we'll wait until the timeout runs out before kicking the client
List<Client> toKick = inGameClients.FindAll(c => List<Client> toKick = inGameClients.FindAll(c =>
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); (!c.NeedsMidRoundSync || firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
toKick.ForEach(c => toKick.ForEach(c =>
{ {
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
@@ -279,7 +279,7 @@ namespace Barotrauma.Networking
var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null); var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null);
shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g)); shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g));
var dockingPorts = Item.ItemList.Where(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null).ToList(); var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null);
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock()); dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime) if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime)
@@ -54,13 +54,14 @@ namespace Barotrauma.Networking
{ {
if (recipient == sender) { continue; } if (recipient == sender) { continue; }
if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; } if (!CanReceive(sender, recipient, out float distanceFactor, out bool isRadio)) { continue; }
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.VOICE); msg.WriteByte((byte)ServerPacketHeader.VOICE);
msg.WriteByte((byte)queue.QueueID); msg.WriteByte((byte)queue.QueueID);
msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8); msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8);
msg.WriteBoolean(isRadio);
queue.Write(msg); queue.Write(msg);
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
@@ -68,15 +69,17 @@ namespace Barotrauma.Networking
} }
} }
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor) private static bool CanReceive(Client sender, Client recipient, out float distanceFactor, out bool isRadio)
{ {
if (Screen.Selected != GameMain.GameScreen) if (Screen.Selected != GameMain.GameScreen)
{ {
distanceFactor = 0.0f; distanceFactor = 0.0f;
isRadio = false;
return true; return true;
} }
distanceFactor = 0.0f; distanceFactor = 0.0f;
isRadio = false;
//no-one can hear muted players //no-one can hear muted players
if (sender.Muted) { return false; } if (sender.Muted) { return false; }
@@ -109,12 +112,14 @@ namespace Barotrauma.Networking
if (recipientSpectating) if (recipientSpectating)
{ {
isRadio = true;
if (recipient.SpectatePos == null) { return true; } if (recipient.SpectatePos == null) { return true; }
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f); distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f);
return distanceFactor < 1.0f; return distanceFactor < 1.0f;
} }
else if (recipientRadio != null && recipientRadio.CanReceive(senderRadio)) else if (recipientRadio != null && recipientRadio.CanReceive(senderRadio))
{ {
isRadio = true;
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f); distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f);
return true; return true;
} }
@@ -93,7 +93,6 @@ namespace Barotrauma
private static bool hasShutDown = false; private static bool hasShutDown = false;
private static void ShutDown() private static void ShutDown()
{ {
SingleThreadWorker.Instance.Dispose();
if (hasShutDown) { return; } if (hasShutDown) { return; }
hasShutDown = true; hasShutDown = true;
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version> <Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -23,6 +23,70 @@
</Holdable> </Holdable>
</Item> </Item>
<Item name="fliptestweapon" identifier="fliptestweapon" category="Weapon" cargocontaineridentifier="metalcrate" tags="mediumitem,weapon,gun,gunsmith,provocativetohumanai,mountableweapon" Scale="0.5" impactsoundtag="impact_metal_light">
<PreferredContainer primary="secarmcab" amount="1" spawnprobability="0.2" notcampaign="true" notpvp="true" />
<PreferredContainer secondary="wrecksecarmcab,abandonedsecarmcab,piratesecarmcab" spawnprobability="0.1" />
<PreferredContainer secondary="armcab" />
<Price baseprice="1000" sold="false" minleveldifficulty="30">
<Price storeidentifier="merchantoutpost" multiplier="1.5" />
<Price storeidentifier="merchantcity" multiplier="1.25" />
<Price storeidentifier="merchantresearch" multiplier="1.25" />
<Price storeidentifier="merchantmilitary" sold="true" multiplier="0.9" minavailable="1" />
<Price storeidentifier="merchantmine" multiplier="1.25" />
<Price storeidentifier="merchantarmory" sold="true" multiplier="0.9" minavailable="1" />
</Price>
<Deconstruct time="10">
<Item identifier="steel" />
<Item identifier="titaniumaluminiumalloy" />
</Deconstruct>
<InventoryIcon texture="Content/Items/InventoryIconAtlas.png" sourcerect="896,831,64,64" origin="0.5,0.5" />
<Sprite texture="Content/Items/Weapons/weapons_new.png" sourcerect="0,244,186,65" depth="0.55" origin="0.5,0.5" />
<Body width="170" height="40" density="25" />
<Holdable slots="RightHand+LeftHand" controlpose="true" holdpos="40,-20" aimpos="45,-10" handle1="-33,-15" handle2="26,5" holdangle="-25">
<StatusEffect type="OnActive" target="This" offset="80,20" OffsetCopiesEntityTransform="true">
<ParticleEmitter particle="electricshock" particlespersecond="60" copyentityangle="true" velocitymin="0" velocitymax="100" distancemin="0" distancemax="10" scalemin="0.03" scalemax="0.04" />
</StatusEffect>
</Holdable>
<Wearable slots="Bag" msg="ItemMsgEquipSelect" canbeselected="false" canbepicked="true" pickkey="Select">
<sprite name="Grenade Launcher Worn" texture="Content/Items/Weapons/weapons_new.png" canbehiddenbyotherwearables="false" sourcerect="0,244,186,65" rotation="90" depth="0.6" limb="Torso" depthlimb="LeftArm" scale="0.5" origin="0.5,0.8" />
</Wearable>
<RangedWeapon barrelpos="80,11" reload="0.8" spread="1" unskilledspread="10" combatPriority="75" drawhudwhenequipped="true" crosshairscale="0.2">
<Crosshair texture="Content/Items/Weapons/Crosshairs.png" sourcerect="0,256,256,256" />
<CrosshairPointer texture="Content/Items/Weapons/Crosshairs.png" sourcerect="256,256,256,256" />
<ParticleEmitter particle="muzzleflashchaingun" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<ParticleEmitter particle="explosionsmoke" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot1.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot2.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot3.ogg" type="OnUse" range="1000" />
<StatusEffect type="OnUse" target="This">
<Explosion range="150.0" force="2" shockwave="false" smoke="false" flames="false" flash="true" sparks="false" underwaterbubble="false" applyfireeffects="false" camerashake="6.0" />
</StatusEffect>
<RequiredItems items="grenade" type="Contained" msg="ItemMsgAmmoRequired" />
<RequiredSkill identifier="weapons" level="60" />
</RangedWeapon>
<!--Holds six grenades at a time-->
<ItemContainer capacity="6" maxstacksize="1" hideitems="false" ShowTotalStackCapacityInContainedStateIndicator="true" containedstateindicatorstyle="bullet" containedspritedepth="0.56">
<Containable items="grenade" hide="true" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="1" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="2" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="3" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="4" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="5" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="6" texture="Content/UI/StatusMonitorUI.png" sourcerect="320,448,64,64" origin="0.5,0.5" />
<SubContainer capacity="1" maxstacksize="1">
<Containable items="flashlight" hide="false" itempos="22,-1" setactive="true" />
</SubContainer>
</ItemContainer>
<aitarget sightrange="500" soundrange="500" fadeouttime="3" />
<Quality>
<QualityStat stattype="ExplosionRadius" value="0.1" />
<QualityStat stattype="ExplosionDamage" value="0.1" />
</Quality>
<Upgrade gameversion="0.10.0.0" scale="0.5" />
<SkillRequirementHint identifier="weapons" level="60" />
</Item>
<Item name="fliptestlight" identifier="fliptestlighttower" width="176" height="352" texturescale="1.0,1.0" scale="0.5" category="Decorative" subcategory="mining" noninteractable="true"> <Item name="fliptestlight" identifier="fliptestlighttower" width="176" height="352" texturescale="1.0,1.0" scale="0.5" category="Decorative" subcategory="mining" noninteractable="true">
<sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="849,1697,176,352" depth="0.97" premultiplyalpha="false" origin="0.5,0.5" /> <sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="849,1697,176,352" depth="0.97" premultiplyalpha="false" origin="0.5,0.5" />
<LightComponent range="160.0" lightcolor="255,234,181,200" IsOn="true" castshadows="false" LightOffset="200,147" allowingameediting="false"> <LightComponent range="160.0" lightcolor="255,234,181,200" IsOn="true" castshadows="false" LightOffset="200,147" allowingameediting="false">
@@ -156,8 +156,7 @@ namespace Barotrauma
Reactor reactor = item.GetComponent<Reactor>(); Reactor reactor = item.GetComponent<Reactor>();
if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); } if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
} }
pathFinder = new PathFinder(WayPoint.WayPointList, false);
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
cachedDistances.Clear(); cachedDistances.Clear();
#if CLIENT #if CLIENT
@@ -324,7 +323,7 @@ namespace Barotrauma
static CachedDistance CalculateNewCachedDistance(Character c) static CachedDistance CalculateNewCachedDistance(Character c)
{ {
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false); pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition)); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
if (path.Unreachable) { return null; } if (path.Unreachable) { return null; }
return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f)); return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
@@ -525,8 +524,7 @@ namespace Barotrauma
{ {
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
} }
else if (item.Prefab.Identifier == "nuclearshell" || else if (item.Prefab.Tags.Contains("nuclearexplosive"))
item.Prefab.Identifier == "nucleardepthcharge")
{ {
UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier()); UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
} }
@@ -1,67 +1,13 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
/// <summary>
/// Thread-safe wrapper for AITarget list operations.
/// Uses copy-on-write pattern for lock-free reads.
/// </summary>
class ThreadSafeAITargetList : IEnumerable<AITarget>
{
private volatile List<AITarget> _list = new List<AITarget>();
private readonly object _writeLock = new object();
public int Count => _list.Count;
public void Add(AITarget target)
{
lock (_writeLock)
{
var newList = new List<AITarget>(_list) { target };
Interlocked.Exchange(ref _list, newList);
}
}
public bool Remove(AITarget target)
{
lock (_writeLock)
{
var newList = new List<AITarget>(_list);
bool removed = newList.Remove(target);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
public void Clear()
{
Interlocked.Exchange(ref _list, new List<AITarget>());
}
public bool Contains(AITarget target) => _list.Contains(target);
public AITarget this[int index] => _list[index];
public IEnumerator<AITarget> GetEnumerator() => _list.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
public List<AITarget> ToList() => new List<AITarget>(_list);
public AITarget FirstOrDefault(Func<AITarget, bool> predicate) => _list.FirstOrDefault(predicate);
public IEnumerable<AITarget> Where(Func<AITarget, bool> predicate) => _list.Where(predicate);
public bool Any(Func<AITarget, bool> predicate) => _list.Any(predicate);
}
partial class AITarget partial class AITarget
{ {
public static ThreadSafeAITargetList List = new ThreadSafeAITargetList(); public static List<AITarget> List = new List<AITarget>();
private Entity entity; private Entity entity;
public Entity Entity public Entity Entity
@@ -197,6 +197,18 @@ namespace Barotrauma
private readonly List<Body> myBodies; private readonly List<Body> myBodies;
#if SERVER
/// <summary>
/// How often the server can send messages about a limb targeting some attack target.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinSetAttackTargetEventInterval = 0.5;
private IDamageable lastDamageTarget;
private Limb lastTargetLimb;
private Limb lastAttackLimb;
private double lastSetAttackTargetEventTime;
#endif
public LatchOntoAI LatchOntoAI { get; private set; } public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; }
public PetBehavior PetBehavior { get; private set; } public PetBehavior PetBehavior { get; private set; }
@@ -2679,11 +2691,19 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning) if (!ActiveAttack.IsRunning)
{ {
#if SERVER #if SERVER
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
{
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb, AttackLimb,
damageTarget, damageTarget,
targetLimb, targetLimb,
SimPosition)); SimPosition));
lastSetAttackTargetEventTime = Timing.TotalTime;
lastDamageTarget = damageTarget;
lastAttackLimb = AttackLimb;
lastTargetLimb = targetLimb;
}
#else #else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif #endif
@@ -5,7 +5,6 @@ using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
@@ -47,7 +46,7 @@ namespace Barotrauma
private float respondToAttackTimer; private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f; private const float RespondToAttackInterval = 1.0f;
private bool wasConscious; private bool wasDead;
private bool freezeAI; private bool freezeAI;
@@ -202,7 +201,7 @@ namespace Barotrauma
} }
if (isIncapacitated) { return; } if (isIncapacitated) { return; }
wasConscious = true; wasDead = false;
respondToAttackTimer -= deltaTime; respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f) if (respondToAttackTimer <= 0.0f)
@@ -382,13 +381,8 @@ namespace Barotrauma
} }
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
// in case of somehow AnimController was a null, eg. something removed AnimController in the middle of an update
if (AnimController != null)
{
AnimController.Crouching = shouldCrouch; AnimController.Crouching = shouldCrouch;
CheckCrouching(deltaTime); CheckCrouching(deltaTime);
}
Character.ClearInputs(); Character.ClearInputs();
if (SortTimer > 0.0f) if (SortTimer > 0.0f)
@@ -1262,14 +1256,15 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult) public override void OnAttacked(Character attacker, AttackResult attackResult)
{ {
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs // If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f)) // (But if the character is already dead, and was dead before this attack, don't react)
if (Character.IsDead && wasDead) { return; }
if (Character.IsIncapacitated || Character.Stun > 0.0f)
{ {
RespondToAttack(attacker, attackResult); RespondToAttack(attacker, attackResult);
wasConscious = false; wasDead = Character.IsDead;
return; return;
} }
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer) if (attacker == null || Character.IsPlayer)
{ {
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them. // The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
@@ -1474,9 +1469,9 @@ namespace Barotrauma
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true); otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing) if (!isWitnessing)
{ {
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID) if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
{ {
// Dead or in different team -> cannot report. // Knocked down or in different team -> cannot report.
continue; continue;
} }
if (otherHumanAI.objectiveManager.HasOrders()) if (otherHumanAI.objectiveManager.HasOrders())
@@ -1500,6 +1495,14 @@ namespace Barotrauma
continue; continue;
} }
} }
else if (!otherCharacter.IsSecurity)
{
//witnessed the attack as non-security, trigger security
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID))
{
TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing));
}
}
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced); float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay); otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
} }
@@ -1643,8 +1646,7 @@ namespace Barotrauma
if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
if (!Character.IsBot) { return; } if (!Character.IsBot) { return; }
List<AIObjective> ObjectivesLocal = ObjectiveManager.Objectives; if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
if (ObjectivesLocal.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
{ {
// Don't replace offensive mode with something else // Don't replace offensive mode with something else
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; } if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
@@ -1824,9 +1826,7 @@ namespace Barotrauma
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true); => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
// ThreadLocal to ensure thread safety - each thread gets its own list instance private static List<Item> matchingItems = new List<Item>();
private static readonly ThreadLocal<List<Item>> matchingItemsLocal = new ThreadLocal<List<Item>>(() => new List<Item>());
private static List<Item> matchingItems => matchingItemsLocal.Value;
/// <summary> /// <summary>
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second. /// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
@@ -1834,16 +1834,15 @@ namespace Barotrauma
/// </summary> /// </summary>
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null) public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
{ {
var localMatchingItems = matchingItems; matchingItems.Clear();
localMatchingItems.Clear(); items = matchingItems;
items = localMatchingItems;
if (character?.Inventory == null) { return false; } if (character?.Inventory == null) { return false; }
character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) && matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
i.ConditionPercentage >= conditionPercentage && i.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(i)) && (!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, localMatchingItems); (predicate == null || predicate(i)), recursive, matchingItems);
items = localMatchingItems; items = matchingItems;
foreach (var item in localMatchingItems) foreach (var item in matchingItems)
{ {
if (item == null) { continue; } if (item == null) { continue; }
@@ -1936,12 +1935,12 @@ namespace Barotrauma
character.IsCriminal = true; character.IsCriminal = true;
character.IsActingOffensively = true; character.IsActingOffensively = true;
} }
if (!TriggerSecurity(otherHumanAI, combatMode)) if (!TriggerSecurity(otherHumanAI, character, combatMode))
{ {
// Else call the others // Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{ {
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode)) if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode))
{ {
// Only alert one guard at a time // Only alert one guard at a time
return; return;
@@ -1951,12 +1950,13 @@ namespace Barotrauma
} }
} }
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode) }
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
{ {
if (humanAI == null) { return false; } if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; } if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
onCompleted: () => onCompleted: () =>
{ {
//if the target is arrested successfully, reset the damage accumulator //if the target is arrested successfully, reset the damage accumulator
@@ -1964,13 +1964,12 @@ namespace Barotrauma
{ {
if (anyCharacter.AIController is HumanAIController anyAI) if (anyCharacter.AIController is HumanAIController anyAI)
{ {
anyAI.structureDamageAccumulator?.Remove(character); anyAI.structureDamageAccumulator?.Remove(targetCharacter);
} }
} }
}); });
return true; return true;
} }
}
public static void ItemTaken(Item item, Character thief) public static void ItemTaken(Item item, Character thief)
{ {
@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using Barotrauma.IO; using Barotrauma.IO;
using System.Linq; using System.Linq;
@@ -10,8 +9,7 @@ namespace Barotrauma
{ {
class NPCConversationCollection : Prefab class NPCConversationCollection : Prefab
{ {
// Thread-safe dictionary for language-based collections public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
public static readonly ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
public readonly LanguageIdentifier Language; public readonly LanguageIdentifier Language;
@@ -162,24 +160,7 @@ namespace Barotrauma
return currentFlags; return currentFlags;
} }
// Thread-safe previous conversations tracking using copy-on-write pattern private static readonly List<NPCConversation> previousConversations = new List<NPCConversation>();
private static volatile List<NPCConversation> _previousConversations = new List<NPCConversation>();
private static readonly object _previousConversationsLock = new object();
private static List<NPCConversation> previousConversations => _previousConversations;
private static void AddToPreviousConversations(NPCConversation conversation)
{
lock (_previousConversationsLock)
{
var newList = new List<NPCConversation>(_previousConversations);
newList.Insert(0, conversation);
if (newList.Count > MaxPreviousConversations)
{
newList.RemoveAt(MaxPreviousConversations);
}
_previousConversations = newList;
}
}
public static List<(Character speaker, string line)> CreateRandom(List<Character> availableSpeakers) public static List<(Character speaker, string line)> CreateRandom(List<Character> availableSpeakers)
{ {
@@ -300,7 +281,8 @@ namespace Barotrauma
if (baseConversation == null) if (baseConversation == null)
{ {
AddToPreviousConversations(selectedConversation); previousConversations.Insert(0, selectedConversation);
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
} }
lineList.Add((speaker, selectedConversation.Line)); lineList.Add((speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations); CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
@@ -119,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveState() protected override bool CheckObjectiveState()
{ {
if (item.IgnoreByAI(character) || Item.IsMarkedForDeconstruction(item)) if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
{ {
Abandon = true; Abandon = true;
} }
@@ -114,7 +114,7 @@ namespace Barotrauma
if (!allowUnloading) { return false; } if (!allowUnloading) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; } if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
} }
if (ignoreItemsMarkedForDeconstruction && Item.IsMarkedForDeconstruction(item)) { return false; } if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (!item.HasAccess(character)) { return false; } if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; } if (item.HasBallastFloraInHull) { return false; }
@@ -133,7 +133,7 @@ namespace Barotrauma
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher) if (operateExtinguisher)
{ {
character.CursorPosition = fs.Position; character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse) if (extinguisherItem.RequireAimToUse)
@@ -1,6 +1,5 @@
#nullable enable #nullable enable
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
@@ -69,9 +68,8 @@ namespace Barotrauma
/// <summary> /// <summary>
/// When did the character last inspect whether some other character has stolen items on them? /// When did the character last inspect whether some other character has stolen items on them?
/// Thread-safe dictionary for concurrent access.
/// </summary> /// </summary>
private static readonly ConcurrentDictionary<Character, double> lastInspectionTimes = new ConcurrentDictionary<Character, double>(); private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private const float NormalInspectionInterval = 120.0f; private const float NormalInspectionInterval = 120.0f;
private const float CriminalInspectionInterval = 30.0f; private const float CriminalInspectionInterval = 30.0f;
@@ -122,7 +122,7 @@ namespace Barotrauma
} }
else else
{ {
Objectives.RemoveAll(o => o?.GetType() == type); Objectives.RemoveAll(o => o.GetType() == type);
} }
Objectives.Add(objective); Objectives.Add(objective);
} }
@@ -173,6 +173,16 @@ namespace Barotrauma
if (character.CanInteractWith(Item, out _, checkLinked: false)) if (character.CanInteractWith(Item, out _, checkLinked: false))
{ {
waitTimer += deltaTime; waitTimer += deltaTime;
//if we're climbing upwards to the item, ensure the character stays within arm's length of it
//without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item,
//then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again
if (character.IsClimbing &&
Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
}
if (waitTimer < WaitTimeBeforeRepair) { return; } if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item); HumanAIController.FaceTarget(Item);
@@ -440,7 +440,7 @@ namespace Barotrauma
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct) if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
{ {
if (item.AllowDeconstruct && !Item.IsMarkedForDeconstruction(item) && if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
//only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that //only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that
(item.Prefab.DeconstructItems.None() || (item.Prefab.DeconstructItems.None() ||
item.Prefab.DeconstructItems.Any(deconstructItem => item.Prefab.DeconstructItems.Any(deconstructItem =>
@@ -454,7 +454,7 @@ namespace Barotrauma
} }
else if (Identifier == Tags.DontDeconstructThis) else if (Identifier == Tags.DontDeconstructThis)
{ {
if (Item.IsMarkedForDeconstruction(item)) { return true; } if (Item.DeconstructItems.Contains(item)) { return true; }
} }
ImmutableArray<Identifier> targetItems = GetTargetItems(option); ImmutableArray<Identifier> targetItems = GetTargetItems(option);
@@ -8,10 +8,8 @@ using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using JointParams = Barotrauma.RagdollParams.JointParams; using JointParams = Barotrauma.RagdollParams.JointParams;
using LimbParams = Barotrauma.RagdollParams.LimbParams; using LimbParams = Barotrauma.RagdollParams.LimbParams;
@@ -28,33 +26,7 @@ namespace Barotrauma
/// </summary> /// </summary>
const float MaxImpactDamage = 0.1f; const float MaxImpactDamage = 0.1f;
// Thread-safe list using copy-on-write pattern (ConcurrentBag doesn't support indexer/Remove) private static readonly List<Ragdoll> list = new List<Ragdoll>();
private static volatile List<Ragdoll> _list = new List<Ragdoll>();
private static readonly object _listLock = new object();
private static List<Ragdoll> list => _list;
private static void ListAdd(Ragdoll ragdoll)
{
lock (_listLock)
{
var newList = new List<Ragdoll>(_list) { ragdoll };
Interlocked.Exchange(ref _list, newList);
}
}
private static bool ListRemove(Ragdoll ragdoll)
{
lock (_listLock)
{
var newList = new List<Ragdoll>(_list);
bool removed = newList.Remove(ragdoll);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
struct Impact struct Impact
{ {
@@ -75,8 +47,7 @@ namespace Barotrauma
} }
} }
// Thread-safe queue for physics collision callbacks private readonly Queue<Impact> impactQueue = new Queue<Impact>();
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
protected Hull currentHull; protected Hull currentHull;
@@ -498,7 +469,7 @@ namespace Barotrauma
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null) public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
{ {
ListAdd(this); list.Add(this);
this.character = character; this.character = character;
Recreate(ragdollParams ?? RagdollParams); Recreate(ragdollParams ?? RagdollParams);
} }
@@ -774,9 +745,12 @@ namespace Barotrauma
if (f2.Body.UserData is not Structure structure) if (f2.Body.UserData is not Structure structure)
{ {
if (!f2.IsSensor) if (!f2.IsSensor)
{
lock (impactQueue)
{ {
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
} }
}
return true; return true;
} }
else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine) else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine)
@@ -847,7 +821,10 @@ namespace Barotrauma
} }
} }
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity)); impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
return true; return true;
} }
@@ -1314,9 +1291,10 @@ namespace Barotrauma
{ {
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; } if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
while (impactQueue.TryDequeue(out var impact)) while (impactQueue.Count > 0)
{ {
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity); var impact = impactQueue.Dequeue();
ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity);
} }
CheckValidity(); CheckValidity();
@@ -2390,7 +2368,7 @@ namespace Barotrauma
LimbJoints = null; LimbJoints = null;
} }
ListRemove(this); list.Remove(this);
} }
public static void RemoveAll() public static void RemoveAll()
@@ -758,6 +758,11 @@ namespace Barotrauma
public float CoolDownTimer { get; set; } public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; } public float CurrentRandomCoolDown { get; private set; }
public float SecondaryCoolDownTimer { get; set; } public float SecondaryCoolDownTimer { get; set; }
/// <summary>
/// The attack is considered to be running from the moment it starts until the <see cref="AttackTimer"/> reaches the <see cref="Duration"/> of the attack, or until the attack lands successfully.
/// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge.
/// </summary>
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public float AfterAttackTimer { get; set; } public float AfterAttackTimer { get; set; }
@@ -7,12 +7,10 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
#if SERVER #if SERVER
using System.Text; using System.Text;
@@ -30,70 +28,12 @@ namespace Barotrauma
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier); public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
/// <summary>
/// Thread-safe wrapper for character list operations.
/// Provides lock-free read operations and synchronized write operations.
/// </summary>
class ThreadSafeCharacterList : IEnumerable<Character>
{
private volatile List<Character> _list = new List<Character>();
private readonly object _writeLock = new object();
public int Count => _list.Count;
public void Add(Character character)
{
lock (_writeLock)
{
var newList = new List<Character>(_list) { character };
Interlocked.Exchange(ref _list, newList);
}
}
public bool Remove(Character character)
{
lock (_writeLock)
{
var newList = new List<Character>(_list);
bool removed = newList.Remove(character);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
public void Clear()
{
Interlocked.Exchange(ref _list, new List<Character>());
}
public bool Contains(Character character) => _list.Contains(character);
public Character this[int index] => _list[index];
public IEnumerator<Character> GetEnumerator() => _list.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
// LINQ-friendly snapshot for complex queries
public List<Character> ToList() => new List<Character>(_list);
public Character FirstOrDefault(Func<Character, bool> predicate) => _list.FirstOrDefault(predicate);
public Character Find(Predicate<Character> predicate) => _list.Find(predicate);
public List<Character> FindAll(Predicate<Character> predicate) => _list.FindAll(predicate);
public IEnumerable<Character> Where(Func<Character, bool> predicate) => _list.Where(predicate);
public bool Any(Func<Character, bool> predicate) => _list.Any(predicate);
public bool None(Func<Character, bool> predicate) => !_list.Any(predicate);
public int CountWhere(Func<Character, bool> predicate) => _list.Count(predicate);
}
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{ {
public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList(); public static readonly List<Character> CharacterList = new List<Character>();
public static int CharacterUpdateInterval = 1; public static int CharacterUpdateInterval = 1;
private static volatile int characterUpdateTick = 1; private static int characterUpdateTick = 1;
public const float MaxHighlightDistance = 150.0f; public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f; public const float MaxDragDistance = 200.0f;
@@ -1057,6 +997,9 @@ namespace Barotrauma
public bool IsForceRagdolled; public bool IsForceRagdolled;
public bool FollowCursor = true; public bool FollowCursor = true;
/// <summary>
/// Is the character currently dead, unconscious or paralyzed?
/// </summary>
public bool IsIncapacitated public bool IsIncapacitated
{ {
get get
@@ -1066,6 +1009,9 @@ namespace Barotrauma
} }
} }
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious public bool IsUnconscious
{ {
get { return CharacterHealth.IsUnconscious; } get { return CharacterHealth.IsUnconscious; }
@@ -1733,7 +1679,8 @@ namespace Barotrauma
AnimController.FindHull(setInWater: true); AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; } if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f); //mass < 35 = husk chimera is the largest vanilla monster that can be contained by default
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f);
CharacterList.Add(this); CharacterList.Add(this);
@@ -2322,8 +2269,11 @@ namespace Barotrauma
{ {
Vector2 targetMovement = GetTargetMovement(); Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement; AnimController.TargetMovement = targetMovement;
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
{
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
} }
}
if (AnimController is HumanoidAnimController humanAnimController) if (AnimController is HumanoidAnimController humanAnimController)
{ {
@@ -2853,11 +2803,10 @@ namespace Barotrauma
} }
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10; int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
int checkedItemCount = 0; int checkedItemCount = 0;
var cachedItems = Item.GetCachedItemList(); for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
for (int i = 0; i < itemsPerFrame && itemIndex < cachedItems.Count; i++, itemIndex++)
{ {
checkedItemCount++; checkedItemCount++;
var item = cachedItems[itemIndex]; var item = Item.ItemList[itemIndex];
if (!item.IsInteractable(this)) { continue; } if (!item.IsInteractable(this)) { continue; }
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; } if (item.Submarine == null) { continue; }
@@ -2893,10 +2842,10 @@ namespace Barotrauma
} }
} }
targetItem = _foundItem; targetItem = _foundItem;
bool completed = itemIndex >= cachedItems.Count - 1; bool completed = itemIndex >= Item.ItemList.Count - 1;
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1) if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
{ {
var msg = $"Went through {checkedItemCount} of total {cachedItems.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}"; var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
if (StopWatch.ElapsedMilliseconds > 5) if (StopWatch.ElapsedMilliseconds > 5)
{ {
DebugConsole.ThrowError(msg); DebugConsole.ThrowError(msg);
@@ -3569,9 +3518,11 @@ namespace Barotrauma
UpdateAttackers(deltaTime); UpdateAttackers(deltaTime);
foreach (var characterTalent in characterTalents) //use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328)
//this way we'll just add them to the end of the list without causing a collection was modified exception
for (int i = 0; i < characterTalents.Count; i++)
{ {
characterTalent.UpdateTalent(deltaTime); characterTalents[i].UpdateTalent(deltaTime);
} }
if (IsDead) { return; } if (IsDead) { return; }
@@ -4971,11 +4922,7 @@ namespace Barotrauma
HealthUpdateInterval = 0.0f; HealthUpdateInterval = 0.0f;
} }
// Thread-static to avoid concurrent modification in parallel item updates private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime) public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{ {
if (actionType == ActionType.OnEating) if (actionType == ActionType.OnEating)
@@ -5004,7 +4951,6 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{ {
var targets = StatusEffectTargets;
targets.Clear(); targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets); statusEffect.Apply(actionType, deltaTime, this, targets);
@@ -5867,6 +5813,12 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier); return info.UnlockedTalents.Contains(identifier);
} }
public bool IsTalentLocked(Identifier talentIdentifier)
{
if (info == null) { return true; }
return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1;
}
public bool HasUnlockedAllTalents() public bool HasUnlockedAllTalents()
{ {
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -8,14 +8,12 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using MoonSharp.Interpreter; using static OneOf.Types.TrueFalseOrNull;
namespace Barotrauma namespace Barotrauma
{ {
@@ -136,9 +134,8 @@ namespace Barotrauma
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>(); private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
// Thread-safe afflictions dictionary for concurrent access private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
private readonly ConcurrentDictionary<Affliction, LimbHealth> afflictions = new ConcurrentDictionary<Affliction, LimbHealth>(); private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
private readonly ConcurrentDictionary<Affliction, byte> irremovableAfflictions = new ConcurrentDictionary<Affliction, byte>();
private Affliction bloodlossAffliction; private Affliction bloodlossAffliction;
private Affliction oxygenLowAffliction; private Affliction oxygenLowAffliction;
private Affliction pressureAffliction; private Affliction pressureAffliction;
@@ -329,13 +326,13 @@ namespace Barotrauma
private void InitIrremovableAfflictions() private void InitIrremovableAfflictions()
{ {
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0); irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0); irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0); irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0); irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
foreach (Affliction affliction in irremovableAfflictions.Keys) foreach (Affliction affliction in irremovableAfflictions)
{ {
afflictions.TryAdd(affliction, null); afflictions.Add(affliction, null);
} }
} }
@@ -343,7 +340,7 @@ namespace Barotrauma
public IReadOnlyCollection<Affliction> GetAllAfflictions() public IReadOnlyCollection<Affliction> GetAllAfflictions()
{ {
return afflictions.Keys.ToList(); return afflictions.Keys;
} }
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter) public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
@@ -508,7 +505,7 @@ namespace Barotrauma
/// </summary> /// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType) public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{ {
// ConcurrentDictionary is thread-safe, no lock needed lock (afflictions) {
// This is a % resistance (0 to 1.0) // This is a % resistance (0 to 1.0)
float resistance = 0.0f; float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -521,6 +518,7 @@ namespace Barotrauma
// The returned value is calculated to be a % resistance again // The returned value is calculated to be a % resistance again
return 1 - ((1 - resistance) * abilityResistanceMultiplier); return 1 - ((1 - resistance) * abilityResistanceMultiplier);
} }
}
public float GetStatValue(StatTypes statType) public float GetStatValue(StatTypes statType)
{ {
@@ -543,25 +541,20 @@ namespace Barotrauma
return false; return false;
} }
// Thread-static to avoid concurrent modification in parallel item updates private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
[ThreadStatic]
private static List<Affliction> t_matchingAfflictions;
private static List<Affliction> MatchingAfflictions => t_matchingAfflictions ??= new List<Affliction>();
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null) public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{ {
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear(); matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys); matchingAfflictions.AddRange(afflictions.Keys);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction); ReduceMatchingAfflictions(amount, treatmentAction);
} }
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{ {
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear(); matchingAfflictions.Clear();
foreach (var affliction in afflictions) foreach (var affliction in afflictions)
{ {
@@ -571,7 +564,7 @@ namespace Barotrauma
} }
} }
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker); ReduceMatchingAfflictions(amount, treatmentAction, attacker);
} }
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb) private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
@@ -581,11 +574,10 @@ namespace Barotrauma
{ {
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear(); matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction); ReduceMatchingAfflictions(amount, treatmentAction);
} }
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
@@ -593,7 +585,6 @@ namespace Barotrauma
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear(); matchingAfflictions.Clear();
var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions) foreach (var affliction in afflictions)
@@ -604,10 +595,10 @@ namespace Barotrauma
matchingAfflictions.Add(affliction.Key); matchingAfflictions.Add(affliction.Key);
} }
} }
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker); ReduceMatchingAfflictions(amount, treatmentAction, attacker);
} }
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null) private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
{ {
if (matchingAfflictions.Count == 0) { return; } if (matchingAfflictions.Count == 0) { return; }
@@ -695,19 +686,12 @@ namespace Barotrauma
} }
} }
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
[ThreadStatic] private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
private static List<Affliction> t_afflictionsToRemove;
[ThreadStatic]
private static List<KeyValuePair<Affliction, LimbHealth>> t_afflictionsToUpdate;
private static List<Affliction> AfflictionsToRemove => t_afflictionsToRemove ??= new List<Affliction>();
private static List<KeyValuePair<Affliction, LimbHealth>> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{ {
if (Unkillable || Character.GodMode) { return; } if (Unkillable || Character.GodMode) { return; }
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear(); afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType || a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
@@ -715,14 +699,14 @@ namespace Barotrauma
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType)); a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
foreach (var affliction in afflictionsToRemove) foreach (var affliction in afflictionsToRemove)
{ {
afflictions.TryRemove(affliction, out _); afflictions.Remove(affliction);
} }
foreach (LimbHealth limbHealth in limbHealths) foreach (LimbHealth limbHealth in limbHealths)
{ {
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); } if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); } if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); } if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
} }
RecalculateVitality(); RecalculateVitality();
@@ -758,28 +742,26 @@ namespace Barotrauma
public void RemoveAfflictions(Func<Affliction, bool> predicate) public void RemoveAfflictions(Func<Affliction, bool> predicate)
{ {
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear(); afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction))); afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove) foreach (var affliction in afflictionsToRemove)
{ {
afflictions.TryRemove(affliction, out _); afflictions.Remove(affliction);
} }
CalculateVitality(); CalculateVitality();
} }
public void RemoveAllAfflictions() public void RemoveAllAfflictions()
{ {
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear(); afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a))); afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
foreach (var affliction in afflictionsToRemove) foreach (var affliction in afflictionsToRemove)
{ {
//set strength to 0 in case the affliction needs to react to becoming inactive //set strength to 0 in case the affliction needs to react to becoming inactive
affliction.Strength = 0.0f; affliction.Strength = 0.0f;
afflictions.TryRemove(affliction, out _); afflictions.Remove(affliction);
} }
foreach (Affliction affliction in irremovableAfflictions.Keys) foreach (Affliction affliction in irremovableAfflictions)
{ {
affliction.Strength = 0.0f; affliction.Strength = 0.0f;
} }
@@ -788,18 +770,17 @@ namespace Barotrauma
public void RemoveNegativeAfflictions() public void RemoveNegativeAfflictions()
{ {
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear(); afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
!irremovableAfflictions.ContainsKey(a) && !irremovableAfflictions.Contains(a) &&
!a.Prefab.IsBuff && !a.Prefab.IsBuff &&
a.Prefab.AfflictionType != "geneticmaterialbuff" && a.Prefab.AfflictionType != "geneticmaterialbuff" &&
a.Prefab.AfflictionType != "geneticmaterialdebuff")); a.Prefab.AfflictionType != "geneticmaterialdebuff"));
foreach (var affliction in afflictionsToRemove) foreach (var affliction in afflictionsToRemove)
{ {
afflictions.TryRemove(affliction, out _); afflictions.Remove(affliction);
} }
foreach (Affliction affliction in irremovableAfflictions.Keys) foreach (Affliction affliction in irremovableAfflictions)
{ {
affliction.Strength = 0.0f; affliction.Strength = 0.0f;
} }
@@ -902,7 +883,7 @@ namespace Barotrauma
var copyAffliction = newAffliction.Prefab.Instantiate( var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength), Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
newAffliction.Source); newAffliction.Source);
afflictions.TryAdd(copyAffliction, limbHealth); afflictions.Add(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character); AchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character); MedicalClinic.OnAfflictionCountChanged(Character);
@@ -939,8 +920,6 @@ namespace Barotrauma
if (!Character.GodMode) if (!Character.GodMode)
{ {
var afflictionsToRemove = AfflictionsToRemove;
var afflictionsToUpdate = AfflictionsToUpdate;
afflictionsToRemove.Clear(); afflictionsToRemove.Clear();
afflictionsToUpdate.Clear(); afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -949,7 +928,7 @@ namespace Barotrauma
if (affliction.Strength <= 0.0f) if (affliction.Strength <= 0.0f)
{ {
AchievementManager.OnAfflictionRemoved(affliction, Character); AchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); } if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue; continue;
} }
if (affliction.Prefab.Duration > 0.0f) if (affliction.Prefab.Duration > 0.0f)
@@ -987,7 +966,7 @@ namespace Barotrauma
foreach (var affliction in afflictionsToRemove) foreach (var affliction in afflictionsToRemove)
{ {
afflictions.TryRemove(affliction, out _); afflictions.Remove(affliction);
} }
if (afflictionsToRemove.Count is not 0) if (afflictionsToRemove.Count is not 0)
@@ -1235,14 +1214,9 @@ namespace Barotrauma
return (causeOfDeath, strongestAffliction); return (causeOfDeath, strongestAffliction);
} }
// Thread-static to avoid concurrent modification in parallel item updates private readonly List<Affliction> allAfflictions = new List<Affliction>();
[ThreadStatic]
private static List<Affliction> t_allAfflictions;
private static List<Affliction> AllAfflictionsList => t_allAfflictions ??= new List<Affliction>();
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null) private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{ {
var allAfflictions = AllAfflictionsList;
allAfflictions.Clear(); allAfflictions.Clear();
if (!mergeSameAfflictions) if (!mergeSameAfflictions)
{ {
@@ -1425,17 +1399,10 @@ namespace Barotrauma
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength); return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
} }
// Thread-static to avoid concurrent modification in parallel updates private readonly List<Affliction> activeAfflictions = new List<Affliction>();
[ThreadStatic] private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
private static List<Affliction> t_activeAfflictions;
[ThreadStatic]
private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions;
private static List<Affliction> ActiveAfflictionsList => t_activeAfflictions ??= new List<Affliction>();
private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>();
public void ServerWrite(IWriteMessage msg) public void ServerWrite(IWriteMessage msg)
{ {
var activeAfflictions = ActiveAfflictionsList;
activeAfflictions.Clear(); activeAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{ {
@@ -1461,7 +1428,6 @@ namespace Barotrauma
} }
} }
var limbAfflictions = LimbAfflictionsList;
limbAfflictions.Clear(); limbAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{ {
@@ -1491,9 +1457,8 @@ namespace Barotrauma
public void Remove() public void Remove()
{ {
RemoveProjSpecific(); RemoveProjSpecific();
// Clear thread-static lists to help with garbage collection afflictionsToRemove.Clear();
AfflictionsToRemove.Clear(); afflictionsToUpdate.Clear();
AfflictionsToUpdate.Clear();
} }
partial void RemoveProjSpecific(); partial void RemoveProjSpecific();
@@ -1568,14 +1533,14 @@ namespace Barotrauma
} }
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; } if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f); float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab); var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null) if (irremovableAffliction != null)
{ {
irremovableAffliction.Strength = strength; irremovableAffliction.Strength = strength;
} }
else else
{ {
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth); afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
} }
} }
} }
@@ -797,14 +797,16 @@ namespace Barotrauma
return AddDamage(simPosition, afflictions, playSound); return AddDamage(simPosition, afflictions, playSound);
} }
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null) public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
{ {
var appliedDamageModifiers = new List<DamageModifier>(); appliedDamageModifiers.Clear();
var afflictionsCopy = new List<Affliction>(); afflictionsCopy.Clear();
foreach (var affliction in afflictions) foreach (var affliction in afflictions)
{ {
var tempModifiers = new List<DamageModifier>(); tempModifiers.Clear();
var newAffliction = affliction; var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced); float random = Rand.Value(Rand.RandSync.Unsynced);
bool foundMatchingModifier = false; bool foundMatchingModifier = false;
@@ -1020,18 +1022,22 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime); partial void UpdateProjSpecific(float deltaTime);
// Thread-static to avoid concurrent modification in parallel item updates #if SERVER
[ThreadStatic] /// <summary>
private static List<Body> t_contactBodies; /// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created.
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>(); /// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinExecuteAttackEventInterval = 0.5f;
private double lastExecuteAttackEventTime;
#endif
private readonly List<Body> contactBodies = new List<Body>();
/// <summary> /// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary> /// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{ {
attackResult = default; attackResult = default;
var contactBodies = ContactBodies;
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning; bool wasRunning = attack.IsRunning;
@@ -1145,9 +1151,13 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult); ExecuteAttack(damageTarget, targetLimb, out attackResult);
} }
#if SERVER #if SERVER
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData( GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb, attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos)); targetSimPos: attackSimPos));
lastExecuteAttackEventTime = Timing.TotalTime;
}
#endif #endif
} }
@@ -1290,11 +1300,7 @@ namespace Barotrauma
} }
} }
// Thread-static to avoid concurrent modification in parallel item updates private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime) public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{ {
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
@@ -1317,7 +1323,6 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{ {
var targets = StatusEffectTargets;
targets.Clear(); targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets); statusEffect.Apply(actionType, deltaTime, character, targets);
@@ -1,12 +1,10 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Barotrauma.IO; using Barotrauma.IO;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
@@ -119,9 +117,8 @@ namespace Barotrauma
public virtual AnimationType AnimationType { get; protected set; } public virtual AnimationType AnimationType { get; protected set; }
/// <summary> /// <summary>
/// The cached animations of all the characters that have been loaded. /// The cached animations of all the characters that have been loaded.
/// Thread-safe cache using ConcurrentDictionary.
/// </summary> /// </summary>
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>> allAnimations = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>>(); private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
[Header("Movement")] [Header("Movement")]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
@@ -247,9 +244,7 @@ namespace Barotrauma
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors); return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
} }
// ThreadLocal for thread-safe error message collection during animation loading private static readonly List<string> errorMessages = new List<string>();
private static readonly ThreadLocal<List<string>> errorMessagesLocal = new ThreadLocal<List<string>>(() => new List<string>());
private static List<string> errorMessages => errorMessagesLocal.Value;
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new() private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
{ {
@@ -267,7 +262,11 @@ namespace Barotrauma
} }
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage; ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
Debug.Assert(contentPackage != null); Debug.Assert(contentPackage != null);
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>()); if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
{
animations = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, animations);
}
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType); string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType) if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
{ {
@@ -419,12 +418,16 @@ namespace Barotrauma
{ {
throw new Exception("Cannot create an animation file of type " + animationType); throw new Exception("Cannot create an animation file of type " + animationType);
} }
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>()); if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath); string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName)) if (anims.ContainsKey(fileName))
{ {
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red); DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
anims.TryRemove(fileName, out _); anims.Remove(fileName);
} }
var instance = new T(); var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString())); XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
@@ -436,7 +439,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(animationElement); instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save(); instance.Save();
instance.Load(contentPath, speciesName); instance.Load(contentPath, speciesName);
anims.TryAdd(fileName, instance); anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite); DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance; return instance;
} }
@@ -464,14 +467,17 @@ namespace Barotrauma
{ {
// Update the key by removing and re-adding the animation. // Update the key by removing and re-adding the animation.
string fileName = FileNameWithoutExtension; string fileName = FileNameWithoutExtension;
if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary<string, AnimationParams> animations)) if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
{ {
animations.TryRemove(fileName, out _); animations.Remove(fileName);
} }
base.UpdatePath(newPath); base.UpdatePath(newPath);
if (animations != null) if (animations != null)
{ {
animations.TryAdd(fileName, this); if (!animations.ContainsKey(fileName))
{
animations.Add(fileName, this);
}
} }
} }
} }
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Xml.Linq; using System.Xml.Linq;
@@ -125,9 +124,8 @@ namespace Barotrauma
/// key1: Species name /// key1: Species name
/// key2: File path /// key2: File path
/// value: Ragdoll parameters /// value: Ragdoll parameters
/// Thread-safe cache using ConcurrentDictionary.
/// </summary> /// </summary>
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>> allRagdolls = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>>(); private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>(); public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>(); public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -224,7 +222,11 @@ namespace Barotrauma
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace()); Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
} }
Debug.Assert(contentPackage != null); Debug.Assert(contentPackage != null);
var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, RagdollParams>()); if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies); string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll)) if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
{ {
@@ -329,10 +331,10 @@ namespace Barotrauma
if (allRagdolls.ContainsKey(speciesName)) if (allRagdolls.ContainsKey(speciesName))
{ {
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red); DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
allRagdolls.TryRemove(speciesName, out _); allRagdolls.Remove(speciesName);
} }
var ragdolls = new ConcurrentDictionary<string, RagdollParams>(); var ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.TryAdd(speciesName, ragdolls); allRagdolls.Add(speciesName, ragdolls);
var instance = new T var instance = new T
{ {
doc = new XDocument(mainElement) doc = new XDocument(mainElement)
@@ -343,7 +345,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(mainElement); instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save(); instance.Save();
instance.Load(contentPath, speciesName); instance.Load(contentPath, speciesName);
ragdolls.TryAdd(instance.FileNameWithoutExtension, instance); ragdolls.Add(instance.FileNameWithoutExtension, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance; return instance;
} }
@@ -360,14 +362,17 @@ namespace Barotrauma
{ {
// Update the key by removing and re-adding the ragdoll. // Update the key by removing and re-adding the ragdoll.
string fileName = FileNameWithoutExtension; string fileName = FileNameWithoutExtension;
if (allRagdolls.TryGetValue(SpeciesName, out ConcurrentDictionary<string, RagdollParams> ragdolls)) if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
{ {
ragdolls.TryRemove(fileName, out _); ragdolls.Remove(fileName);
} }
base.UpdatePath(fullPath); base.UpdatePath(fullPath);
if (ragdolls != null) if (ragdolls != null)
{ {
ragdolls.TryAdd(fileName, this); if (!ragdolls.ContainsKey(fileName))
{
ragdolls.Add(fileName, this);
}
} }
} }
} }
@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
{ {
foreach (Identifier identifier in option.TalentIdentifiers) foreach (Identifier identifier in option.TalentIdentifiers)
{ {
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; } if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
identifiers.Add(identifier); identifiers.Add(identifier);
} }
foreach (var (_, value) in option.ShowCaseTalents) foreach (var (_, value) in option.ShowCaseTalents)
{ {
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet(); var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
if (ids.Count is 0) { continue; } if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced()); identifiers.Add(value.GetRandomUnsynced());
@@ -1,8 +1,6 @@
using Barotrauma.Abilities; using Barotrauma.Abilities;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
@@ -74,9 +72,7 @@ namespace Barotrauma
} }
} }
// ThreadLocal for thread-safe talent checking private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
private static readonly ThreadLocal<HashSet<Identifier>> checkedNonStackableTalentsLocal = new ThreadLocal<HashSet<Identifier>>(() => new HashSet<Identifier>());
private static HashSet<Identifier> checkedNonStackableTalents => checkedNonStackableTalentsLocal.Value;
/// <summary> /// <summary>
/// Checks talents for a given AbilityObject taking into account non-stackable talents. /// Checks talents for a given AbilityObject taking into account non-stackable talents.
@@ -131,7 +131,7 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; } if (character.IsTalentLocked(talentIdentifier)) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier)) if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{ {
@@ -163,16 +163,6 @@ namespace Barotrauma
return false; return false;
} }
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
{
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents) public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{ {
List<Identifier> viableTalents = new List<Identifier>(); List<Identifier> viableTalents = new List<Identifier>();
@@ -1,4 +1,4 @@
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -21,7 +21,7 @@ namespace Barotrauma
var npcConversationCollection = new NPCConversationCollection(this, mainElement); var npcConversationCollection = new NPCConversationCollection(this, mainElement);
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language)) if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
{ {
NPCConversationCollection.Collections.TryAdd(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>()); NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
} }
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding); NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
} }
@@ -252,7 +252,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
})); }));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)] [name]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() => () =>
{ {
string[] creatureAndJobNames = string[] creatureAndJobNames =
@@ -271,7 +271,7 @@ namespace Barotrauma
}; };
}, isCheat: true)); }, isCheat: true));
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character", commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character",
(string[] args) => (string[] args) =>
{ {
if (Character.Controlled == null) if (Character.Controlled == null)
@@ -294,7 +294,10 @@ namespace Barotrauma
{ {
return new string[][] return new string[][]
{ {
GetItemNameOrIdParams().ToArray() GetItemNameOrIdParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
}; };
}, isCheat: true)); }, isCheat: true));
@@ -311,7 +314,7 @@ namespace Barotrauma
}; };
}, isCheat: true)); }, isCheat: true));
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".", commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition] [quality]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
(string[] args) => (string[] args) =>
{ {
TrySpawnItem(args); TrySpawnItem(args);
@@ -321,7 +324,10 @@ namespace Barotrauma
return new string[][] return new string[][]
{ {
GetItemNameOrIdParams().ToArray(), GetItemNameOrIdParams().ToArray(),
GetSpawnPosParams().ToArray() GetSpawnPosParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
}; };
}, isCheat: true)); }, isCheat: true));
@@ -1324,6 +1330,7 @@ namespace Barotrauma
} }
else else
{ {
if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); }
NewMessage("Level seed: " + Level.Loaded.Seed); NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
@@ -1333,17 +1340,29 @@ namespace Barotrauma
} }
},null)); },null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor] [submarine_team]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
onExecute:(string[] args) => onExecute:(string[] args) =>
{ {
if (Submarine.MainSub == null) { return; } if (Submarine.MainSub == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{ {
#if SERVER #if SERVER
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client."); ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
#else #else
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
#endif #endif
} }
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
@@ -1356,9 +1375,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.StartPosition; Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null) if (Level.Loaded.StartOutpost != null)
{ {
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2; pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
} }
Submarine.MainSub.SetPosition(pos); submarineToTeleport.SetPosition(pos);
} }
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{ {
@@ -1370,9 +1389,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.EndPosition; Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null) if (Level.Loaded.EndOutpost != null)
{ {
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2; pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
} }
Submarine.MainSub.SetPosition(pos); submarineToTeleport.SetPosition(pos);
} }
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{ {
@@ -1381,8 +1400,8 @@ namespace Barotrauma
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return; return;
} }
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null) if (submarineDockingPort != null && outpostDockingPort != null)
{ {
@@ -1394,7 +1413,8 @@ namespace Barotrauma
{ {
return new string[][] return new string[][]
{ {
new string[] { "start", "end", "endoutpost", "cursor" } new string[] { "start", "end", "endoutpost", "cursor" },
ListAvailableSubmarines()
}; };
}, isCheat: true)); }, isCheat: true));
@@ -1478,7 +1498,7 @@ namespace Barotrauma
newItemName = args[2]; newItemName = args[2];
} }
var oldItem = Item.ItemList.Where(it => it.Name == args[0]).ElementAtOrDefault(itemIndex); var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
if (oldItem == null) if (oldItem == null)
{ {
ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex})."); ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex}).");
@@ -1852,7 +1872,7 @@ namespace Barotrauma
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) => commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
{ {
Item reactorItem = Item.ItemList.FirstOrDefault(i => i.GetComponent<Reactor>() != null); Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
if (reactorItem == null) { return; } if (reactorItem == null) { return; }
var reactor = reactorItem.GetComponent<Reactor>(); var reactor = reactorItem.GetComponent<Reactor>();
@@ -2570,6 +2590,16 @@ namespace Barotrauma
return locationNames.ToArray(); return locationNames.ToArray();
} }
private static string[] ListAvailableSubmarines()
{
List<string> submarineNames = new();
foreach (var submarine in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
submarineNames.Add(submarine.Info.Name + "_" + submarine.TeamID);
}
return submarineNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition) private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{ {
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase)) if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
@@ -2952,7 +2982,7 @@ namespace Barotrauma
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName; isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
} }
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew); ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter);
if (usePreConfiguredNPC) if (usePreConfiguredNPC)
{ {
@@ -2983,6 +3013,14 @@ namespace Barotrauma
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter => Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter =>
{ {
if (renameCharacter != null)
{
if (renameCharacter.Length > 31)
{
renameCharacter = renameCharacter.Substring(0, 32);
}
newCharacter.Info.Name = renameCharacter;
}
SetTeamAndCrew(newCharacter); SetTeamAndCrew(newCharacter);
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint); newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
newCharacter.GiveIdCardTags(spawnPoint); newCharacter.GiveIdCardTags(spawnPoint);
@@ -3010,7 +3048,7 @@ namespace Barotrauma
} }
} }
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew) void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter)
{ {
spawnPosition = Vector2.Zero; spawnPosition = Vector2.Zero;
spawnPoint = null; spawnPoint = null;
@@ -3096,6 +3134,12 @@ namespace Barotrauma
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}."); ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
} }
} }
argIndex++;
renameCharacter = null;
if (args.Length > argIndex)
{
renameCharacter = args[argIndex];
}
} }
} }
@@ -3103,6 +3147,7 @@ namespace Barotrauma
{ {
yield return "cursor"; yield return "cursor";
yield return "inventory"; yield return "inventory";
yield return "cargo";
#if SERVER #if SERVER
if (GameMain.Server != null) if (GameMain.Server != null)
@@ -3143,6 +3188,8 @@ namespace Barotrauma
} }
} }
private static ImmutableArray<string> ItemQualityNames = ["normal", "good", "excellent", "masterwork"];
private static void TrySpawnItem(string[] args) private static void TrySpawnItem(string[] args)
{ {
try try
@@ -3203,6 +3250,7 @@ namespace Barotrauma
int amount = 1; int amount = 1;
int conditionPrc = 100; int conditionPrc = 100;
int itemQuality = 0;
if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex)) if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex))
{ {
@@ -3223,19 +3271,30 @@ namespace Barotrauma
break; break;
default: default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray()); var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } if (matchingCharacter != null) { spawnInventory = matchingCharacter.Inventory; }
break; break;
} }
if (args.Length > spawnLocationIndex + 1) if (args.Length > spawnLocationIndex + 1)
{ {
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100000); amount = Math.Min(amount, 100);
} }
if (args.Length > spawnLocationIndex + 2) if (args.Length > spawnLocationIndex + 2)
{ {
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; } if (!int.TryParse(args[spawnLocationIndex + 2], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
}
if (args.Length > spawnLocationIndex + 3)
{
for (int i = 0; i <= Quality.MaxQuality; i++)
{
if (args[spawnLocationIndex + 3].ToLowerInvariant() == ItemQualityNames[i])
{
itemQuality = i;
}
}
} }
} }
@@ -3257,7 +3316,7 @@ namespace Barotrauma
} }
else else
{ {
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition); Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality);
} }
} }
else if (spawnInventory != null) else if (spawnInventory != null)
@@ -3284,6 +3343,7 @@ namespace Barotrauma
} }
item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f); item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f);
item.Quality = itemQuality;
} }
} }
} }
@@ -31,12 +31,17 @@ namespace Barotrauma
Actions = new List<EventAction>(); Actions = new List<EventAction>();
foreach (var e in element.Elements()) foreach (var e in element.Elements())
{ {
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) if (e.NameAsIdentifier().Equals("statuseffect"))
{ {
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.", DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: element.ContentPackage); contentPackage: element.ContentPackage);
continue; continue;
} }
else if (e.NameAsIdentifier().Equals(nameof(OnRoundEndAction)))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". {nameof(OnRoundEndAction)} configured as a sub action. Please configure it as an action at the end of the event.",
contentPackage: element.ContentPackage);
}
var action = Instantiate(scriptedEvent, e); var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); } if (action != null) { Actions.Add(action); }
} }
@@ -123,7 +123,7 @@ namespace Barotrauma
AddTargetPredicate( AddTargetPredicate(
Tags.Traitor, Tags.Traitor,
ScriptedEvent.TargetPredicate.EntityType.Character, ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c));
} }
private void TagNonTraitors() private void TagNonTraitors()
@@ -131,7 +131,7 @@ namespace Barotrauma
AddTargetPredicate( AddTargetPredicate(
Tags.NonTraitor, Tags.NonTraitor,
ScriptedEvent.TargetPredicate.EntityType.Character, ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
} }
private void TagNonTraitorPlayers() private void TagNonTraitorPlayers()
@@ -139,7 +139,7 @@ namespace Barotrauma
AddTargetPredicate( AddTargetPredicate(
Tags.NonTraitorPlayer, Tags.NonTraitorPlayer,
ScriptedEvent.TargetPredicate.EntityType.Character, ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
} }
private void TagBots(bool playerCrewOnly) private void TagBots(bool playerCrewOnly)
@@ -151,7 +151,8 @@ namespace Barotrauma
e is Character c && e is Character c &&
c.IsBot && c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) &&
CharacterTeamMatches(c));
} }
private void TagCrew() private void TagCrew()
@@ -171,7 +172,7 @@ namespace Barotrauma
private void TagHumansByTag(Identifier tag) private void TagHumansByTag(Identifier tag)
{ {
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag))); AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c)));
} }
private void TagHumansByJobIdentifier(Identifier jobIdentifier) private void TagHumansByJobIdentifier(Identifier jobIdentifier)
@@ -1,8 +1,7 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Immutable; using System.Collections.Generic;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
@@ -11,22 +10,11 @@ namespace Barotrauma
/// </summary> /// </summary>
class UnlockPathAction : EventAction class UnlockPathAction : EventAction
{ {
private static volatile ImmutableHashSet<LocationConnection> _pathsUnlockedThisRound = private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
ImmutableHashSet<LocationConnection>.Empty;
public static void ResetPathsUnlockedThisRound() public static void ResetPathsUnlockedThisRound()
{ {
_pathsUnlockedThisRound = ImmutableHashSet<LocationConnection>.Empty; pathsUnlockedThisRound.Clear();
}
private static void AddUnlockedPath(LocationConnection connection)
{
ImmutableHashSet<LocationConnection> original, updated;
do
{
original = _pathsUnlockedThisRound;
updated = original.Add(connection);
} while (Interlocked.CompareExchange(ref _pathsUnlockedThisRound, updated, original) != original);
} }
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -52,7 +40,7 @@ namespace Barotrauma
{ {
if (!connection.Locked) { continue; } if (!connection.Locked) { continue; }
connection.Locked = false; connection.Locked = false;
AddUnlockedPath(connection); pathsUnlockedThisRound.Add(connection);
#if SERVER #if SERVER
NotifyUnlock(connection); NotifyUnlock(connection);
#else #else
@@ -73,7 +61,7 @@ namespace Barotrauma
#if SERVER #if SERVER
public static void NotifyPathsUnlockedThisRound(Client client) public static void NotifyPathsUnlockedThisRound(Client client)
{ {
foreach (LocationConnection connection in _pathsUnlockedThisRound) foreach (LocationConnection connection in pathsUnlockedThisRound)
{ {
NotifyUnlock(connection, client); NotifyUnlock(connection, client);
} }
@@ -3,11 +3,8 @@ using Barotrauma.Items.Components;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
@@ -46,7 +43,7 @@ namespace Barotrauma
private Level level; private Level level;
private volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty; private readonly List<Sprite> preloadedSprites = new List<Sprite>();
//The "intensity" of the current situation (a value between 0.0 - 1.0). //The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on. //High when a disaster has struck, low when nothing special is going on.
@@ -86,18 +83,14 @@ namespace Barotrauma
private float crewAwayResetTimer; private float crewAwayResetTimer;
private float crewAwayDuration; private float crewAwayDuration;
// volatile + ImmutableCollections private readonly List<EventSet> pendingEventSets = new List<EventSet>();
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents = private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty; private readonly List<Event> activeEvents = new List<Event>();
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty; private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty; private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
#if DEBUG && SERVER #if DEBUG && SERVER
@@ -119,10 +112,10 @@ namespace Barotrauma
public IEnumerable<Event> ActiveEvents public IEnumerable<Event> ActiveEvents
{ {
get { return _activeEvents; } get { return activeEvents; }
} }
public readonly ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<Event>(); public readonly Queue<Event> QueuedEvents = new Queue<Event>();
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>(); public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
@@ -138,8 +131,8 @@ namespace Barotrauma
} }
} }
private volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty; private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e))); public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
public readonly EventLog EventLog = new EventLog(); public readonly EventLog EventLog = new EventLog();
@@ -150,72 +143,6 @@ namespace Barotrauma
public bool Enabled = true; public bool Enabled = true;
private static T AtomicUpdate<T>(ref T location, Func<T, T> updateFunc) where T : class
{
T original, updated;
do
{
original = Volatile.Read(ref location);
updated = updateFunc(original);
} while (Interlocked.CompareExchange(ref location, updated, original) != original);
return updated;
}
// activeEvents
private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev));
private void ClearActiveEvents() => _activeEvents = ImmutableList<Event>.Empty;
// pendingEventSets
private void AddPendingEventSet(EventSet eventSet) =>
AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet));
private void RemovePendingEventSetAt(int index) =>
AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list);
private void ClearPendingEventSets() => _pendingEventSets = ImmutableList<EventSet>.Empty;
// selectedEvents
private void AddSelectedEvent(EventSet eventSet, Event ev) =>
AtomicUpdate(ref _selectedEvents, dict =>
{
var currentList = dict.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
return dict.SetItem(eventSet, currentList.Add(ev));
});
private void RemoveSelectedEventSet(EventSet eventSet) =>
AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet));
private void ClearSelectedEvents() =>
_selectedEvents = ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private ImmutableList<Event> GetSelectedEvents(EventSet eventSet) =>
_selectedEvents.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet);
// finishedEvents
private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev));
private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet<Event>.Empty;
private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev);
// nonRepeatableEvents
private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id));
private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
// preloadedSprites
private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite));
private void ClearPreloadedSprites()
{
var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList<Sprite>.Empty);
foreach (var s in sprites) { s.Remove(); }
}
// timeStamps
private void ClearTimeStamps() => _timeStamps = ImmutableList<TimeStamp>.Empty;
private void EnqueueDeferredAction(Action action) =>
AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action));
private void ProcessDeferredActions()
{
var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue<Action>.Empty);
foreach (var action in actions) { action(); }
}
private MTRandom random; private MTRandom random;
public int RandomSeed { get; private set; } public int RandomSeed { get; private set; }
@@ -225,15 +152,15 @@ namespace Barotrauma
if (isClient) { return; } if (isClient) { return; }
ClearTimeStamps(); timeStamps.Clear();
ClearPendingEventSets(); pendingEventSets.Clear();
ClearSelectedEvents(); selectedEvents.Clear();
ClearActiveEvents(); activeEvents.Clear();
#if SERVER #if SERVER
MissionAction.ResetMissionsUnlockedThisRound(); MissionAction.ResetMissionsUnlockedThisRound();
UnlockPathAction.ResetPathsUnlockedThisRound(); UnlockPathAction.ResetPathsUnlockedThisRound();
#endif #endif
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false); pathFinder = new PathFinder(WayPoint.WayPointList, false);
totalPathLength = 0.0f; totalPathLength = 0.0f;
if (level != null) if (level != null)
{ {
@@ -308,8 +235,8 @@ namespace Barotrauma
void AddSet(EventSet eventSet) void AddSet(EventSet eventSet)
{ {
if (_pendingEventSets.Contains(eventSet)) { return; } if (pendingEventSets.Contains(eventSet)) { return; }
AddPendingEventSet(eventSet); pendingEventSets.Add(eventSet);
CreateEvents(eventSet); CreateEvents(eventSet);
} }
@@ -360,7 +287,7 @@ namespace Barotrauma
{ {
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{ {
AddNonRepeatableEvent(ep.Identifier); nonRepeatableEvents.Add(ep.Identifier);
} }
} }
foreach (EventSet childSet in eventSet.ChildSets) foreach (EventSet childSet in eventSet.ChildSets)
@@ -405,13 +332,13 @@ namespace Barotrauma
public void ActivateEvent(Event newEvent) public void ActivateEvent(Event newEvent)
{ {
AddActiveEvent(newEvent); activeEvents.Add(newEvent);
newEvent.Init(); newEvent.Init();
} }
public void ClearEvents() public void ClearEvents()
{ {
ClearActiveEvents(); activeEvents.Clear();
} }
private void SelectSettings() private void SelectSettings()
@@ -464,8 +391,7 @@ namespace Barotrauma
public IEnumerable<ContentFile> GetFilesToPreload() public IEnumerable<ContentFile> GetFilesToPreload()
{ {
var snapshot = _selectedEvents; foreach (List<Event> eventList in selectedEvents.Values)
foreach (ImmutableList<Event> eventList in snapshot.Values)
{ {
foreach (Event ev in eventList) foreach (Event ev in eventList)
{ {
@@ -518,13 +444,13 @@ namespace Barotrauma
foreach (ContentFile file in filesToPreload) foreach (ContentFile file in filesToPreload)
{ {
file.Preload(AddPreloadedSprite); file.Preload(preloadedSprites.Add);
} }
} }
public void TriggerOnEndRoundActions() public void TriggerOnEndRoundActions()
{ {
foreach (var ev in _activeEvents) foreach (var ev in activeEvents)
{ {
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f); (ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
} }
@@ -532,16 +458,17 @@ namespace Barotrauma
public void EndRound() public void EndRound()
{ {
ClearPendingEventSets(); pendingEventSets.Clear();
ClearSelectedEvents(); selectedEvents.Clear();
ClearActiveEvents(); activeEvents.Clear();
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue QueuedEvents.Clear();
ClearFinishedEvents(); finishedEvents.Clear();
ClearNonRepeatableEvents(); nonRepeatableEvents.Clear();
ClearPreloadedSprites(); preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
ClearTimeStamps(); timeStamps.Clear();
pathFinder = null; pathFinder = null;
} }
@@ -557,7 +484,7 @@ namespace Barotrauma
{ {
if (registerFinishedOnly) if (registerFinishedOnly)
{ {
foreach (var finishedEvent in _finishedEvents) foreach (var finishedEvent in finishedEvents)
{ {
EventSet parentSet = finishedEvent.ParentSet; EventSet parentSet = finishedEvent.ParentSet;
if (parentSet == null) { continue; } if (parentSet == null) { continue; }
@@ -572,7 +499,7 @@ namespace Barotrauma
} }
} }
level.LevelData.EventHistory.AddRange(_selectedEvents.Values level.LevelData.EventHistory.AddRange(selectedEvents.Values
.SelectMany(v => v) .SelectMany(v => v)
.Select(e => e.Prefab.Identifier) .Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
@@ -582,14 +509,14 @@ namespace Barotrauma
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
} }
} }
level.LevelData.NonRepeatableEvents.AddRange(_nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly) if (!registerFinishedOnly)
{ {
level.LevelData.FinishedEvents.Clear(); level.LevelData.FinishedEvents.Clear();
} }
bool Register(Identifier eventId) => !registerFinishedOnly || _finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
} }
public void SkipEventCooldown() public void SkipEventCooldown()
@@ -607,7 +534,7 @@ namespace Barotrauma
private void CreateEvents(EventSet eventSet) private void CreateEvents(EventSet eventSet)
{ {
RemoveSelectedEventSet(eventSet); selectedEvents.Remove(eventSet);
if (level == null) { return; } if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; } if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
@@ -674,7 +601,11 @@ namespace Barotrauma
if (newEvent == null) { continue; } if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
AddSelectedEvent(eventSet, newEvent); if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
unusedEvents.Remove(subEventPrefab); unusedEvents.Remove(subEventPrefab);
} }
} }
@@ -713,7 +644,11 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance(RandomSeed); var newEvent = eventPrefab.CreateInstance(RandomSeed);
if (newEvent == null) { continue; } if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
AddSelectedEvent(eventSet, newEvent); if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
} }
var location = GetEventLocation(); var location = GetEventLocation();
@@ -905,10 +840,9 @@ namespace Barotrauma
if (!eventsInitialized) if (!eventsInitialized)
{ {
var selectedSnapshot = _selectedEvents; foreach (var eventSet in selectedEvents.Keys)
foreach (var eventSet in selectedSnapshot.Keys)
{ {
foreach (var ev in selectedSnapshot[eventSet]) foreach (var ev in selectedEvents[eventSet])
{ {
ev.Init(eventSet); ev.Init(eventSet);
} }
@@ -979,25 +913,23 @@ namespace Barotrauma
{ {
recheck = false; recheck = false;
//activate pending event sets that can be activated //activate pending event sets that can be activated
var pendingSnapshot = _pendingEventSets; for (int i = pendingEventSets.Count - 1; i >= 0; i--)
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
{ {
var eventSet = pendingSnapshot[i]; var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; } if (!CanStartEventSet(eventSet)) { continue; }
RemovePendingEventSetAt(i); pendingEventSets.RemoveAt(i);
var selectedEventsList = GetSelectedEvents(eventSet); if (selectedEvents.ContainsKey(eventSet))
if (selectedEventsList.Count > 0)
{ {
//start events in this set //start events in this set
foreach (Event ev in selectedEventsList) foreach (Event ev in selectedEvents[eventSet])
{ {
AddActiveEvent(ev); activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold; eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown)) if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
{ {
eventCoolDown = settings.EventCooldown; eventCoolDown = settings.EventCooldown;
} }
@@ -1005,15 +937,12 @@ namespace Barotrauma
{ {
ev.Finished += () => ev.Finished += () =>
{ {
EnqueueDeferredAction(() => pendingEventSets.Add(eventSet);
{
AddPendingEventSet(eventSet);
CreateEvents(eventSet); CreateEvents(eventSet);
foreach (Event newEvent in GetSelectedEvents(eventSet)) foreach (Event newEvent in selectedEvents[eventSet])
{ {
if (!newEvent.Initialized) { newEvent.Init(eventSet); } if (!newEvent.Initialized) { newEvent.Init(eventSet); }
} }
});
}; };
} }
} }
@@ -1022,40 +951,37 @@ namespace Barotrauma
//add child event sets to pending //add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets) foreach (EventSet childEventSet in eventSet.ChildSets)
{ {
AddPendingEventSet(childEventSet); pendingEventSets.Add(childEventSet);
recheck = true; recheck = true;
} }
} }
} while (recheck); } while (recheck);
var activeSnapshot = _activeEvents; foreach (Event ev in activeEvents)
foreach (Event ev in activeSnapshot)
{ {
if (!ev.IsFinished) if (!ev.IsFinished)
{ {
ev.Update(deltaTime); ev.Update(deltaTime);
} }
else if (ev.Prefab != null && !IsEventFinished(ev)) else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
{ {
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{ {
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
} }
AddFinishedEvent(ev); finishedEvents.Add(ev);
} }
} }
if (QueuedEvents.TryDequeue(out var queuedEvent)) if (QueuedEvents.Count > 0)
{ {
AddActiveEvent(queuedEvent); activeEvents.Add(QueuedEvents.Dequeue());
} }
ProcessDeferredActions();
} }
public void EntitySpawned(Entity entity) public void EntitySpawned(Entity entity)
{ {
foreach (var ev in _activeEvents) foreach (var ev in activeEvents)
{ {
if (ev is ScriptedEvent scriptedEvent) if (ev is ScriptedEvent scriptedEvent)
{ {
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
@@ -61,48 +60,47 @@ namespace Barotrauma
return null; return null;
} }
#endif #endif
private static volatile ImmutableDictionary<Identifier, EventPrefab> _allEventPrefabs =
ImmutableDictionary<Identifier, EventPrefab>.Empty; private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
public static IEnumerable<EventPrefab> GetAllEventPrefabs() public static IEnumerable<EventPrefab> GetAllEventPrefabs()
{ {
return _allEventPrefabs.Values; return AllEventPrefabs.Values;
} }
/// <summary> /// <summary>
/// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets), /// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets),
/// and adds them to <see cref="_allEventPrefabs"/>. /// and adds them to <see cref="AllEventPrefabs"/>.
/// </summary> /// </summary>
public static void RefreshAllEventPrefabs() public static void RefreshAllEventPrefabs()
{ {
var builder = ImmutableDictionary.CreateBuilder<Identifier, EventPrefab>(); AllEventPrefabs.Clear();
foreach (var eventPrefab in EventPrefab.Prefabs) foreach (var eventPrefab in EventPrefab.Prefabs)
{ {
builder.TryAdd(eventPrefab.Identifier, eventPrefab); AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
} }
foreach (var eventSet in Prefabs) foreach (var eventSet in Prefabs)
{ {
AddChildEventPrefabs(eventSet, builder); AddChildEventPrefabs(eventSet);
} }
Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable());
} }
private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary<Identifier, EventPrefab>.Builder builder) private static void AddChildEventPrefabs(EventSet set)
{ {
foreach (var subEventPrefabs in set.EventPrefabs) foreach (var subEventPrefabs in set.EventPrefabs)
{ {
foreach (var eventPrefab in subEventPrefabs.EventPrefabs) foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
{ {
builder.TryAdd(eventPrefab.Identifier, eventPrefab); AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
} }
} }
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); } foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
} }
public static EventPrefab GetEventPrefab(Identifier identifier) public static EventPrefab GetEventPrefab(Identifier identifier)
{ {
return _allEventPrefabs.GetValueOrDefault(identifier); return AllEventPrefabs.GetValueOrDefault(identifier);
} }
/// <summary> /// <summary>
@@ -41,7 +41,7 @@ namespace Barotrauma
protected override void InitEventSpecific(EventSet parentSet) protected override void InitEventSpecific(EventSet parentSet)
{ {
var matchingItems = Item.ItemList.Where(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)).ToList(); var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
for (int i = 0; i < itemAmount; i++) for (int i = 0; i < itemAmount; i++)
{ {
@@ -111,7 +111,7 @@ namespace Barotrauma
{ {
if (!itemTag.IsEmpty) if (!itemTag.IsEmpty)
{ {
var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList(); var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any()) if (!itemsToDestroy.Any())
{ {
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".", DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".",
@@ -181,7 +181,7 @@ namespace Barotrauma
return; return;
} }
destructibleItems.Clear(); destructibleItems.Clear();
destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag))); destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
if (destructibleItems.None()) if (destructibleItems.None())
{ {
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".", DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".",
@@ -283,7 +283,7 @@ namespace Barotrauma
if (!IsClient) if (!IsClient)
{ {
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false); PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos)); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
if (!path.Unreachable) if (!path.Unreachable)
{ {
@@ -128,7 +128,7 @@ namespace Barotrauma
{ {
foreach (var stackedItem in item.GetStackedItems()) foreach (var stackedItem in item.GetStackedItems())
{ {
Item.MarkForDeconstruction(stackedItem); Item.DeconstructItems.Add(stackedItem);
} }
#if CLIENT #if CLIENT
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
@@ -138,7 +138,7 @@ namespace Barotrauma
{ {
foreach (var stackedItem in item.GetStackedItems()) foreach (var stackedItem in item.GetStackedItems())
{ {
Item.UnmarkForDeconstruction(stackedItem); Item.DeconstructItems.Remove(stackedItem);
} }
} }
} }
@@ -1089,7 +1089,7 @@ namespace Barotrauma
//Clear the grids to allow for garbage collection //Clear the grids to allow for garbage collection
Powered.Grids.Clear(); Powered.Grids.Clear();
Powered.ClearChangedConnections(); Powered.ChangedConnections.Clear();
try try
{ {
@@ -1146,7 +1146,6 @@ namespace Barotrauma
EventManager?.EndRound(); EventManager?.EndRound();
StatusEffect.StopAll(); StatusEffect.StopAll();
AfflictionPrefab.ClearAllEffects(); AfflictionPrefab.ClearAllEffects();
PhysicsBodyQueue.Clear();
IsRunning = false; IsRunning = false;
#if CLIENT #if CLIENT

Some files were not shown because too many files have changed in this diff Show More