Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9ad542029 | |||
| b5b25a2ccb | |||
| 25683dcf39 | |||
| 02689d0d86 | |||
| 099d664731 | |||
| 1f7d695bba | |||
| 50327a4d83 | |||
| 9b35f6b23f | |||
| 928cfb4fde | |||
| a9634988e9 | |||
| 87e0191a97 | |||
| 790378d2a5 | |||
| a4607dffad | |||
| ef66d27ffe | |||
| 84cb7cfeb7 | |||
| 53be3f0073 | |||
| 89aa818c0b | |||
| 3a5fbfde1e | |||
| b9b457262f | |||
| e161d92117 | |||
| 2e656509a8 | |||
| b0b4acf392 | |||
| e9673bf7f5 | |||
| eeeb3d9db3 | |||
| c882b0bb45 | |||
| 6c32873d4e | |||
| bdd4dcfb9e | |||
| ebe8ec455f | |||
| d440ccbce2 | |||
| 1294ba6dec | |||
| 4c8e016dea | |||
| 232f7203e2 | |||
| df0a4e62f5 | |||
| 7055480015 | |||
| 0e14983e88 | |||
| 9b05c51ae5 | |||
| 5e003bcf11 | |||
| 121f670c8b | |||
| f05d4ef129 | |||
| 4167448279 | |||
| 413cc3ed4c | |||
| 4f2da55a8e | |||
| f1808d9231 | |||
| bcec409618 | |||
| 14c610e6c7 | |||
| 958335a01c | |||
| b358882016 | |||
| b8f1642d9d | |||
| 64818775cb | |||
| 5bfa15564a | |||
| 2ea97d3d5e | |||
| bb2490eb13 | |||
| 57daa3ccb7 | |||
| c1fdedf955 | |||
| b70b6bf326 | |||
| 28acf02de0 | |||
| 1e76377b63 | |||
| 3dd9cfa741 | |||
| 26ac37ed46 | |||
| 5c136aab41 | |||
| cf6104f630 | |||
| 7c8dd452cf | |||
| cf3a50d6b5 | |||
| ac329a70a4 | |||
| e62450c763 | |||
| 44a9ce2417 | |||
| 23e0ff7aa6 | |||
| de53b4514e | |||
| 994610869d | |||
| b0e3faa2ad | |||
| fe34dcb0bd | |||
| 8717706b1c | |||
| b402e09b82 | |||
| 80832459b9 | |||
| cf2c8b8e0e | |||
| f802356b0a | |||
| ecc4d8a6c1 | |||
| d4b774642b | |||
| 103f28481d | |||
| dd51bdae3f | |||
| e5aa381e38 | |||
| 8190b3f312 | |||
| e26cfa2e52 | |||
| 5b52d22b1f | |||
| 5a452709c3 | |||
| 8470e81dc7 | |||
| 947a481400 | |||
| 368b18d440 | |||
| 1fe68aa861 | |||
| f38a7bd574 | |||
| ce8b984542 | |||
| d0969cc723 | |||
| a66b9041ec | |||
| 83c198bc26 | |||
| 26b3827210 | |||
| 4b04131fe3 | |||
| 168ce83820 | |||
| 5cbb635e54 | |||
| 0452c1fc2d | |||
| 800dec3b84 | |||
| 580f26b526 | |||
| f8ff97d2b7 | |||
| 9ee4728e2a | |||
| 22a74bf1fd | |||
| 845fcefad7 | |||
| 9b55bf4847 | |||
| e74f083300 | |||
| 09bc2d0891 | |||
| 28b355911d | |||
| e2c4282477 | |||
| 3192cc8b00 | |||
| de73a18637 | |||
| 8e8b8eb8aa | |||
| c676233dfd | |||
| 9b61cda6cf | |||
| ebf2935fb4 | |||
| 6e556a02d4 | |||
| 94556fd6e7 | |||
| d9d980122d | |||
| 394856fa04 | |||
| a28333ff4e | |||
| f4138d2398 | |||
| 91992194a9 | |||
| a7e9dc8c9c | |||
| da5b9389db | |||
| ddd9dac2fb | |||
| 124b1e545d | |||
| f52617deab | |||
| c8657caefa | |||
| c28a08a713 | |||
| a0287b8561 | |||
| 6ac49a10f4 | |||
| f70251fa3b | |||
| 13bfffa777 | |||
| a50dce8fc2 | |||
| a36bd70505 | |||
| 36471f2239 | |||
| dff38f7609 | |||
| 5f38b4a43a | |||
| 07d838eee0 | |||
| 5747d896eb | |||
| ad152ee747 | |||
| 471e256353 | |||
| 59584fefc1 | |||
| 948b7c48c5 | |||
| 5e33a908d2 | |||
| 9e2fc13460 | |||
| 6bbe5be5e6 | |||
| 9dde5cac7d | |||
| 6b9e48f96a | |||
| 30149b504d | |||
| fb4648d759 | |||
| d14c353115 | |||
| f1bae4c1ca | |||
| a61e705dbf | |||
| 64831bd580 | |||
| dc1093eeed | |||
| 95376622fa | |||
| 511f98ec18 | |||
| 70e98467e8 | |||
| e8bec96970 | |||
| a505d48a4c | |||
| bcc4357a16 | |||
| 668197ad6f | |||
| 0a91b89694 | |||
| 5b10661874 | |||
| 36a126774a | |||
| 705137e993 | |||
| ce4cd1fefd | |||
| 224e32ccf1 | |||
| f01ee61278 | |||
| c637e34b48 | |||
| 67c195034d | |||
| 02b1f524b6 | |||
| e76aaf5a34 | |||
| 422e8a6185 | |||
| ba10d9d031 | |||
| 87dc9be10e | |||
| d47b75c778 | |||
| 2c29969bfb | |||
| c84d9660e2 | |||
| dcd7df4860 | |||
| b3d0fbeb5d | |||
| fa340e91de | |||
| 771e73a798 | |||
| e75208507d | |||
| 863ee23583 | |||
| 9cc20a03c0 | |||
| cae3741953 | |||
| 80555ef933 | |||
| cf251451ed | |||
| 4cf4b1604b | |||
| fd037153ee | |||
| 02a7338ab8 | |||
| 70dd602bcf | |||
| ea602f6d2f | |||
| 06348d3ba5 | |||
| 2eb593f461 | |||
| 244c0fbec3 | |||
| 7b529bce57 | |||
| 36bed09bde | |||
| 024b07d5f4 | |||
| 0cab7954f8 | |||
| b325a01eea | |||
| 7e541cef3d | |||
| bb8869268e | |||
| cb171d350d | |||
| 5777b64a18 | |||
| 9b9529107c | |||
| 5421c7df4f | |||
| 4f02cb4967 | |||
| 6b8a0a7dca | |||
| dfb31eef16 | |||
| c6c0aadb00 | |||
| 3b65ea9008 | |||
| 708fe93efe | |||
| 13a9bc443e | |||
| 37e3a195dc | |||
| a28a6f3320 | |||
| 67d3d5f587 | |||
| f0f09c20fa | |||
| f28749d455 | |||
| ab2638b2cb | |||
| 22f587b7b9 | |||
| 92232d114b | |||
| 6619def365 | |||
| c776a5424d | |||
| 2a1d7760e6 | |||
| 009957e3b6 | |||
| 297f6a38cb | |||
| 440cbed76a | |||
| b36224f480 | |||
| f9a467453a | |||
| 60ed549605 | |||
| 3d51abc56b | |||
| 295c365a8f | |||
| adf9303a7e | |||
| b626fd1e47 | |||
| ed09908c3b | |||
| 0a9c673753 | |||
| c2aa7dd948 | |||
| 25081466be | |||
| 8dedc54468 | |||
| d0f5cb87e7 | |||
| 8dc58445f9 | |||
| 6faf1a0fcb | |||
| ee0988588f | |||
| 6e66a3114a | |||
| 15e0a2bd10 | |||
| 09faa8403a | |||
| 7e0e671539 | |||
| cc07db941f | |||
| f1e1b9238d | |||
| dda26df250 | |||
| 6e7b7c804c | |||
| 6a21255a38 | |||
| 6362a9c34f | |||
| 3ddaceb5ac | |||
| 055a508901 | |||
| 0bfceacaf3 | |||
| 0438d3c4ba | |||
| 3e81e27160 | |||
| 42acb32c69 | |||
| d6968f4ea9 | |||
| 75da3a398d | |||
| 1dad2babb7 | |||
| fe982b15b0 | |||
| 595470ccfb | |||
| 7d39c092c6 | |||
| 6a6be3caa4 | |||
| 1daf68dea1 | |||
| 4f8531332e | |||
| d70885711b | |||
| 6499e7608c | |||
| 569e14f50f | |||
| 71c2e54afd | |||
| bd5d04f5ab | |||
| 26b657a96f | |||
| cce5bf26c8 | |||
| 4d97a427f9 | |||
| aa7e825e70 | |||
| 2778df0fe7 | |||
| c6713f37bb | |||
| 52d920d969 | |||
| cb88d215fa | |||
| 7436ea3e8c | |||
| d2b9ca4c1b | |||
| 4b2bac7cd8 | |||
| 1da82cdec2 | |||
| 76fc52e042 | |||
| 6880e5e9ee | |||
| 01cc1d331b | |||
| 9e957a75b0 | |||
| a546615f69 | |||
| 9f1c3fa823 | |||
| d5d14e9684 | |||
| 086f45510f | |||
| e24024cbb2 | |||
| e7e444e9b2 | |||
| caec44c57d | |||
| f4a0d149ca | |||
| 7c61859840 | |||
| 9474f7654c | |||
| 854d7bea1f | |||
| 7b8275100d | |||
| 886eebdbb2 |
@@ -73,7 +73,7 @@ body:
|
||||
label: Version
|
||||
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
|
||||
options:
|
||||
- v1.13.3.1 (Summer Update 2026)
|
||||
- v1.12.6.2 (Spring Update 2026)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -61,3 +61,5 @@ Deploy/DeployAll/PrivateKey.*
|
||||
#Rider
|
||||
*.DotSettings.user
|
||||
.vscode/settings.json
|
||||
.vscode/launch.json
|
||||
.vscode/tasks.json
|
||||
|
||||
@@ -258,9 +258,7 @@ namespace Barotrauma
|
||||
//RelativeSpacing = 0.05f
|
||||
};
|
||||
|
||||
GUIFrame left = new(new RectTransform(new Vector2(0.25f, 1f), characterIndicatorArea.RectTransform), style: null);
|
||||
|
||||
InventorySlotContainer = new GUICustomComponent(new RectTransform(Vector2.One, left.RectTransform),
|
||||
InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight),
|
||||
(spriteBatch, component) =>
|
||||
{
|
||||
for (int i = 0; i < character.Inventory.Capacity; i++)
|
||||
@@ -268,10 +266,6 @@ namespace Barotrauma
|
||||
if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { 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
|
||||
bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn();
|
||||
|
||||
@@ -298,7 +292,8 @@ 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,
|
||||
OnClicked = (button, userData) =>
|
||||
@@ -321,11 +316,12 @@ namespace Barotrauma
|
||||
|
||||
return true;
|
||||
},
|
||||
ToolTip = TextManager.Get("tutorial.roles.medic.objective.cpr"),
|
||||
ToolTip = TextManager.Get("doctor.cprobjective"),
|
||||
IgnoreLayoutGroups = true,
|
||||
Visible = false
|
||||
};
|
||||
|
||||
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.5f, 1.0f), characterIndicatorArea.RectTransform),
|
||||
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform),
|
||||
(spriteBatch, component) =>
|
||||
{
|
||||
DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true);
|
||||
@@ -372,6 +368,8 @@ namespace Barotrauma
|
||||
CanBeFocused = false
|
||||
};
|
||||
|
||||
characterIndicatorArea.Recalculate();
|
||||
|
||||
healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null)
|
||||
{
|
||||
HoverCursor = CursorState.Hand
|
||||
@@ -2145,7 +2143,7 @@ namespace Barotrauma
|
||||
if (existingAffliction == null)
|
||||
{
|
||||
existingAffliction = afflictionPrefab.Instantiate(strength);
|
||||
afflictions.Add(existingAffliction, limb);
|
||||
afflictions.TryAdd(existingAffliction, limb);
|
||||
newAdded = true;
|
||||
}
|
||||
existingAffliction.SetStrength(strength);
|
||||
|
||||
@@ -867,19 +867,6 @@ namespace Barotrauma
|
||||
});
|
||||
}, 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) =>
|
||||
{
|
||||
SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid;
|
||||
@@ -1401,12 +1388,12 @@ namespace Barotrauma
|
||||
if (me.SimPosition.Length() > 2000.0f)
|
||||
{
|
||||
NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
|
||||
MapEntity.MapEntityList.RemoveAt(i);
|
||||
MapEntity.MapEntityList.Remove(me);
|
||||
}
|
||||
else if (!me.ShouldBeSaved)
|
||||
{
|
||||
NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
|
||||
MapEntity.MapEntityList.RemoveAt(i);
|
||||
MapEntity.MapEntityList.Remove(me);
|
||||
}
|
||||
else if (me is Item)
|
||||
{
|
||||
@@ -4467,24 +4454,17 @@ namespace Barotrauma
|
||||
|
||||
public static void StartLocalMPSession(int numClients = 2)
|
||||
{
|
||||
string extraArguments = "-multiclienttestmode";
|
||||
if (NetConfig.UseLenientHandshake)
|
||||
{
|
||||
extraArguments += " -lenienthandshake";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Process.GetProcessesByName("DedicatedServer").Length == 0)
|
||||
{
|
||||
#if WINDOWS
|
||||
Process.Start("DedicatedServer.exe", arguments: extraArguments);
|
||||
Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode");
|
||||
#else
|
||||
Process.Start("./DedicatedServer", arguments: extraArguments);
|
||||
Process.Start("./DedicatedServer", arguments: "-multiclienttestmode");
|
||||
#endif
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
GameClient.MultiClientTestMode = true;
|
||||
#endif
|
||||
@@ -4498,13 +4478,10 @@ namespace Barotrauma
|
||||
for (int i = 2; i <= numClients; i++)
|
||||
{
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
|
||||
string clientArguments = $"-connect server localhost -username client{i} -skipintro";
|
||||
|
||||
#if WINDOWS
|
||||
Process.Start("Barotrauma.exe", arguments: $"{clientArguments} {extraArguments}");
|
||||
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
|
||||
#else
|
||||
Process.Start("./Barotrauma", arguments: $"{clientArguments} {extraArguments}");
|
||||
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
|
||||
public void DebugDraw(SpriteBatch spriteBatch)
|
||||
{
|
||||
foreach (Event ev in activeEvents)
|
||||
foreach (Event ev in _activeEvents)
|
||||
{
|
||||
Vector2 drawPos = ev.DebugDrawPos;
|
||||
drawPos.Y = -drawPos.Y;
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (isGraphHovered || isGraphSelected)
|
||||
{
|
||||
foreach (var timeStamp in timeStamps)
|
||||
foreach (var timeStamp in _timeStamps)
|
||||
{
|
||||
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
|
||||
if (t == order)
|
||||
@@ -205,7 +205,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
adjustedYStep = GUI.AdjustForTextScale(12);
|
||||
foreach (EventSet eventSet in pendingEventSets)
|
||||
foreach (EventSet eventSet in _pendingEventSets)
|
||||
{
|
||||
if (Submarine.MainSub == null) { break; }
|
||||
|
||||
@@ -263,7 +263,7 @@ namespace Barotrauma
|
||||
y += yStep;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -651,11 +650,11 @@ namespace Barotrauma
|
||||
|
||||
if (MouseOn != null)
|
||||
{
|
||||
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
|
||||
if (!MouseOn.ToolTip.IsNullOrWhiteSpace())
|
||||
{
|
||||
MouseOn.DrawToolTip(spriteBatch);
|
||||
}
|
||||
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
|
||||
}
|
||||
|
||||
if (SubEditorScreen.IsSubEditor())
|
||||
@@ -2324,10 +2323,10 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Creates a 7-segment display.
|
||||
/// </summary>
|
||||
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/>.</param>
|
||||
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/> or empty.</param>
|
||||
/// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param>
|
||||
/// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param>
|
||||
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)
|
||||
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, 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)
|
||||
{
|
||||
@@ -2338,7 +2337,7 @@ namespace Barotrauma
|
||||
};
|
||||
|
||||
leftLabel = null;
|
||||
if (leftLabelText != null)
|
||||
if (!leftLabelText.IsNullOrEmpty())
|
||||
{
|
||||
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;
|
||||
CalculateCaretPos();
|
||||
ClearSelection();
|
||||
bool wasSelected = selected;
|
||||
selected = true;
|
||||
GUI.KeyboardDispatcher.Subscriber = this;
|
||||
OnSelected?.Invoke(this, Keys.None);
|
||||
if (!selected && PlaySoundOnSelect && !ignoreSelectSound)
|
||||
if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound)
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -113,10 +113,7 @@ namespace Barotrauma
|
||||
|
||||
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState);
|
||||
|
||||
if (currentBackgroundTexture.Texture != null)
|
||||
{
|
||||
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
|
||||
}
|
||||
overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale);
|
||||
|
||||
double noiseT = Timing.TotalTime * 0.02f;
|
||||
|
||||
@@ -278,12 +278,6 @@ namespace Barotrauma
|
||||
GameClient.MultiClientTestMode = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ConsoleArguments.Contains("-lenienthandshake"))
|
||||
{
|
||||
NetConfig.UseLenientHandshake = true;
|
||||
}
|
||||
|
||||
GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window);
|
||||
|
||||
PerformanceCounter = new PerformanceCounter();
|
||||
|
||||
@@ -867,7 +867,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Add(stackedItem);
|
||||
Item.MarkForDeconstruction(stackedItem);
|
||||
}
|
||||
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
|
||||
}
|
||||
@@ -875,7 +875,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Remove(stackedItem);
|
||||
Item.UnmarkForDeconstruction(stackedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,12 @@ namespace Barotrauma
|
||||
(int)SlotPositions[i].Y,
|
||||
(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>();
|
||||
if (itemContainer != null)
|
||||
{
|
||||
@@ -616,7 +622,6 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (HideSlot(i)) { continue; }
|
||||
var item = slots[i].FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)).Fallback(Msg);
|
||||
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg));
|
||||
}
|
||||
|
||||
CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled);
|
||||
|
||||
@@ -1933,7 +1933,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
void CalculateDistance()
|
||||
{
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
|
||||
if (!path.Unreachable)
|
||||
{
|
||||
|
||||
+8
-42
@@ -12,12 +12,9 @@ namespace Barotrauma.Items.Components
|
||||
private partial class PowerGroup
|
||||
{
|
||||
private GUIFrame? frame;
|
||||
private GUIFrame? groupContent;
|
||||
private GUILayoutGroup? nameGroup;
|
||||
private GUITextBox? nameBox;
|
||||
private GUITextBlock? loadDisplayNameLabel;
|
||||
private GUIScrollBar? ratioSlider;
|
||||
private readonly List<GUITextBlock> powerUnitLabels = [];
|
||||
private readonly List<GUITextBlock> powerUnitLabels = new List<GUITextBlock>();
|
||||
private GUIFrame? divider;
|
||||
|
||||
public bool IsVisible { get; private set; } = true;
|
||||
@@ -25,9 +22,9 @@ namespace Barotrauma.Items.Components
|
||||
public void CreateGUI()
|
||||
{
|
||||
frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null);
|
||||
groupContent = new GUIFrame(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
|
||||
GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
|
||||
|
||||
nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
|
||||
GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
|
||||
{
|
||||
Stretch = true
|
||||
};
|
||||
@@ -40,7 +37,7 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
};
|
||||
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Screen.Selected == GameMain.SubEditorScreen ? Name : DisplayName.Value, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
|
||||
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
|
||||
{
|
||||
MaxTextLength = MaxNameLength,
|
||||
OverflowClip = true,
|
||||
@@ -51,40 +48,17 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
};
|
||||
nameBox.OnSelected += (tb, _) =>
|
||||
{
|
||||
if (tb.Selected) { return; }
|
||||
tb.Text = Name;
|
||||
};
|
||||
nameBox.OnDeselected += (tb, _) =>
|
||||
{
|
||||
Name = tb.Text;
|
||||
tb.CaretIndex = 0;
|
||||
if (GameMain.Client == null) { return; }
|
||||
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) },
|
||||
out loadDisplayNameLabel, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
|
||||
out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
|
||||
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")
|
||||
{
|
||||
Step = SupplyRatioStep,
|
||||
@@ -104,17 +78,16 @@ namespace Barotrauma.Items.Components
|
||||
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),
|
||||
out GUITextBlock? _, out GUITextBlock ratioDisplayUnitLabel,
|
||||
out GUITextBlock? _, out GUITextBlock _,
|
||||
rightLabelText: "%");
|
||||
ratioDisplay.TextGetter = () => DisplayRatio.ToString();
|
||||
|
||||
GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) },
|
||||
out GUITextBlock? outputDisplayNameLabel, out GUITextBlock outputDisplayUnitLabel,
|
||||
out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel,
|
||||
TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font);
|
||||
outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString();
|
||||
|
||||
powerUnitLabels.Add(loadDisplayUnitLabel);
|
||||
powerUnitLabels.Add(ratioDisplayUnitLabel);
|
||||
powerUnitLabels.Add(outputDisplayUnitLabel);
|
||||
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
|
||||
|
||||
@@ -138,14 +111,7 @@ namespace Barotrauma.Items.Components
|
||||
IsVisible = PowerOut.Wires.Count >= 1;
|
||||
frame!.Visible = IsVisible;
|
||||
divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this;
|
||||
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));
|
||||
}
|
||||
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -716,19 +716,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
GetAvailablePower(out float batteryCharge, out float batteryCapacity);
|
||||
|
||||
List<Item> availableAmmo = [];
|
||||
AddAmmoFromContainer(item.GetComponent<ItemContainer>());
|
||||
List<Item> availableAmmo = new List<Item>();
|
||||
foreach (MapEntity e in item.linkedTo)
|
||||
{
|
||||
if (e is not Item linkedItem) { continue; }
|
||||
AddAmmoFromContainer(linkedItem.GetComponent<ItemContainer>());
|
||||
}
|
||||
|
||||
void AddAmmoFromContainer(ItemContainer itemContainer)
|
||||
{
|
||||
if (itemContainer == null) { return; }
|
||||
if (!(e is Item linkedItem)) { continue; }
|
||||
var itemContainer = linkedItem.GetComponent<ItemContainer>();
|
||||
if (itemContainer == null) { continue; }
|
||||
availableAmmo.AddRange(itemContainer.Inventory.AllItems);
|
||||
//add empty slots too
|
||||
for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++)
|
||||
{
|
||||
availableAmmo.Add(null);
|
||||
|
||||
@@ -339,19 +339,6 @@ namespace Barotrauma
|
||||
toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖";
|
||||
}
|
||||
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)
|
||||
{
|
||||
colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
|
||||
@@ -370,6 +357,18 @@ namespace Barotrauma
|
||||
#if DEBUG
|
||||
toolTip += $" ({item.Prefab.Identifier})";
|
||||
#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))
|
||||
{
|
||||
toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
|
||||
@@ -1302,6 +1301,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
SoundPlayer.PlayUISound(GUISoundType.DropItem);
|
||||
bool removed = false;
|
||||
if (Screen.Selected is SubEditorScreen editor)
|
||||
{
|
||||
@@ -1892,7 +1892,7 @@ namespace Barotrauma
|
||||
|
||||
}
|
||||
}
|
||||
else if (Item.DeconstructItems.Contains(item) &&
|
||||
else if (Item.IsMarkedForDeconstruction(item) &&
|
||||
OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
|
||||
{
|
||||
DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
|
||||
|
||||
+6
-6
@@ -75,11 +75,11 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
|
||||
|
||||
|
||||
// ReSharper restore FieldCanBeMadeReadOnly.Local
|
||||
private const string SettingsResetButtonText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetVisibleSettings";
|
||||
private const string SettingsResetPromptTitle = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Title";
|
||||
private const string SettingsResetPromptContents = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Message";
|
||||
private const string SettingsResetPromptYesText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Yes";
|
||||
private const string SettingsResetPromptNoText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.No";
|
||||
private const string SettingsResetButtonText = "LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings";
|
||||
private const string SettingsResetPromptTitle = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title";
|
||||
private const string SettingsResetPromptContents = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message";
|
||||
private const string SettingsResetPromptYesText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes";
|
||||
private const string SettingsResetPromptNoText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No";
|
||||
|
||||
|
||||
private event Action OnApplyInstalledModsChanges;
|
||||
@@ -100,7 +100,7 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
|
||||
var menuTitleLayoutGroup = new GUILayoutGroup(
|
||||
new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft);
|
||||
GUIUtil.Label(menuTitleLayoutGroup,
|
||||
GetLocalizedString($"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"),
|
||||
GetLocalizedString("LuaCsForBarotrauma.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"),
|
||||
GUIStyle.LargeFont, new Vector2(1f, 1f));
|
||||
|
||||
// page contents
|
||||
|
||||
+2
-2
@@ -47,9 +47,9 @@ public class SettingsMenuSystem : ISettingsMenuSystem
|
||||
|
||||
_gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance,
|
||||
GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods",
|
||||
$"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton");
|
||||
"LuaCsForBarotrauma.SettingsMenu.ModGameplayButton");
|
||||
/*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance,
|
||||
"SettingsMenuTab.Controls", $"{LuaCsSetup.PackageName}.SettingsMenu.ModControlsButton");
|
||||
"SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton");
|
||||
*/
|
||||
|
||||
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance);
|
||||
|
||||
+1
-3
@@ -8,7 +8,7 @@ using System.Xml.Linq;
|
||||
namespace Barotrauma
|
||||
{
|
||||
|
||||
class BackgroundCreature : ISteerable, ILevelRenderableObject
|
||||
class BackgroundCreature : ISteerable
|
||||
{
|
||||
const float MaxDepth = 10000.0f;
|
||||
|
||||
@@ -76,8 +76,6 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public Vector3 Position => new Vector3(position.X, position.Y, Depth);
|
||||
|
||||
public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position)
|
||||
{
|
||||
this.Prefab = prefab;
|
||||
|
||||
+53
-3
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -14,11 +15,52 @@ namespace Barotrauma
|
||||
|
||||
private float checkVisibleTimer;
|
||||
|
||||
private readonly List<BackgroundCreature> creatures = [];
|
||||
private readonly List<BackgroundCreature> creatures = new List<BackgroundCreature>();
|
||||
|
||||
private readonly List<BackgroundCreature> visibleCreatures = [];
|
||||
private readonly List<BackgroundCreature> visibleCreatures = new List<BackgroundCreature>();
|
||||
|
||||
public IEnumerable<BackgroundCreature> VisibleCreatures => visibleCreatures;
|
||||
public BackgroundCreatureManager()
|
||||
{
|
||||
/*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)
|
||||
{
|
||||
@@ -119,6 +161,14 @@ 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)
|
||||
{
|
||||
foreach (BackgroundCreature creature in visibleCreatures)
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Barotrauma
|
||||
|
||||
public void DrawFront(SpriteBatch spriteBatch, Camera cam)
|
||||
{
|
||||
renderer?.DrawForeground(spriteBatch, cam, backgroundCreatureManager, LevelObjectManager);
|
||||
renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager);
|
||||
}
|
||||
public void ClientEventRead(IReadMessage msg, float sendingTime)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LevelObject : ILevelRenderableObject
|
||||
partial class LevelObject
|
||||
{
|
||||
public float SwingTimer;
|
||||
public float ScaleOscillateTimer;
|
||||
|
||||
+45
-114
@@ -8,20 +8,13 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public interface ILevelRenderableObject
|
||||
{
|
||||
public Vector3 Position { get; }
|
||||
|
||||
}
|
||||
|
||||
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?
|
||||
private readonly List<ILevelRenderableObject> visibleObjectsBack = new List<ILevelRenderableObject>(MaxVisibleObjects);
|
||||
private readonly List<ILevelRenderableObject> visibleObjectsMid = new List<ILevelRenderableObject>(MaxVisibleObjects);
|
||||
private readonly List<ILevelRenderableObject> visibleObjectsFront = new List<ILevelRenderableObject>(MaxVisibleObjects);
|
||||
private readonly HashSet<ILevelRenderableObject> allVisibleObjects = new HashSet<ILevelRenderableObject>(MaxVisibleObjects);
|
||||
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>(MaxVisibleObjects);
|
||||
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>(MaxVisibleObjects);
|
||||
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>(MaxVisibleObjects);
|
||||
private readonly HashSet<LevelObject> allVisibleObjects = new HashSet<LevelObject>(MaxVisibleObjects);
|
||||
|
||||
private double NextRefreshTime;
|
||||
|
||||
@@ -42,44 +35,35 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime, Camera cam)
|
||||
{
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsBack)
|
||||
foreach (LevelObject obj in visibleObjectsBack)
|
||||
{
|
||||
if (obj is LevelObject levelObj)
|
||||
{
|
||||
levelObj.Update(deltaTime, cam);
|
||||
obj.Update(deltaTime, cam);
|
||||
}
|
||||
}
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsMid)
|
||||
foreach (LevelObject obj in visibleObjectsMid)
|
||||
{
|
||||
if (obj is LevelObject levelObj)
|
||||
{
|
||||
levelObj.Update(deltaTime, cam);
|
||||
obj.Update(deltaTime, cam);
|
||||
}
|
||||
}
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsFront)
|
||||
foreach (LevelObject obj in visibleObjectsFront)
|
||||
{
|
||||
if (obj is LevelObject levelObj)
|
||||
{
|
||||
levelObj.Update(deltaTime, cam);
|
||||
}
|
||||
obj.Update(deltaTime, cam);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all visible objects, but not in order, because internally uses a HashSet.
|
||||
/// </summary>
|
||||
public IEnumerable<ILevelRenderableObject> GetAllVisibleObjects()
|
||||
public IEnumerable<LevelObject> GetAllVisibleObjects()
|
||||
{
|
||||
allVisibleObjects.Clear();
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsBack)
|
||||
foreach (LevelObject obj in visibleObjectsBack)
|
||||
{
|
||||
allVisibleObjects.Add(obj);
|
||||
}
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsMid)
|
||||
foreach (LevelObject obj in visibleObjectsMid)
|
||||
{
|
||||
allVisibleObjects.Add(obj);
|
||||
}
|
||||
foreach (ILevelRenderableObject obj in visibleObjectsFront)
|
||||
foreach (LevelObject obj in visibleObjectsFront)
|
||||
{
|
||||
allVisibleObjects.Add(obj);
|
||||
}
|
||||
@@ -89,7 +73,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Checks which level objects are in camera view and adds them to the visibleObjects lists
|
||||
/// </summary>
|
||||
private void RefreshVisibleObjects(Rectangle currentIndices, BackgroundCreatureManager backgroundCreatureManager, float zoom)
|
||||
private void RefreshVisibleObjects(Rectangle currentIndices, float zoom)
|
||||
{
|
||||
visibleObjectsBack.Clear();
|
||||
visibleObjectsMid.Clear();
|
||||
@@ -168,27 +152,6 @@ 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
|
||||
//(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
|
||||
@@ -202,28 +165,28 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Draw the objects behind the level walls
|
||||
/// </summary>
|
||||
public void DrawObjectsBack(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
|
||||
public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam)
|
||||
{
|
||||
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsBack);
|
||||
DrawObjects(spriteBatch, cam, visibleObjectsBack);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the objects in front of the level walls, but behind characters
|
||||
/// </summary>
|
||||
public void DrawObjectsMid(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
|
||||
public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam)
|
||||
{
|
||||
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsMid);
|
||||
DrawObjects(spriteBatch, cam, visibleObjectsMid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the objects in front of the level walls and characters
|
||||
/// </summary>
|
||||
public void DrawObjectsFront(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
|
||||
public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam)
|
||||
{
|
||||
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsFront);
|
||||
DrawObjects(spriteBatch, cam, visibleObjectsFront);
|
||||
}
|
||||
|
||||
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, BackgroundCreatureManager backgroundCreatureManager, List<ILevelRenderableObject> objectList)
|
||||
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List<LevelObject> objectList)
|
||||
{
|
||||
Rectangle indices = Rectangle.Empty;
|
||||
indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize);
|
||||
@@ -244,7 +207,7 @@ namespace Barotrauma
|
||||
float z = 0.0f;
|
||||
if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime))
|
||||
{
|
||||
RefreshVisibleObjects(indices, backgroundCreatureManager, cam.Zoom);
|
||||
RefreshVisibleObjects(indices, cam.Zoom);
|
||||
ForceRefreshVisibleObjects = false;
|
||||
if (cam.Zoom < 0.1f)
|
||||
{
|
||||
@@ -253,64 +216,50 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool prevObjectHasDeformableSprite = false;
|
||||
foreach (ILevelRenderableObject obj2 in objectList)
|
||||
foreach (LevelObject obj in objectList)
|
||||
{
|
||||
Vector2 camDiff = new Vector2(obj2.Position.X, obj2.Position.Y) - cam.WorldViewCenter;
|
||||
Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter;
|
||||
camDiff.Y = -camDiff.Y;
|
||||
|
||||
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;
|
||||
Sprite activeSprite = obj.Sprite;
|
||||
activeSprite?.Draw(
|
||||
spriteBatch,
|
||||
new Vector2(levelObject.Position.X, -levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength,
|
||||
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / levelObject.Prefab.FadeOutDepth),
|
||||
new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength,
|
||||
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth),
|
||||
activeSprite.Origin,
|
||||
levelObject.CurrentRotation,
|
||||
levelObject.CurrentScale,
|
||||
obj.CurrentRotation,
|
||||
obj.CurrentScale,
|
||||
SpriteEffects.None,
|
||||
z);
|
||||
|
||||
if (hasDeformableSprite)
|
||||
if (obj.ActivePrefab.DeformableSprite != null)
|
||||
{
|
||||
if (levelObject.CurrentSpriteDeformation != null)
|
||||
if (obj.CurrentSpriteDeformation != null)
|
||||
{
|
||||
levelObject.ActivePrefab.DeformableSprite.Deform(levelObject.CurrentSpriteDeformation);
|
||||
obj.ActivePrefab.DeformableSprite.Deform(obj.CurrentSpriteDeformation);
|
||||
}
|
||||
else
|
||||
{
|
||||
levelObject.ActivePrefab.DeformableSprite.Reset();
|
||||
obj.ActivePrefab.DeformableSprite.Reset();
|
||||
}
|
||||
levelObject.ActivePrefab.DeformableSprite?.Draw(cam,
|
||||
new Vector3(new Vector2(levelObject.Position.X, levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, z * 10.0f),
|
||||
levelObject.ActivePrefab.DeformableSprite.Origin,
|
||||
levelObject.CurrentRotation,
|
||||
levelObject.CurrentScale,
|
||||
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / 5000.0f));
|
||||
obj.ActivePrefab.DeformableSprite?.Draw(cam,
|
||||
new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f),
|
||||
obj.ActivePrefab.DeformableSprite.Origin,
|
||||
obj.CurrentRotation,
|
||||
obj.CurrentScale,
|
||||
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f));
|
||||
}
|
||||
prevObjectHasDeformableSprite = hasDeformableSprite;
|
||||
|
||||
|
||||
if (GameMain.DebugDraw)
|
||||
{
|
||||
GUI.DrawRectangle(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
|
||||
GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
|
||||
|
||||
if (levelObject.Triggers == null) { continue; }
|
||||
foreach (LevelTrigger trigger in levelObject.Triggers)
|
||||
if (obj.Triggers == null) { continue; }
|
||||
foreach (LevelTrigger trigger in obj.Triggers)
|
||||
{
|
||||
if (trigger.PhysicsBody == null) continue;
|
||||
GUI.DrawLine(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
|
||||
GUI.DrawLine(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
|
||||
|
||||
Vector2 flowForce = trigger.GetWaterFlowVelocity();
|
||||
if (flowForce.LengthSquared() > 1)
|
||||
@@ -323,24 +272,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,9 +214,8 @@ namespace Barotrauma
|
||||
//calculate the sum of the forces of nearby level triggers
|
||||
//and use it to move the water texture and water distortion effect
|
||||
Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity;
|
||||
foreach (ILevelRenderableObject obj in level.LevelObjectManager.GetAllVisibleObjects())
|
||||
foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects())
|
||||
{
|
||||
if (obj is not LevelObject levelObject) { continue; }
|
||||
if (levelObject.Triggers == null) { continue; }
|
||||
//use the largest water flow velocity of all the triggers
|
||||
Vector2 objectMaxFlow = Vector2.Zero;
|
||||
@@ -275,7 +274,11 @@ namespace Barotrauma
|
||||
SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null,
|
||||
cam.Transform);
|
||||
|
||||
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, backgroundCreatureManager, cam);
|
||||
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam);
|
||||
if (cam.Zoom > 0.05f)
|
||||
{
|
||||
backgroundCreatureManager?.Draw(spriteBatch, cam);
|
||||
}
|
||||
|
||||
level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset);
|
||||
|
||||
@@ -289,18 +292,17 @@ namespace Barotrauma
|
||||
BlendState.NonPremultiplied,
|
||||
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
|
||||
cam.Transform);
|
||||
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, backgroundCreatureManager, cam);
|
||||
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam);
|
||||
spriteBatch.End();
|
||||
}
|
||||
|
||||
public void DrawForeground(SpriteBatch spriteBatch, Camera cam,
|
||||
BackgroundCreatureManager backgroundCreatureManager, LevelObjectManager backgroundSpriteManager = null)
|
||||
public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null)
|
||||
{
|
||||
spriteBatch.Begin(SpriteSortMode.Deferred,
|
||||
BlendState.NonPremultiplied,
|
||||
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
|
||||
cam.Transform);
|
||||
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, backgroundCreatureManager, cam);
|
||||
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam);
|
||||
spriteBatch.End();
|
||||
}
|
||||
|
||||
|
||||
@@ -471,11 +471,11 @@ namespace Barotrauma
|
||||
|
||||
if (item0 == null && item1 != null)
|
||||
{
|
||||
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
}
|
||||
else if (item0 != null && item1 == null)
|
||||
{
|
||||
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
|
||||
}
|
||||
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)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
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)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
depthSortedDamageable.Clear();
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
@@ -217,7 +217,7 @@ namespace Barotrauma
|
||||
|
||||
public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
|
||||
{
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
|
||||
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
|
||||
|
||||
foreach (MapEntity e in entitiesToRender)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EntitySpawner : Entity, IServerSerializable
|
||||
{
|
||||
public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>();
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
@@ -34,7 +54,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found.");
|
||||
}
|
||||
receivedEvents.Add((entity, true));
|
||||
receivedEventsQueue.Enqueue((entity, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,7 +77,7 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier);
|
||||
}
|
||||
}
|
||||
receivedEvents.Add((newItem, false));
|
||||
receivedEventsQueue.Enqueue((newItem, false));
|
||||
}
|
||||
break;
|
||||
case (byte)SpawnableType.Character:
|
||||
@@ -68,7 +88,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
receivedEvents.Add((character, false));
|
||||
receivedEventsQueue.Enqueue((character, false));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -2547,7 +2547,7 @@ namespace Barotrauma.Networking
|
||||
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
|
||||
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
|
||||
outmsg.WriteUInt16(ChatMessage.LastID);
|
||||
outmsg.WriteUInt16(SpoofEntityManagerReceivedId ? (ushort)1 : EntityEventManager.LastReceivedID);
|
||||
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
|
||||
outmsg.WriteUInt16(LastClientListUpdateID);
|
||||
|
||||
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
|
||||
@@ -3370,12 +3370,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
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;
|
||||
|
||||
public bool TypingChatMessage(GUITextBox textBox, string text)
|
||||
@@ -3922,7 +3916,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
errorLines.Add("");
|
||||
errorLines.Add("EntitySpawner events:");
|
||||
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
|
||||
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot())
|
||||
{
|
||||
errorLines.Add(
|
||||
(isRemoval ? "Remove " : "Create ") +
|
||||
|
||||
@@ -89,7 +89,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
byte queueId = msg.ReadByte();
|
||||
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
|
||||
bool isRadio = msg.ReadBoolean();
|
||||
VoipQueue queue = queues.Find(q => q.QueueID == queueId);
|
||||
|
||||
if (queue == null)
|
||||
@@ -118,21 +117,19 @@ namespace Barotrauma.Networking
|
||||
float rangeMultiplier = spectating ? 2.0f : 1.0f;
|
||||
WifiComponent senderRadio = null;
|
||||
|
||||
var messageType = isRadio ? ChatMessageType.Radio : ChatMessageType.Default;
|
||||
|
||||
var messageType =
|
||||
!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.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
|
||||
client.RadioNoise = 0.0f;
|
||||
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.SetRange(senderRadioRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadioRange * speechImpedimentMultiplier * rangeMultiplier);
|
||||
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier);
|
||||
if (distanceFactor > RangeNear && !spectating)
|
||||
{
|
||||
//noise starts increasing exponentially after 40% range
|
||||
|
||||
@@ -533,18 +533,6 @@ namespace Barotrauma
|
||||
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
|
||||
|
||||
var minButtonSize = new Point(120, 20);
|
||||
@@ -1138,11 +1126,6 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
if (NetConfig.UseLenientHandshake)
|
||||
{
|
||||
arguments.Add("-lenienthandshake");
|
||||
}
|
||||
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Sounds;
|
||||
using Barotrauma.Steam;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Sounds;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -2047,8 +2044,6 @@ namespace Barotrauma
|
||||
|
||||
private bool SaveSubToFile(string name, ContentPackage packageToSaveTo)
|
||||
{
|
||||
bool remoteStorageWasEnabled = Submarine.MainSub.Info.SaveToRemoteStorage;
|
||||
|
||||
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
|
||||
|
||||
static string getExistingFilePath(ContentPackage package, string fileName)
|
||||
@@ -2277,16 +2272,6 @@ namespace Barotrauma
|
||||
linkedSubBox.AddItem(sub.Name, sub);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3238,42 +3223,6 @@ namespace Barotrauma
|
||||
|
||||
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);
|
||||
|
||||
GUIButton createTabberBtn(string labelTag)
|
||||
@@ -3797,7 +3746,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var package = GetLocalPackageThatOwnsSub(subInfo);
|
||||
if (package != null || subInfo.IsFromRemoteStorage)
|
||||
if (package != null)
|
||||
{
|
||||
deleteBtn.Enabled = true;
|
||||
}
|
||||
@@ -3822,29 +3771,13 @@ namespace Barotrauma
|
||||
searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); };
|
||||
searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; };
|
||||
|
||||
List<SubmarineInfo> allSubs = [.. SubmarineInfo.SavedSubmarines];
|
||||
foreach (SteamRemoteStorage.RemoteFile remoteFile in SteamRemoteStorage.Files.Where(file => file.Filename.EndsWith(".sub")))
|
||||
{
|
||||
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);
|
||||
var sortedSubs = GetLoadableSubs()
|
||||
.OrderBy(s => s.Type)
|
||||
.ThenBy(s => s.Name)
|
||||
.ToList();
|
||||
|
||||
SubmarineInfo prevSub = null;
|
||||
|
||||
foreach (SubmarineInfo sub in sortedSubs)
|
||||
{
|
||||
if (prevSub == null || prevSub.Type != sub.Type)
|
||||
@@ -3862,34 +3795,25 @@ namespace Barotrauma
|
||||
prevSub = sub;
|
||||
}
|
||||
|
||||
string displayPath = sub.FilePath;
|
||||
if (sub.IsFromRemoteStorage)
|
||||
string pathWithoutUserName = Path.GetFullPath(sub.FilePath);
|
||||
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
|
||||
if (pathWithoutUserName.StartsWith(saveFolder))
|
||||
{
|
||||
displayPath += $" {TextManager.Get("RemoteStorage")}";
|
||||
pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..];
|
||||
}
|
||||
else
|
||||
{
|
||||
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
|
||||
string fullPath = Path.GetFullPath(displayPath);
|
||||
if (fullPath.StartsWith(saveFolder))
|
||||
{
|
||||
displayPath = $"...{fullPath[saveFolder.Length..]}";
|
||||
}
|
||||
pathWithoutUserName = sub.FilePath;
|
||||
}
|
||||
|
||||
LocalizedString limitedName = 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)
|
||||
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) },
|
||||
ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80))
|
||||
{
|
||||
UserData = sub,
|
||||
ToolTip = displayPath
|
||||
ToolTip = pathWithoutUserName
|
||||
};
|
||||
|
||||
if (sub.IsFromRemoteStorage)
|
||||
{
|
||||
// remote storage
|
||||
textBlock.OverrideTextColor(RemoteStorageHelper.SteamColor);
|
||||
}
|
||||
else if (ContentPackageManager.VanillaCorePackage == null || ContentPackageManager.VanillaCorePackage.Files.None(f => f.Path == sub.FilePath))
|
||||
if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false))
|
||||
{
|
||||
if (GetLocalPackageThatOwnsSub(sub) == null &&
|
||||
ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage)
|
||||
@@ -4094,9 +4018,10 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subList.SelectedComponent?.UserData is not SubmarineInfo selectedSubInfo) { return false; }
|
||||
if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; }
|
||||
|
||||
if (!selectedSubInfo.IsFromRemoteStorage && GetLocalPackageThatOwnsSub(selectedSubInfo) is null)
|
||||
var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo);
|
||||
if (ownerPackage is null)
|
||||
{
|
||||
if (IsVanillaSub(selectedSubInfo))
|
||||
{
|
||||
@@ -4261,20 +4186,18 @@ namespace Barotrauma
|
||||
//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.
|
||||
//(deleting from the Submarines folder is also currently allowed, but this is temporary)
|
||||
ContentPackage subPackage = GetLocalPackageThatOwnsSub(sub);
|
||||
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { subPackage = null; }
|
||||
if (!sub.IsFromRemoteStorage && subPackage == null) { return; }
|
||||
var subPackage = GetLocalPackageThatOwnsSub(sub);
|
||||
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; }
|
||||
|
||||
GUIMessageBox msgBox = new(TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), [TextManager.Get("Yes"), TextManager.Get("Cancel")]);
|
||||
var msgBox = new GUIMessageBox(
|
||||
TextManager.Get("DeleteDialogLabel"),
|
||||
TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name),
|
||||
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
|
||||
msgBox.Buttons[0].OnClicked += (btn, userData) =>
|
||||
{
|
||||
if (sub.IsFromRemoteStorage)
|
||||
{
|
||||
RemoteStorageHelper.TryDelete(sub.FilePath);
|
||||
}
|
||||
else if (subPackage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (subPackage != null)
|
||||
{
|
||||
File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false);
|
||||
ModProject modProject = new ModProject(subPackage);
|
||||
@@ -4286,16 +4209,16 @@ namespace Barotrauma
|
||||
MainSub.Info.FilePath = null;
|
||||
}
|
||||
}
|
||||
sub.Dispose();
|
||||
CreateLoadScreen();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
|
||||
}
|
||||
}
|
||||
|
||||
sub.Dispose();
|
||||
CreateLoadScreen();
|
||||
return msgBox.Close(btn, userData);
|
||||
return true;
|
||||
};
|
||||
msgBox.Buttons[0].OnClicked += msgBox.Close;
|
||||
msgBox.Buttons[1].OnClicked += msgBox.Close;
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.NewMessage("Missing Localization for property: " + propertyTag);
|
||||
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
|
||||
@@ -467,7 +467,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (toolTip.IsNullOrEmpty())
|
||||
{
|
||||
toolTip = property.GetAttribute<Serialize>()?.Description;
|
||||
toolTip = property.GetAttribute<Serialize>().Description;
|
||||
}
|
||||
|
||||
GUIComponent propertyField = null;
|
||||
|
||||
@@ -503,14 +503,24 @@ namespace Barotrauma.Sounds
|
||||
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
|
||||
try
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
|
||||
lock (channelsLock)
|
||||
{
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this);
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
try
|
||||
{
|
||||
if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances)
|
||||
{
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
|
||||
}
|
||||
|
||||
if (ALSourceIndex >= 0)
|
||||
@@ -585,18 +595,18 @@ namespace Barotrauma.Sounds
|
||||
SetProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
}
|
||||
}
|
||||
#if !DEBUG
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
#if !DEBUG
|
||||
}
|
||||
#endif
|
||||
|
||||
void SetProperties()
|
||||
|
||||
@@ -417,6 +417,15 @@ namespace Barotrauma.Sounds
|
||||
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)
|
||||
{
|
||||
if (Disabled) { return -1; }
|
||||
@@ -427,6 +436,18 @@ namespace Barotrauma.Sounds
|
||||
|
||||
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++)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
|
||||
@@ -436,7 +457,6 @@ namespace Barotrauma.Sounds
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we couldn't get a free source to assign to this channel!
|
||||
return -1;
|
||||
@@ -476,13 +496,25 @@ namespace Barotrauma.Sounds
|
||||
int count = 0;
|
||||
lock (playingChannels[(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++; };
|
||||
count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex);
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#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>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
@@ -28,11 +28,14 @@ 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 } &&
|
||||
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
|
||||
causeOfDeath != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
|
||||
Client ownerClient = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
ownerClient.SpectateOnly = true;
|
||||
@@ -51,7 +54,7 @@ namespace Barotrauma
|
||||
|
||||
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
|
||||
{
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient);
|
||||
@@ -62,7 +65,7 @@ namespace Barotrauma
|
||||
|
||||
if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this)
|
||||
{
|
||||
var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
var owner = clients.FirstOrDefault(c => c.Character == this);
|
||||
if (owner != null)
|
||||
{
|
||||
if (!LuaCsSetup.Instance.Game.overrideTraitors)
|
||||
@@ -71,11 +74,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (client.InGame)
|
||||
{
|
||||
client.PendingPositionUpdates.Enqueue(this);
|
||||
client.TryEnqueuePositionUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,9 @@ namespace Barotrauma
|
||||
case ControlEventData controlEventData:
|
||||
Client owner = controlEventData.Owner;
|
||||
msg.WriteBoolean(owner == c && owner.Character == this);
|
||||
msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
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 });
|
||||
break;
|
||||
case CharacterStatusEventData statusEventData:
|
||||
@@ -742,7 +744,9 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
Client ownerClient = clients.FirstOrDefault(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
|
||||
if (ownerClient != null)
|
||||
{
|
||||
msg.WriteBoolean(true);
|
||||
|
||||
@@ -1799,35 +1799,22 @@ namespace Barotrauma
|
||||
(Client client, Vector2 cursorWorldPos, string[] args) =>
|
||||
{
|
||||
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))
|
||||
{
|
||||
submarineToTeleport.SetPosition(cursorWorldPos);
|
||||
Submarine.MainSub.SetPosition(cursorWorldPos);
|
||||
}
|
||||
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
submarineToTeleport.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
|
||||
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
|
||||
}
|
||||
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
submarineToTeleport.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
|
||||
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
|
||||
}
|
||||
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
|
||||
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
|
||||
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
|
||||
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
|
||||
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);
|
||||
|
||||
@@ -82,10 +82,13 @@ namespace Barotrauma
|
||||
|
||||
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 the action doesn't target anyone in specific, it's shown to every client
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (IsBlockedByAnotherConversation(client, duration)) { return true; }
|
||||
}
|
||||
@@ -95,7 +98,7 @@ namespace Barotrauma
|
||||
foreach (Entity e in targets)
|
||||
{
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; }
|
||||
}
|
||||
}
|
||||
@@ -117,13 +120,16 @@ namespace Barotrauma
|
||||
partial void ShowDialog(Character speaker, Character targetCharacter)
|
||||
{
|
||||
targetClients.Clear();
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
|
||||
foreach (Entity e in entities)
|
||||
{
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (targetClient != null)
|
||||
{
|
||||
targetClients.Add(targetClient);
|
||||
@@ -135,7 +141,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
foreach (Client c in clients)
|
||||
{
|
||||
if (CanClientReceive(c))
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ partial class EventLogAction : EventAction
|
||||
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
|
||||
{
|
||||
if (eventLog == null) { return; }
|
||||
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
List<Client> targetClients = new List<Client>();
|
||||
@@ -19,7 +23,7 @@ partial class EventLogAction : EventAction
|
||||
{
|
||||
if (target is Character character)
|
||||
{
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
targetClients.Add(ownerClient);
|
||||
@@ -38,7 +42,7 @@ partial class EventLogAction : EventAction
|
||||
}
|
||||
else
|
||||
{
|
||||
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog)
|
||||
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, clients) && ShowInServerLog)
|
||||
{
|
||||
Log(targetClients: null);
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,3 +1,5 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EventObjectiveAction : EventAction
|
||||
@@ -13,9 +15,12 @@ namespace Barotrauma
|
||||
ParentObjectiveId,
|
||||
CanBeCompleted);
|
||||
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
|
||||
if (TargetTag.IsEmpty)
|
||||
{
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (client.Character == null) { continue; }
|
||||
EventManager.ServerWriteObjective(client, objective);
|
||||
@@ -26,7 +31,7 @@ namespace Barotrauma
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (target is not Character character) { continue; }
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
|
||||
if (ownerClient == null) { continue; }
|
||||
EventManager.ServerWriteObjective(ownerClient, objective);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ partial class HighlightAction : EventAction
|
||||
IEnumerable<Client>? targetClients = null;
|
||||
if (targetCharacters != null)
|
||||
{
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
targetClients = targetCharacters
|
||||
.Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c))
|
||||
.Select(c => clients.FirstOrDefault(client => client.Character == c))
|
||||
.Where(c => c != null)!;
|
||||
}
|
||||
GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -22,7 +23,9 @@ namespace Barotrauma
|
||||
|
||||
private static void NotifyMissionUnlock(Mission mission)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
NotifyMissionUnlock(mission, client);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Barotrauma
|
||||
byte selectedOption = inc.ReadByte();
|
||||
bool isIgnore = selectedOption == byte.MaxValue;
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
foreach (Event ev in _activeEvents)
|
||||
{
|
||||
if (ev is not ScriptedEvent scriptedEvent) { continue; }
|
||||
|
||||
|
||||
@@ -1179,7 +1179,6 @@ namespace Barotrauma
|
||||
NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg);
|
||||
|
||||
if (GameMain.Server is null) { return; }
|
||||
if (transfer.Amount <= 0) { return; }
|
||||
|
||||
if (transfer.Sender.TryUnwrap(out var id))
|
||||
{
|
||||
|
||||
@@ -76,7 +76,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
var (msg, deliveryMethod) = PrepareToSend(opcode, data);
|
||||
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (predicate is not null && !predicate(client)) { continue; }
|
||||
|
||||
|
||||
@@ -193,6 +193,13 @@ namespace Barotrauma
|
||||
(item.PreviousParentInventory == null ||
|
||||
!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
|
||||
if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied)
|
||||
{
|
||||
@@ -204,15 +211,15 @@ namespace Barotrauma
|
||||
#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);
|
||||
#endif
|
||||
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
|
||||
if (item.body != null)
|
||||
{
|
||||
sender.PendingPositionUpdates.Enqueue(item);
|
||||
sender.TryEnqueuePositionUpdate(item);
|
||||
}
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
TryPutItem(item, slotIndex, allowSwapping: false, allowCombine: false, user: sender.Character, createNetworkEvent: false);
|
||||
TryPutItem(item, slotIndex, true, true, sender.Character, false);
|
||||
for (int j = 0; j < capacity; j++)
|
||||
{
|
||||
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace Barotrauma
|
||||
|
||||
//don't create updates if all clients are very far from the hull
|
||||
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
|
||||
if (!GameMain.Server.ConnectedClients.Any(c =>
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
if (!clients.Any(c =>
|
||||
(c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) ||
|
||||
(c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) )
|
||||
{
|
||||
|
||||
@@ -8,17 +8,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
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)
|
||||
{
|
||||
c.KickAFKTimer = 0.0f;
|
||||
@@ -80,7 +69,8 @@ namespace Barotrauma.Networking
|
||||
txt = msg.ReadString() ?? "";
|
||||
}
|
||||
|
||||
txt = SanitizeText(c, txt);
|
||||
// Sanitize incoming text message from client so they can't use RichString features
|
||||
txt = txt.Replace('‖', ' ');
|
||||
|
||||
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }
|
||||
|
||||
|
||||
@@ -64,6 +64,32 @@ namespace Barotrauma.Networking
|
||||
// key = entity, value = NetTime.Now when sending
|
||||
public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
|
||||
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;
|
||||
|
||||
@@ -126,7 +152,7 @@ namespace Barotrauma.Networking
|
||||
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
|
||||
{
|
||||
syncedKarma = karma;
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +379,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
NeedsMidRoundSync = false;
|
||||
PendingPositionUpdates.Clear();
|
||||
pendingPositionUpdatesSet.Clear();
|
||||
EntityEventLastSent.Clear();
|
||||
LastSentEntityEventID = 0;
|
||||
LastRecvEntityEventID = 0;
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Barotrauma.Networking
|
||||
StartTime = DateTime.Now;
|
||||
|
||||
OnStarted(transfer);
|
||||
GameMain.Server.LastClientListUpdateID++;
|
||||
GameMain.Server.IncrementLastClientListUpdateID();
|
||||
|
||||
return transfer;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (numRemoved > 0 || endedTransfers.Count > 0)
|
||||
{
|
||||
GameMain.Server.LastClientListUpdateID++;
|
||||
GameMain.Server.IncrementLastClientListUpdateID();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
|
||||
if (newClient.Connection == OwnerConnection && OwnerConnection != null)
|
||||
{
|
||||
@@ -742,11 +742,6 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
|
||||
}
|
||||
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,10 +899,7 @@ namespace Barotrauma.Networking
|
||||
string subHash = inc.ReadString();
|
||||
CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
|
||||
|
||||
var matchingSub =
|
||||
ServerSettings.AllowSubVoting ?
|
||||
Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, connectedClients) :
|
||||
SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
|
||||
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
|
||||
|
||||
if (GameStarted)
|
||||
{
|
||||
@@ -2148,7 +2140,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (lastSent > NetTime.Now - updateInterval) { continue; }
|
||||
}
|
||||
if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
|
||||
c.TryEnqueuePositionUpdate(otherCharacter);
|
||||
}
|
||||
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
@@ -2157,7 +2149,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)
|
||||
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 (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
|
||||
c.TryEnqueuePositionUpdate(sub);
|
||||
}
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
@@ -2174,7 +2166,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (lastSent > NetTime.Now - updateInterval) { continue; }
|
||||
}
|
||||
if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
|
||||
c.TryEnqueuePositionUpdate(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2218,7 +2210,7 @@ namespace Barotrauma.Networking
|
||||
entity.Removed ||
|
||||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
|
||||
{
|
||||
c.PendingPositionUpdates.Dequeue();
|
||||
c.DequeuePositionUpdate();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2240,7 +2232,7 @@ namespace Barotrauma.Networking
|
||||
outmsg.WritePadBits();
|
||||
|
||||
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
|
||||
c.PendingPositionUpdates.Dequeue();
|
||||
c.DequeuePositionUpdate();
|
||||
}
|
||||
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
|
||||
|
||||
@@ -3215,7 +3207,7 @@ namespace Barotrauma.Networking
|
||||
initiatedStartGame = false;
|
||||
GameMain.ResetFrameTime();
|
||||
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
|
||||
roundStartTime = DateTime.Now;
|
||||
|
||||
@@ -3514,7 +3506,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
|
||||
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
//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
|
||||
c.NameId++;
|
||||
@@ -3528,7 +3520,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
@@ -3545,14 +3537,14 @@ namespace Barotrauma.Networking
|
||||
c.Name = newName;
|
||||
c.RejectedName = string.Empty;
|
||||
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//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
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4791,7 +4783,9 @@ namespace Barotrauma.Networking
|
||||
public static string CharacterLogName(Character character)
|
||||
{
|
||||
if (character == null) { return "[NULL]"; }
|
||||
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
// Create snapshot to avoid concurrent access issues during parallel updates
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
Client client = clients.FirstOrDefault(c => c.Character == character);
|
||||
return ClientLogName(client, character.LogName);
|
||||
}
|
||||
|
||||
@@ -4802,8 +4796,8 @@ namespace Barotrauma.Networking
|
||||
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(line, messageType));
|
||||
|
||||
GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType);
|
||||
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
var clients = GameMain.Server.ConnectedClients.ToArray();
|
||||
foreach (Client client in clients)
|
||||
{
|
||||
if (!client.HasPermission(ClientPermissions.ServerLog)) continue;
|
||||
//use sendername as the message type
|
||||
@@ -4844,7 +4838,7 @@ namespace Barotrauma.Networking
|
||||
private void UpdateClientLobbies()
|
||||
{
|
||||
// Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
|
||||
LastClientListUpdateID++;
|
||||
IncrementLastClientListUpdateID();
|
||||
}
|
||||
|
||||
private List<Client> GetPlayingClients()
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace Barotrauma
|
||||
{
|
||||
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);
|
||||
GameMain.NetworkMember.LastClientListUpdateID++;
|
||||
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
||||
}
|
||||
else if (existingAffliction != null)
|
||||
{
|
||||
|
||||
+1
-1
@@ -373,7 +373,7 @@ namespace Barotrauma.Networking
|
||||
// in which case we'll wait until the timeout runs out before kicking the client
|
||||
List<Client> toKick = inGameClients.FindAll(c =>
|
||||
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
|
||||
(!c.NeedsMidRoundSync || firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
|
||||
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
|
||||
toKick.ForEach(c =>
|
||||
{
|
||||
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);
|
||||
shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g));
|
||||
|
||||
var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null);
|
||||
var dockingPorts = Item.ItemList.Where(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null).ToList();
|
||||
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
|
||||
|
||||
if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime)
|
||||
|
||||
@@ -54,14 +54,13 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (recipient == sender) { continue; }
|
||||
|
||||
if (!CanReceive(sender, recipient, out float distanceFactor, out bool isRadio)) { continue; }
|
||||
if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; }
|
||||
|
||||
IWriteMessage msg = new WriteOnlyMessage();
|
||||
|
||||
msg.WriteByte((byte)ServerPacketHeader.VOICE);
|
||||
msg.WriteByte((byte)queue.QueueID);
|
||||
msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8);
|
||||
msg.WriteBoolean(isRadio);
|
||||
queue.Write(msg);
|
||||
|
||||
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
|
||||
@@ -69,17 +68,15 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor, out bool isRadio)
|
||||
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor)
|
||||
{
|
||||
if (Screen.Selected != GameMain.GameScreen)
|
||||
{
|
||||
distanceFactor = 0.0f;
|
||||
isRadio = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
distanceFactor = 0.0f;
|
||||
isRadio = false;
|
||||
|
||||
//no-one can hear muted players
|
||||
if (sender.Muted) { return false; }
|
||||
@@ -112,14 +109,12 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (recipientSpectating)
|
||||
{
|
||||
isRadio = true;
|
||||
if (recipient.SpectatePos == null) { return true; }
|
||||
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f);
|
||||
return distanceFactor < 1.0f;
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace Barotrauma
|
||||
private static bool hasShutDown = false;
|
||||
private static void ShutDown()
|
||||
{
|
||||
SingleThreadWorker.Instance.Dispose();
|
||||
if (hasShutDown) { return; }
|
||||
hasShutDown = true;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.13.3.1</Version>
|
||||
<Version>1.12.7.0</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
BIN
Binary file not shown.
-64
@@ -23,70 +23,6 @@
|
||||
</Holdable>
|
||||
</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">
|
||||
<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">
|
||||
|
||||
@@ -156,7 +156,8 @@ namespace Barotrauma
|
||||
Reactor reactor = item.GetComponent<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();
|
||||
|
||||
#if CLIENT
|
||||
@@ -323,7 +324,7 @@ namespace Barotrauma
|
||||
|
||||
static CachedDistance CalculateNewCachedDistance(Character c)
|
||||
{
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
|
||||
if (path.Unreachable) { return null; }
|
||||
return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
|
||||
@@ -524,7 +525,8 @@ namespace Barotrauma
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
|
||||
}
|
||||
else if (item.Prefab.Tags.Contains("nuclearexplosive"))
|
||||
else if (item.Prefab.Identifier == "nuclearshell" ||
|
||||
item.Prefab.Identifier == "nucleardepthcharge")
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,67 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
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
|
||||
{
|
||||
public static List<AITarget> List = new List<AITarget>();
|
||||
public static ThreadSafeAITargetList List = new ThreadSafeAITargetList();
|
||||
|
||||
private Entity entity;
|
||||
public Entity Entity
|
||||
|
||||
@@ -197,18 +197,6 @@ namespace Barotrauma
|
||||
|
||||
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 SwarmBehavior SwarmBehavior { get; private set; }
|
||||
public PetBehavior PetBehavior { get; private set; }
|
||||
@@ -2691,19 +2679,11 @@ namespace Barotrauma
|
||||
if (!ActiveAttack.IsRunning)
|
||||
{
|
||||
#if SERVER
|
||||
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
|
||||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
|
||||
AttackLimb,
|
||||
damageTarget,
|
||||
targetLimb,
|
||||
SimPosition));
|
||||
lastSetAttackTargetEventTime = Timing.TotalTime;
|
||||
lastDamageTarget = damageTarget;
|
||||
lastAttackLimb = AttackLimb;
|
||||
lastTargetLimb = targetLimb;
|
||||
}
|
||||
#else
|
||||
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -46,7 +47,7 @@ namespace Barotrauma
|
||||
|
||||
private float respondToAttackTimer;
|
||||
private const float RespondToAttackInterval = 1.0f;
|
||||
private bool wasDead;
|
||||
private bool wasConscious;
|
||||
|
||||
private bool freezeAI;
|
||||
|
||||
@@ -201,7 +202,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (isIncapacitated) { return; }
|
||||
|
||||
wasDead = false;
|
||||
wasConscious = true;
|
||||
|
||||
respondToAttackTimer -= deltaTime;
|
||||
if (respondToAttackTimer <= 0.0f)
|
||||
@@ -381,8 +382,13 @@ namespace Barotrauma
|
||||
}
|
||||
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;
|
||||
CheckCrouching(deltaTime);
|
||||
}
|
||||
|
||||
Character.ClearInputs();
|
||||
|
||||
if (SortTimer > 0.0f)
|
||||
@@ -1256,15 +1262,14 @@ namespace Barotrauma
|
||||
|
||||
public override void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
// If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
|
||||
// (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)
|
||||
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
|
||||
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
|
||||
{
|
||||
RespondToAttack(attacker, attackResult);
|
||||
wasDead = Character.IsDead;
|
||||
wasConscious = false;
|
||||
return;
|
||||
}
|
||||
if (Character.IsDead) { return; }
|
||||
if (attacker == null || Character.IsPlayer)
|
||||
{
|
||||
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
|
||||
@@ -1469,9 +1474,9 @@ namespace Barotrauma
|
||||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
|
||||
{
|
||||
// Knocked down or in different team -> cannot report.
|
||||
// Dead or in different team -> cannot report.
|
||||
continue;
|
||||
}
|
||||
if (otherHumanAI.objectiveManager.HasOrders())
|
||||
@@ -1495,14 +1500,6 @@ namespace Barotrauma
|
||||
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);
|
||||
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
|
||||
}
|
||||
@@ -1646,7 +1643,8 @@ namespace Barotrauma
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
if (!Character.IsBot) { return; }
|
||||
if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
|
||||
List<AIObjective> ObjectivesLocal = ObjectiveManager.Objectives;
|
||||
if (ObjectivesLocal.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
|
||||
{
|
||||
// Don't replace offensive mode with something else
|
||||
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
|
||||
@@ -1826,7 +1824,9 @@ namespace Barotrauma
|
||||
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);
|
||||
|
||||
private static List<Item> matchingItems = new List<Item>();
|
||||
// ThreadLocal to ensure thread safety - each thread gets its own list instance
|
||||
private static readonly ThreadLocal<List<Item>> matchingItemsLocal = new ThreadLocal<List<Item>>(() => new List<Item>());
|
||||
private static List<Item> matchingItems => matchingItemsLocal.Value;
|
||||
|
||||
/// <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.
|
||||
@@ -1834,15 +1834,16 @@ namespace Barotrauma
|
||||
/// </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)
|
||||
{
|
||||
matchingItems.Clear();
|
||||
items = matchingItems;
|
||||
var localMatchingItems = matchingItems;
|
||||
localMatchingItems.Clear();
|
||||
items = localMatchingItems;
|
||||
if (character?.Inventory == null) { return false; }
|
||||
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
||||
character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
||||
i.ConditionPercentage >= conditionPercentage &&
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
||||
items = matchingItems;
|
||||
foreach (var item in matchingItems)
|
||||
(predicate == null || predicate(i)), recursive, localMatchingItems);
|
||||
items = localMatchingItems;
|
||||
foreach (var item in localMatchingItems)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
|
||||
@@ -1935,12 +1936,12 @@ namespace Barotrauma
|
||||
character.IsCriminal = true;
|
||||
character.IsActingOffensively = true;
|
||||
}
|
||||
if (!TriggerSecurity(otherHumanAI, character, combatMode))
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
// Else call the others
|
||||
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, character, combatMode))
|
||||
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
|
||||
{
|
||||
// Only alert one guard at a time
|
||||
return;
|
||||
@@ -1950,13 +1951,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
|
||||
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
|
||||
{
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
@@ -1964,12 +1964,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(targetCharacter);
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ItemTaken(Item item, Character thief)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.IO;
|
||||
using System.Linq;
|
||||
@@ -9,7 +10,8 @@ namespace Barotrauma
|
||||
{
|
||||
class NPCConversationCollection : Prefab
|
||||
{
|
||||
public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
|
||||
// Thread-safe dictionary for language-based collections
|
||||
public static readonly ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
|
||||
|
||||
public readonly LanguageIdentifier Language;
|
||||
|
||||
@@ -160,7 +162,24 @@ namespace Barotrauma
|
||||
return currentFlags;
|
||||
}
|
||||
|
||||
private static readonly List<NPCConversation> previousConversations = new List<NPCConversation>();
|
||||
// Thread-safe previous conversations tracking using copy-on-write pattern
|
||||
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)
|
||||
{
|
||||
@@ -281,8 +300,7 @@ namespace Barotrauma
|
||||
|
||||
if (baseConversation == null)
|
||||
{
|
||||
previousConversations.Insert(0, selectedConversation);
|
||||
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
|
||||
AddToPreviousConversations(selectedConversation);
|
||||
}
|
||||
lineList.Add((speaker, selectedConversation.Line));
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ namespace Barotrauma
|
||||
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
|
||||
if (item.IgnoreByAI(character) || Item.IsMarkedForDeconstruction(item))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ namespace Barotrauma
|
||||
if (!allowUnloading) { return false; }
|
||||
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
|
||||
}
|
||||
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
|
||||
if (ignoreItemsMarkedForDeconstruction && Item.IsMarkedForDeconstruction(item)) { return false; }
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
if (item.HasBallastFloraInHull) { return false; }
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ namespace Barotrauma
|
||||
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
|
||||
if (operateExtinguisher)
|
||||
{
|
||||
character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
|
||||
character.CursorPosition = fs.Position;
|
||||
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
|
||||
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
|
||||
if (extinguisherItem.RequireAimToUse)
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -68,8 +69,9 @@ namespace Barotrauma
|
||||
|
||||
/// <summary>
|
||||
/// When did the character last inspect whether some other character has stolen items on them?
|
||||
/// Thread-safe dictionary for concurrent access.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
|
||||
private static readonly ConcurrentDictionary<Character, double> lastInspectionTimes = new ConcurrentDictionary<Character, double>();
|
||||
|
||||
private const float NormalInspectionInterval = 120.0f;
|
||||
private const float CriminalInspectionInterval = 30.0f;
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Objectives.RemoveAll(o => o.GetType() == type);
|
||||
Objectives.RemoveAll(o => o?.GetType() == type);
|
||||
}
|
||||
Objectives.Add(objective);
|
||||
}
|
||||
|
||||
-10
@@ -173,16 +173,6 @@ namespace Barotrauma
|
||||
if (character.CanInteractWith(Item, out _, checkLinked: false))
|
||||
{
|
||||
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; }
|
||||
|
||||
HumanAIController.FaceTarget(Item);
|
||||
|
||||
@@ -440,7 +440,7 @@ namespace Barotrauma
|
||||
|
||||
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
|
||||
{
|
||||
if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
|
||||
if (item.AllowDeconstruct && !Item.IsMarkedForDeconstruction(item) &&
|
||||
//only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that
|
||||
(item.Prefab.DeconstructItems.None() ||
|
||||
item.Prefab.DeconstructItems.Any(deconstructItem =>
|
||||
@@ -454,7 +454,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (Identifier == Tags.DontDeconstructThis)
|
||||
{
|
||||
if (Item.DeconstructItems.Contains(item)) { return true; }
|
||||
if (Item.IsMarkedForDeconstruction(item)) { return true; }
|
||||
}
|
||||
|
||||
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
|
||||
|
||||
@@ -8,8 +8,10 @@ using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using JointParams = Barotrauma.RagdollParams.JointParams;
|
||||
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
||||
@@ -26,7 +28,33 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
const float MaxImpactDamage = 0.1f;
|
||||
|
||||
private static readonly List<Ragdoll> list = new List<Ragdoll>();
|
||||
// Thread-safe list using copy-on-write pattern (ConcurrentBag doesn't support indexer/Remove)
|
||||
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
|
||||
{
|
||||
@@ -47,7 +75,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
||||
// Thread-safe queue for physics collision callbacks
|
||||
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
|
||||
|
||||
protected Hull currentHull;
|
||||
|
||||
@@ -469,7 +498,7 @@ namespace Barotrauma
|
||||
|
||||
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
|
||||
{
|
||||
list.Add(this);
|
||||
ListAdd(this);
|
||||
this.character = character;
|
||||
Recreate(ragdollParams ?? RagdollParams);
|
||||
}
|
||||
@@ -745,12 +774,9 @@ namespace Barotrauma
|
||||
if (f2.Body.UserData is not Structure structure)
|
||||
{
|
||||
if (!f2.IsSensor)
|
||||
{
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine)
|
||||
@@ -821,10 +847,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
lock (impactQueue)
|
||||
{
|
||||
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1291,10 +1314,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
|
||||
|
||||
while (impactQueue.Count > 0)
|
||||
while (impactQueue.TryDequeue(out var impact))
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity);
|
||||
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
|
||||
}
|
||||
|
||||
CheckValidity();
|
||||
@@ -2368,7 +2390,7 @@ namespace Barotrauma
|
||||
LimbJoints = null;
|
||||
}
|
||||
|
||||
list.Remove(this);
|
||||
ListRemove(this);
|
||||
}
|
||||
|
||||
public static void RemoveAll()
|
||||
|
||||
@@ -758,11 +758,6 @@ namespace Barotrauma
|
||||
public float CoolDownTimer { get; set; }
|
||||
public float CurrentRandomCoolDown { get; private 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 float AfterAttackTimer { get; set; }
|
||||
|
||||
@@ -7,10 +7,12 @@ using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
#if SERVER
|
||||
using System.Text;
|
||||
@@ -28,12 +30,70 @@ namespace Barotrauma
|
||||
|
||||
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
|
||||
{
|
||||
public static readonly List<Character> CharacterList = new List<Character>();
|
||||
public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList();
|
||||
|
||||
public static int CharacterUpdateInterval = 1;
|
||||
private static int characterUpdateTick = 1;
|
||||
private static volatile int characterUpdateTick = 1;
|
||||
|
||||
public const float MaxHighlightDistance = 150.0f;
|
||||
public const float MaxDragDistance = 200.0f;
|
||||
@@ -997,9 +1057,6 @@ namespace Barotrauma
|
||||
public bool IsForceRagdolled;
|
||||
public bool FollowCursor = true;
|
||||
|
||||
/// <summary>
|
||||
/// Is the character currently dead, unconscious or paralyzed?
|
||||
/// </summary>
|
||||
public bool IsIncapacitated
|
||||
{
|
||||
get
|
||||
@@ -1009,9 +1066,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the character dead or below 0 vitality and not able to stay conscious?
|
||||
/// </summary>
|
||||
public bool IsUnconscious
|
||||
{
|
||||
get { return CharacterHealth.IsUnconscious; }
|
||||
@@ -1679,8 +1733,7 @@ namespace Barotrauma
|
||||
AnimController.FindHull(setInWater: true);
|
||||
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
|
||||
|
||||
//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);
|
||||
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
|
||||
|
||||
CharacterList.Add(this);
|
||||
|
||||
@@ -2269,11 +2322,8 @@ namespace Barotrauma
|
||||
{
|
||||
Vector2 targetMovement = GetTargetMovement();
|
||||
AnimController.TargetMovement = targetMovement;
|
||||
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
|
||||
{
|
||||
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
if (AnimController is HumanoidAnimController humanAnimController)
|
||||
{
|
||||
@@ -2803,10 +2853,11 @@ namespace Barotrauma
|
||||
}
|
||||
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
|
||||
int checkedItemCount = 0;
|
||||
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
|
||||
var cachedItems = Item.GetCachedItemList();
|
||||
for (int i = 0; i < itemsPerFrame && itemIndex < cachedItems.Count; i++, itemIndex++)
|
||||
{
|
||||
checkedItemCount++;
|
||||
var item = Item.ItemList[itemIndex];
|
||||
var item = cachedItems[itemIndex];
|
||||
if (!item.IsInteractable(this)) { continue; }
|
||||
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
||||
if (item.Submarine == null) { continue; }
|
||||
@@ -2842,10 +2893,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
targetItem = _foundItem;
|
||||
bool completed = itemIndex >= Item.ItemList.Count - 1;
|
||||
bool completed = itemIndex >= cachedItems.Count - 1;
|
||||
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
|
||||
{
|
||||
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
|
||||
var msg = $"Went through {checkedItemCount} of total {cachedItems.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
|
||||
if (StopWatch.ElapsedMilliseconds > 5)
|
||||
{
|
||||
DebugConsole.ThrowError(msg);
|
||||
@@ -3518,11 +3569,9 @@ namespace Barotrauma
|
||||
|
||||
UpdateAttackers(deltaTime);
|
||||
|
||||
//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++)
|
||||
foreach (var characterTalent in characterTalents)
|
||||
{
|
||||
characterTalents[i].UpdateTalent(deltaTime);
|
||||
characterTalent.UpdateTalent(deltaTime);
|
||||
}
|
||||
|
||||
if (IsDead) { return; }
|
||||
@@ -4922,7 +4971,11 @@ namespace Barotrauma
|
||||
HealthUpdateInterval = 0.0f;
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<ISerializableEntity> t_statusEffectTargets;
|
||||
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
|
||||
|
||||
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
||||
{
|
||||
if (actionType == ActionType.OnEating)
|
||||
@@ -4951,6 +5004,7 @@ namespace Barotrauma
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = StatusEffectTargets;
|
||||
targets.Clear();
|
||||
statusEffect.AddNearbyTargets(WorldPosition, targets);
|
||||
statusEffect.Apply(actionType, deltaTime, this, targets);
|
||||
@@ -5813,12 +5867,6 @@ namespace Barotrauma
|
||||
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()
|
||||
{
|
||||
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
|
||||
|
||||
@@ -8,12 +8,14 @@ using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using static OneOf.Types.TrueFalseOrNull;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -134,8 +136,9 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
|
||||
|
||||
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
|
||||
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
|
||||
// Thread-safe afflictions dictionary for concurrent access
|
||||
private readonly ConcurrentDictionary<Affliction, LimbHealth> afflictions = new ConcurrentDictionary<Affliction, LimbHealth>();
|
||||
private readonly ConcurrentDictionary<Affliction, byte> irremovableAfflictions = new ConcurrentDictionary<Affliction, byte>();
|
||||
private Affliction bloodlossAffliction;
|
||||
private Affliction oxygenLowAffliction;
|
||||
private Affliction pressureAffliction;
|
||||
@@ -326,13 +329,13 @@ namespace Barotrauma
|
||||
|
||||
private void InitIrremovableAfflictions()
|
||||
{
|
||||
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
|
||||
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
|
||||
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
|
||||
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
|
||||
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
|
||||
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||
{
|
||||
afflictions.Add(affliction, null);
|
||||
afflictions.TryAdd(affliction, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +343,7 @@ namespace Barotrauma
|
||||
|
||||
public IReadOnlyCollection<Affliction> GetAllAfflictions()
|
||||
{
|
||||
return afflictions.Keys;
|
||||
return afflictions.Keys.ToList();
|
||||
}
|
||||
|
||||
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
|
||||
@@ -505,7 +508,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
||||
{
|
||||
lock (afflictions) {
|
||||
// ConcurrentDictionary is thread-safe, no lock needed
|
||||
// This is a % resistance (0 to 1.0)
|
||||
float resistance = 0.0f;
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
@@ -518,7 +521,6 @@ namespace Barotrauma
|
||||
// The returned value is calculated to be a % resistance again
|
||||
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
public float GetStatValue(StatTypes statType)
|
||||
{
|
||||
@@ -541,20 +543,25 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[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)
|
||||
{
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
matchingAfflictions.AddRange(afflictions.Keys);
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
|
||||
}
|
||||
|
||||
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
||||
{
|
||||
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
foreach (var affliction in afflictions)
|
||||
{
|
||||
@@ -564,7 +571,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
|
||||
}
|
||||
|
||||
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
|
||||
@@ -574,10 +581,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
|
||||
|
||||
ReduceMatchingAfflictions(amount, treatmentAction);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
|
||||
}
|
||||
|
||||
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
||||
@@ -585,6 +593,7 @@ namespace Barotrauma
|
||||
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
||||
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
||||
|
||||
var matchingAfflictions = MatchingAfflictions;
|
||||
matchingAfflictions.Clear();
|
||||
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
|
||||
foreach (var affliction in afflictions)
|
||||
@@ -595,10 +604,10 @@ namespace Barotrauma
|
||||
matchingAfflictions.Add(affliction.Key);
|
||||
}
|
||||
}
|
||||
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
||||
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
|
||||
}
|
||||
|
||||
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
|
||||
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null)
|
||||
{
|
||||
if (matchingAfflictions.Count == 0) { return; }
|
||||
|
||||
@@ -686,12 +695,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
|
||||
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
|
||||
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel
|
||||
[ThreadStatic]
|
||||
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)
|
||||
{
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
|
||||
@@ -699,14 +715,14 @@ namespace Barotrauma
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||
}
|
||||
|
||||
RecalculateVitality();
|
||||
@@ -742,26 +758,28 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveAfflictions(Func<Affliction, bool> predicate)
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
public void RemoveAllAfflictions()
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
affliction.Strength = 0.0f;
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||
{
|
||||
affliction.Strength = 0.0f;
|
||||
}
|
||||
@@ -770,17 +788,18 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveNegativeAfflictions()
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
||||
!irremovableAfflictions.Contains(a) &&
|
||||
!irremovableAfflictions.ContainsKey(a) &&
|
||||
!a.Prefab.IsBuff &&
|
||||
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
|
||||
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
foreach (Affliction affliction in irremovableAfflictions.Keys)
|
||||
{
|
||||
affliction.Strength = 0.0f;
|
||||
}
|
||||
@@ -883,7 +902,7 @@ namespace Barotrauma
|
||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
|
||||
newAffliction.Source);
|
||||
afflictions.Add(copyAffliction, limbHealth);
|
||||
afflictions.TryAdd(copyAffliction, limbHealth);
|
||||
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
||||
MedicalClinic.OnAfflictionCountChanged(Character);
|
||||
|
||||
@@ -920,6 +939,8 @@ namespace Barotrauma
|
||||
|
||||
if (!Character.GodMode)
|
||||
{
|
||||
var afflictionsToRemove = AfflictionsToRemove;
|
||||
var afflictionsToUpdate = AfflictionsToUpdate;
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
@@ -928,7 +949,7 @@ namespace Barotrauma
|
||||
if (affliction.Strength <= 0.0f)
|
||||
{
|
||||
AchievementManager.OnAfflictionRemoved(affliction, Character);
|
||||
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
|
||||
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
|
||||
continue;
|
||||
}
|
||||
if (affliction.Prefab.Duration > 0.0f)
|
||||
@@ -966,7 +987,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
afflictions.TryRemove(affliction, out _);
|
||||
}
|
||||
|
||||
if (afflictionsToRemove.Count is not 0)
|
||||
@@ -1214,9 +1235,14 @@ namespace Barotrauma
|
||||
return (causeOfDeath, strongestAffliction);
|
||||
}
|
||||
|
||||
private readonly List<Affliction> allAfflictions = new List<Affliction>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[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)
|
||||
{
|
||||
var allAfflictions = AllAfflictionsList;
|
||||
allAfflictions.Clear();
|
||||
if (!mergeSameAfflictions)
|
||||
{
|
||||
@@ -1399,10 +1425,17 @@ namespace Barotrauma
|
||||
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
|
||||
}
|
||||
|
||||
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
|
||||
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
|
||||
// Thread-static to avoid concurrent modification in parallel updates
|
||||
[ThreadStatic]
|
||||
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)
|
||||
{
|
||||
var activeAfflictions = ActiveAfflictionsList;
|
||||
activeAfflictions.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
@@ -1428,6 +1461,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var limbAfflictions = LimbAfflictionsList;
|
||||
limbAfflictions.Clear();
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
@@ -1457,8 +1491,9 @@ namespace Barotrauma
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
// Clear thread-static lists to help with garbage collection
|
||||
AfflictionsToRemove.Clear();
|
||||
AfflictionsToUpdate.Clear();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
@@ -1533,14 +1568,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
|
||||
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
|
||||
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
|
||||
var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab);
|
||||
if (irremovableAffliction != null)
|
||||
{
|
||||
irremovableAffliction.Strength = strength;
|
||||
}
|
||||
else
|
||||
{
|
||||
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
|
||||
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,16 +797,14 @@ namespace Barotrauma
|
||||
return AddDamage(simPosition, afflictions, playSound);
|
||||
}
|
||||
|
||||
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
|
||||
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
|
||||
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
|
||||
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification
|
||||
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
|
||||
{
|
||||
appliedDamageModifiers.Clear();
|
||||
afflictionsCopy.Clear();
|
||||
var appliedDamageModifiers = new List<DamageModifier>();
|
||||
var afflictionsCopy = new List<Affliction>();
|
||||
foreach (var affliction in afflictions)
|
||||
{
|
||||
tempModifiers.Clear();
|
||||
var tempModifiers = new List<DamageModifier>();
|
||||
var newAffliction = affliction;
|
||||
float random = Rand.Value(Rand.RandSync.Unsynced);
|
||||
bool foundMatchingModifier = false;
|
||||
@@ -1022,22 +1020,18 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
#if SERVER
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// 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
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<Body> t_contactBodies;
|
||||
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>();
|
||||
|
||||
private readonly List<Body> contactBodies = new List<Body>();
|
||||
/// <summary>
|
||||
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
|
||||
/// </summary>
|
||||
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
|
||||
{
|
||||
attackResult = default;
|
||||
var contactBodies = ContactBodies;
|
||||
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
@@ -1151,13 +1145,9 @@ namespace Barotrauma
|
||||
ExecuteAttack(damageTarget, targetLimb, out attackResult);
|
||||
}
|
||||
#if SERVER
|
||||
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
|
||||
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
|
||||
targetSimPos: attackSimPos));
|
||||
lastExecuteAttackEventTime = Timing.TotalTime;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1300,7 +1290,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
// Thread-static to avoid concurrent modification in parallel item updates
|
||||
[ThreadStatic]
|
||||
private static List<ISerializableEntity> t_statusEffectTargets;
|
||||
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
|
||||
|
||||
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
||||
{
|
||||
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
|
||||
@@ -1323,6 +1317,7 @@ namespace Barotrauma
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = StatusEffectTargets;
|
||||
targets.Clear();
|
||||
statusEffect.AddNearbyTargets(WorldPosition, targets);
|
||||
statusEffect.Apply(actionType, deltaTime, character, targets);
|
||||
|
||||
+14
-20
@@ -1,10 +1,12 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Barotrauma.IO;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
@@ -117,8 +119,9 @@ namespace Barotrauma
|
||||
public virtual AnimationType AnimationType { get; protected set; }
|
||||
/// <summary>
|
||||
/// The cached animations of all the characters that have been loaded.
|
||||
/// Thread-safe cache using ConcurrentDictionary.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
|
||||
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>> allAnimations = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>>();
|
||||
|
||||
[Header("Movement")]
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
||||
@@ -244,7 +247,9 @@ namespace Barotrauma
|
||||
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
|
||||
}
|
||||
|
||||
private static readonly List<string> errorMessages = new List<string>();
|
||||
// ThreadLocal for thread-safe error message collection during animation loading
|
||||
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()
|
||||
{
|
||||
@@ -262,11 +267,7 @@ namespace Barotrauma
|
||||
}
|
||||
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
|
||||
Debug.Assert(contentPackage != null);
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
|
||||
{
|
||||
animations = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, animations);
|
||||
}
|
||||
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
|
||||
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
|
||||
{
|
||||
@@ -418,16 +419,12 @@ namespace Barotrauma
|
||||
{
|
||||
throw new Exception("Cannot create an animation file of type " + animationType);
|
||||
}
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
anims = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, anims);
|
||||
}
|
||||
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
||||
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
|
||||
if (anims.ContainsKey(fileName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
||||
anims.Remove(fileName);
|
||||
anims.TryRemove(fileName, out _);
|
||||
}
|
||||
var instance = new T();
|
||||
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
||||
@@ -439,7 +436,7 @@ namespace Barotrauma
|
||||
instance.IsLoaded = instance.Deserialize(animationElement);
|
||||
instance.Save();
|
||||
instance.Load(contentPath, speciesName);
|
||||
anims.Add(fileName, instance);
|
||||
anims.TryAdd(fileName, instance);
|
||||
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
||||
return instance;
|
||||
}
|
||||
@@ -467,17 +464,14 @@ namespace Barotrauma
|
||||
{
|
||||
// Update the key by removing and re-adding the animation.
|
||||
string fileName = FileNameWithoutExtension;
|
||||
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
|
||||
if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary<string, AnimationParams> animations))
|
||||
{
|
||||
animations.Remove(fileName);
|
||||
animations.TryRemove(fileName, out _);
|
||||
}
|
||||
base.UpdatePath(newPath);
|
||||
if (animations != null)
|
||||
{
|
||||
if (!animations.ContainsKey(fileName))
|
||||
{
|
||||
animations.Add(fileName, this);
|
||||
}
|
||||
animations.TryAdd(fileName, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-16
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Xml.Linq;
|
||||
@@ -124,8 +125,9 @@ namespace Barotrauma
|
||||
/// key1: Species name
|
||||
/// key2: File path
|
||||
/// value: Ragdoll parameters
|
||||
/// Thread-safe cache using ConcurrentDictionary.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
|
||||
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>> allRagdolls = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>>();
|
||||
|
||||
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
|
||||
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
|
||||
@@ -222,11 +224,7 @@ namespace Barotrauma
|
||||
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
|
||||
}
|
||||
Debug.Assert(contentPackage != null);
|
||||
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
|
||||
{
|
||||
ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
}
|
||||
var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, RagdollParams>());
|
||||
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
|
||||
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
|
||||
{
|
||||
@@ -331,10 +329,10 @@ namespace Barotrauma
|
||||
if (allRagdolls.ContainsKey(speciesName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
|
||||
allRagdolls.Remove(speciesName);
|
||||
allRagdolls.TryRemove(speciesName, out _);
|
||||
}
|
||||
var ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
var ragdolls = new ConcurrentDictionary<string, RagdollParams>();
|
||||
allRagdolls.TryAdd(speciesName, ragdolls);
|
||||
var instance = new T
|
||||
{
|
||||
doc = new XDocument(mainElement)
|
||||
@@ -345,7 +343,7 @@ namespace Barotrauma
|
||||
instance.IsLoaded = instance.Deserialize(mainElement);
|
||||
instance.Save();
|
||||
instance.Load(contentPath, speciesName);
|
||||
ragdolls.Add(instance.FileNameWithoutExtension, instance);
|
||||
ragdolls.TryAdd(instance.FileNameWithoutExtension, instance);
|
||||
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
|
||||
return instance;
|
||||
}
|
||||
@@ -362,17 +360,14 @@ namespace Barotrauma
|
||||
{
|
||||
// Update the key by removing and re-adding the ragdoll.
|
||||
string fileName = FileNameWithoutExtension;
|
||||
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
|
||||
if (allRagdolls.TryGetValue(SpeciesName, out ConcurrentDictionary<string, RagdollParams> ragdolls))
|
||||
{
|
||||
ragdolls.Remove(fileName);
|
||||
ragdolls.TryRemove(fileName, out _);
|
||||
}
|
||||
base.UpdatePath(fullPath);
|
||||
if (ragdolls != null)
|
||||
{
|
||||
if (!ragdolls.ContainsKey(fileName))
|
||||
{
|
||||
ragdolls.Add(fileName, this);
|
||||
}
|
||||
ragdolls.TryAdd(fileName, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
foreach (Identifier identifier in option.TalentIdentifiers)
|
||||
{
|
||||
if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
|
||||
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
|
||||
|
||||
identifiers.Add(identifier);
|
||||
}
|
||||
|
||||
foreach (var (_, value) in option.ShowCaseTalents)
|
||||
{
|
||||
var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
|
||||
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
|
||||
if (ids.Count is 0) { continue; }
|
||||
|
||||
identifiers.Add(value.GetRandomUnsynced());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Barotrauma.Abilities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -72,7 +74,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
|
||||
// ThreadLocal for thread-safe talent checking
|
||||
private static readonly ThreadLocal<HashSet<Identifier>> checkedNonStackableTalentsLocal = new ThreadLocal<HashSet<Identifier>>(() => new HashSet<Identifier>());
|
||||
private static HashSet<Identifier> checkedNonStackableTalents => checkedNonStackableTalentsLocal.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
|
||||
|
||||
if (character.IsTalentLocked(talentIdentifier)) { return false; }
|
||||
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
|
||||
|
||||
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
|
||||
{
|
||||
@@ -163,6 +163,16 @@ namespace Barotrauma
|
||||
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)
|
||||
{
|
||||
List<Identifier> viableTalents = new List<Identifier>();
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
|
||||
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
|
||||
{
|
||||
NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
|
||||
NPCConversationCollection.Collections.TryAdd(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
|
||||
}
|
||||
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ namespace Barotrauma
|
||||
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)] [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,
|
||||
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,
|
||||
() =>
|
||||
{
|
||||
string[] creatureAndJobNames =
|
||||
@@ -271,7 +271,7 @@ namespace Barotrauma
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character",
|
||||
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character",
|
||||
(string[] args) =>
|
||||
{
|
||||
if (Character.Controlled == null)
|
||||
@@ -294,10 +294,7 @@ namespace Barotrauma
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
GetItemNameOrIdParams().ToArray(),
|
||||
new string[] { "1" },
|
||||
new string[] { "100" },
|
||||
ItemQualityNames.ToArray()
|
||||
GetItemNameOrIdParams().ToArray()
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -314,7 +311,7 @@ namespace Barotrauma
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
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\".",
|
||||
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\".",
|
||||
(string[] args) =>
|
||||
{
|
||||
TrySpawnItem(args);
|
||||
@@ -324,10 +321,7 @@ namespace Barotrauma
|
||||
return new string[][]
|
||||
{
|
||||
GetItemNameOrIdParams().ToArray(),
|
||||
GetSpawnPosParams().ToArray(),
|
||||
new string[] { "1" },
|
||||
new string[] { "100" },
|
||||
ItemQualityNames.ToArray()
|
||||
GetSpawnPosParams().ToArray()
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -1330,7 +1324,6 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); }
|
||||
NewMessage("Level seed: " + Level.Loaded.Seed);
|
||||
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()));
|
||||
@@ -1340,29 +1333,17 @@ namespace Barotrauma
|
||||
}
|
||||
},null));
|
||||
|
||||
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.",
|
||||
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.",
|
||||
onExecute:(string[] args) =>
|
||||
{
|
||||
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 SERVER
|
||||
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
|
||||
#else
|
||||
submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
|
||||
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
|
||||
#endif
|
||||
}
|
||||
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -1375,9 +1356,9 @@ namespace Barotrauma
|
||||
Vector2 pos = Level.Loaded.StartPosition;
|
||||
if (Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
|
||||
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
|
||||
}
|
||||
submarineToTeleport.SetPosition(pos);
|
||||
Submarine.MainSub.SetPosition(pos);
|
||||
}
|
||||
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1389,9 +1370,9 @@ namespace Barotrauma
|
||||
Vector2 pos = Level.Loaded.EndPosition;
|
||||
if (Level.Loaded.EndOutpost != null)
|
||||
{
|
||||
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
|
||||
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
|
||||
}
|
||||
submarineToTeleport.SetPosition(pos);
|
||||
Submarine.MainSub.SetPosition(pos);
|
||||
}
|
||||
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1400,8 +1381,8 @@ namespace Barotrauma
|
||||
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
|
||||
return;
|
||||
}
|
||||
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
|
||||
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
|
||||
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
|
||||
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
|
||||
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
|
||||
if (submarineDockingPort != null && outpostDockingPort != null)
|
||||
{
|
||||
@@ -1413,8 +1394,7 @@ namespace Barotrauma
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
new string[] { "start", "end", "endoutpost", "cursor" },
|
||||
ListAvailableSubmarines()
|
||||
new string[] { "start", "end", "endoutpost", "cursor" }
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -1498,7 +1478,7 @@ namespace Barotrauma
|
||||
newItemName = args[2];
|
||||
}
|
||||
|
||||
var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
|
||||
var oldItem = Item.ItemList.Where(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
|
||||
if (oldItem == null)
|
||||
{
|
||||
ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex}).");
|
||||
@@ -1872,7 +1852,7 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
|
||||
{
|
||||
Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
|
||||
Item reactorItem = Item.ItemList.FirstOrDefault(i => i.GetComponent<Reactor>() != null);
|
||||
if (reactorItem == null) { return; }
|
||||
|
||||
var reactor = reactorItem.GetComponent<Reactor>();
|
||||
@@ -2590,16 +2570,6 @@ namespace Barotrauma
|
||||
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)
|
||||
{
|
||||
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
|
||||
@@ -2982,7 +2952,7 @@ namespace Barotrauma
|
||||
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
|
||||
}
|
||||
|
||||
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter);
|
||||
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew);
|
||||
|
||||
if (usePreConfiguredNPC)
|
||||
{
|
||||
@@ -3013,14 +2983,6 @@ namespace Barotrauma
|
||||
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
|
||||
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);
|
||||
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
|
||||
newCharacter.GiveIdCardTags(spawnPoint);
|
||||
@@ -3048,7 +3010,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter)
|
||||
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew)
|
||||
{
|
||||
spawnPosition = Vector2.Zero;
|
||||
spawnPoint = null;
|
||||
@@ -3134,12 +3096,6 @@ namespace Barotrauma
|
||||
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
|
||||
}
|
||||
}
|
||||
argIndex++;
|
||||
renameCharacter = null;
|
||||
if (args.Length > argIndex)
|
||||
{
|
||||
renameCharacter = args[argIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3147,7 +3103,6 @@ namespace Barotrauma
|
||||
{
|
||||
yield return "cursor";
|
||||
yield return "inventory";
|
||||
yield return "cargo";
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
@@ -3188,8 +3143,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ItemQualityNames = ["normal", "good", "excellent", "masterwork"];
|
||||
|
||||
private static void TrySpawnItem(string[] args)
|
||||
{
|
||||
try
|
||||
@@ -3250,7 +3203,6 @@ namespace Barotrauma
|
||||
|
||||
int amount = 1;
|
||||
int conditionPrc = 100;
|
||||
int itemQuality = 0;
|
||||
|
||||
if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex))
|
||||
{
|
||||
@@ -3278,23 +3230,12 @@ namespace Barotrauma
|
||||
if (args.Length > spawnLocationIndex + 1)
|
||||
{
|
||||
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
|
||||
amount = Math.Min(amount, 100);
|
||||
amount = Math.Min(amount, 100000);
|
||||
}
|
||||
|
||||
if (args.Length > spawnLocationIndex + 2)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3316,7 +3257,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality);
|
||||
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition);
|
||||
}
|
||||
}
|
||||
else if (spawnInventory != null)
|
||||
@@ -3343,7 +3284,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f);
|
||||
item.Quality = itemQuality;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,17 +31,12 @@ namespace Barotrauma
|
||||
Actions = new List<EventAction>();
|
||||
foreach (var e in element.Elements())
|
||||
{
|
||||
if (e.NameAsIdentifier().Equals("statuseffect"))
|
||||
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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);
|
||||
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);
|
||||
if (action != null) { Actions.Add(action); }
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Barotrauma
|
||||
AddTargetPredicate(
|
||||
Tags.Traitor,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c));
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagNonTraitors()
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
AddTargetPredicate(
|
||||
Tags.NonTraitor,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagNonTraitorPlayers()
|
||||
@@ -139,7 +139,7 @@ namespace Barotrauma
|
||||
AddTargetPredicate(
|
||||
Tags.NonTraitorPlayer,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
|
||||
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagBots(bool playerCrewOnly)
|
||||
@@ -151,8 +151,7 @@ namespace Barotrauma
|
||||
e is Character c &&
|
||||
c.IsBot &&
|
||||
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
|
||||
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) &&
|
||||
CharacterTeamMatches(c));
|
||||
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
|
||||
}
|
||||
|
||||
private void TagCrew()
|
||||
@@ -172,7 +171,7 @@ namespace Barotrauma
|
||||
|
||||
private void TagHumansByTag(Identifier tag)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c)));
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
|
||||
}
|
||||
|
||||
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -10,11 +11,22 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class UnlockPathAction : EventAction
|
||||
{
|
||||
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
|
||||
private static volatile ImmutableHashSet<LocationConnection> _pathsUnlockedThisRound =
|
||||
ImmutableHashSet<LocationConnection>.Empty;
|
||||
|
||||
public static void ResetPathsUnlockedThisRound()
|
||||
{
|
||||
pathsUnlockedThisRound.Clear();
|
||||
_pathsUnlockedThisRound = ImmutableHashSet<LocationConnection>.Empty;
|
||||
}
|
||||
|
||||
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) { }
|
||||
@@ -40,7 +52,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!connection.Locked) { continue; }
|
||||
connection.Locked = false;
|
||||
pathsUnlockedThisRound.Add(connection);
|
||||
AddUnlockedPath(connection);
|
||||
#if SERVER
|
||||
NotifyUnlock(connection);
|
||||
#else
|
||||
@@ -61,7 +73,7 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
public static void NotifyPathsUnlockedThisRound(Client client)
|
||||
{
|
||||
foreach (LocationConnection connection in pathsUnlockedThisRound)
|
||||
foreach (LocationConnection connection in _pathsUnlockedThisRound)
|
||||
{
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -43,7 +46,7 @@ namespace Barotrauma
|
||||
|
||||
private Level level;
|
||||
|
||||
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
|
||||
private volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty;
|
||||
|
||||
//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.
|
||||
@@ -83,14 +86,18 @@ namespace Barotrauma
|
||||
private float crewAwayResetTimer;
|
||||
private float crewAwayDuration;
|
||||
|
||||
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
|
||||
// volatile + ImmutableCollections
|
||||
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
|
||||
|
||||
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
|
||||
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents =
|
||||
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
|
||||
|
||||
private readonly List<Event> activeEvents = new List<Event>();
|
||||
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty;
|
||||
|
||||
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
|
||||
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
|
||||
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty;
|
||||
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
|
||||
|
||||
|
||||
#if DEBUG && SERVER
|
||||
@@ -112,10 +119,10 @@ namespace Barotrauma
|
||||
|
||||
public IEnumerable<Event> ActiveEvents
|
||||
{
|
||||
get { return activeEvents; }
|
||||
get { return _activeEvents; }
|
||||
}
|
||||
|
||||
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
|
||||
public readonly ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<Event>();
|
||||
|
||||
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
|
||||
|
||||
@@ -131,8 +138,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
|
||||
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
|
||||
private volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty;
|
||||
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e)));
|
||||
|
||||
public readonly EventLog EventLog = new EventLog();
|
||||
|
||||
@@ -143,6 +150,72 @@ namespace Barotrauma
|
||||
|
||||
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;
|
||||
public int RandomSeed { get; private set; }
|
||||
|
||||
@@ -152,15 +225,15 @@ namespace Barotrauma
|
||||
|
||||
if (isClient) { return; }
|
||||
|
||||
timeStamps.Clear();
|
||||
pendingEventSets.Clear();
|
||||
selectedEvents.Clear();
|
||||
activeEvents.Clear();
|
||||
ClearTimeStamps();
|
||||
ClearPendingEventSets();
|
||||
ClearSelectedEvents();
|
||||
ClearActiveEvents();
|
||||
#if SERVER
|
||||
MissionAction.ResetMissionsUnlockedThisRound();
|
||||
UnlockPathAction.ResetPathsUnlockedThisRound();
|
||||
#endif
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
totalPathLength = 0.0f;
|
||||
if (level != null)
|
||||
{
|
||||
@@ -235,8 +308,8 @@ namespace Barotrauma
|
||||
|
||||
void AddSet(EventSet eventSet)
|
||||
{
|
||||
if (pendingEventSets.Contains(eventSet)) { return; }
|
||||
pendingEventSets.Add(eventSet);
|
||||
if (_pendingEventSets.Contains(eventSet)) { return; }
|
||||
AddPendingEventSet(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
}
|
||||
|
||||
@@ -287,7 +360,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
|
||||
{
|
||||
nonRepeatableEvents.Add(ep.Identifier);
|
||||
AddNonRepeatableEvent(ep.Identifier);
|
||||
}
|
||||
}
|
||||
foreach (EventSet childSet in eventSet.ChildSets)
|
||||
@@ -332,13 +405,13 @@ namespace Barotrauma
|
||||
|
||||
public void ActivateEvent(Event newEvent)
|
||||
{
|
||||
activeEvents.Add(newEvent);
|
||||
AddActiveEvent(newEvent);
|
||||
newEvent.Init();
|
||||
}
|
||||
|
||||
public void ClearEvents()
|
||||
{
|
||||
activeEvents.Clear();
|
||||
ClearActiveEvents();
|
||||
}
|
||||
|
||||
private void SelectSettings()
|
||||
@@ -391,7 +464,8 @@ namespace Barotrauma
|
||||
|
||||
public IEnumerable<ContentFile> GetFilesToPreload()
|
||||
{
|
||||
foreach (List<Event> eventList in selectedEvents.Values)
|
||||
var snapshot = _selectedEvents;
|
||||
foreach (ImmutableList<Event> eventList in snapshot.Values)
|
||||
{
|
||||
foreach (Event ev in eventList)
|
||||
{
|
||||
@@ -444,13 +518,13 @@ namespace Barotrauma
|
||||
|
||||
foreach (ContentFile file in filesToPreload)
|
||||
{
|
||||
file.Preload(preloadedSprites.Add);
|
||||
file.Preload(AddPreloadedSprite);
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerOnEndRoundActions()
|
||||
{
|
||||
foreach (var ev in activeEvents)
|
||||
foreach (var ev in _activeEvents)
|
||||
{
|
||||
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
|
||||
}
|
||||
@@ -458,17 +532,16 @@ namespace Barotrauma
|
||||
|
||||
public void EndRound()
|
||||
{
|
||||
pendingEventSets.Clear();
|
||||
selectedEvents.Clear();
|
||||
activeEvents.Clear();
|
||||
QueuedEvents.Clear();
|
||||
finishedEvents.Clear();
|
||||
nonRepeatableEvents.Clear();
|
||||
ClearPendingEventSets();
|
||||
ClearSelectedEvents();
|
||||
ClearActiveEvents();
|
||||
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue
|
||||
ClearFinishedEvents();
|
||||
ClearNonRepeatableEvents();
|
||||
|
||||
preloadedSprites.ForEach(s => s.Remove());
|
||||
preloadedSprites.Clear();
|
||||
ClearPreloadedSprites();
|
||||
|
||||
timeStamps.Clear();
|
||||
ClearTimeStamps();
|
||||
|
||||
pathFinder = null;
|
||||
}
|
||||
@@ -484,7 +557,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (registerFinishedOnly)
|
||||
{
|
||||
foreach (var finishedEvent in finishedEvents)
|
||||
foreach (var finishedEvent in _finishedEvents)
|
||||
{
|
||||
EventSet parentSet = finishedEvent.ParentSet;
|
||||
if (parentSet == null) { continue; }
|
||||
@@ -499,7 +572,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
level.LevelData.EventHistory.AddRange(selectedEvents.Values
|
||||
level.LevelData.EventHistory.AddRange(_selectedEvents.Values
|
||||
.SelectMany(v => v)
|
||||
.Select(e => e.Prefab.Identifier)
|
||||
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
|
||||
@@ -509,14 +582,14 @@ namespace Barotrauma
|
||||
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)
|
||||
{
|
||||
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()
|
||||
@@ -534,7 +607,7 @@ namespace Barotrauma
|
||||
|
||||
private void CreateEvents(EventSet eventSet)
|
||||
{
|
||||
selectedEvents.Remove(eventSet);
|
||||
RemoveSelectedEventSet(eventSet);
|
||||
if (level == null) { return; }
|
||||
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
|
||||
@@ -601,11 +674,7 @@ namespace Barotrauma
|
||||
if (newEvent == null) { continue; }
|
||||
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
|
||||
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
}
|
||||
selectedEvents[eventSet].Add(newEvent);
|
||||
AddSelectedEvent(eventSet, newEvent);
|
||||
unusedEvents.Remove(subEventPrefab);
|
||||
}
|
||||
}
|
||||
@@ -644,11 +713,7 @@ namespace Barotrauma
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
if (newEvent == null) { continue; }
|
||||
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
}
|
||||
selectedEvents[eventSet].Add(newEvent);
|
||||
AddSelectedEvent(eventSet, newEvent);
|
||||
}
|
||||
|
||||
var location = GetEventLocation();
|
||||
@@ -840,9 +905,10 @@ namespace Barotrauma
|
||||
|
||||
if (!eventsInitialized)
|
||||
{
|
||||
foreach (var eventSet in selectedEvents.Keys)
|
||||
var selectedSnapshot = _selectedEvents;
|
||||
foreach (var eventSet in selectedSnapshot.Keys)
|
||||
{
|
||||
foreach (var ev in selectedEvents[eventSet])
|
||||
foreach (var ev in selectedSnapshot[eventSet])
|
||||
{
|
||||
ev.Init(eventSet);
|
||||
}
|
||||
@@ -913,23 +979,25 @@ namespace Barotrauma
|
||||
{
|
||||
recheck = false;
|
||||
//activate pending event sets that can be activated
|
||||
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
|
||||
var pendingSnapshot = _pendingEventSets;
|
||||
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var eventSet = pendingEventSets[i];
|
||||
var eventSet = pendingSnapshot[i];
|
||||
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
|
||||
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
|
||||
if (!CanStartEventSet(eventSet)) { continue; }
|
||||
|
||||
pendingEventSets.RemoveAt(i);
|
||||
RemovePendingEventSetAt(i);
|
||||
|
||||
if (selectedEvents.ContainsKey(eventSet))
|
||||
var selectedEventsList = GetSelectedEvents(eventSet);
|
||||
if (selectedEventsList.Count > 0)
|
||||
{
|
||||
//start events in this set
|
||||
foreach (Event ev in selectedEvents[eventSet])
|
||||
foreach (Event ev in selectedEventsList)
|
||||
{
|
||||
activeEvents.Add(ev);
|
||||
AddActiveEvent(ev);
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
|
||||
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown))
|
||||
{
|
||||
eventCoolDown = settings.EventCooldown;
|
||||
}
|
||||
@@ -937,12 +1005,15 @@ namespace Barotrauma
|
||||
{
|
||||
ev.Finished += () =>
|
||||
{
|
||||
pendingEventSets.Add(eventSet);
|
||||
EnqueueDeferredAction(() =>
|
||||
{
|
||||
AddPendingEventSet(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
foreach (Event newEvent in selectedEvents[eventSet])
|
||||
foreach (Event newEvent in GetSelectedEvents(eventSet))
|
||||
{
|
||||
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -951,37 +1022,40 @@ namespace Barotrauma
|
||||
//add child event sets to pending
|
||||
foreach (EventSet childEventSet in eventSet.ChildSets)
|
||||
{
|
||||
pendingEventSets.Add(childEventSet);
|
||||
AddPendingEventSet(childEventSet);
|
||||
recheck = true;
|
||||
}
|
||||
}
|
||||
} while (recheck);
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
var activeSnapshot = _activeEvents;
|
||||
foreach (Event ev in activeSnapshot)
|
||||
{
|
||||
if (!ev.IsFinished)
|
||||
{
|
||||
ev.Update(deltaTime);
|
||||
}
|
||||
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
|
||||
else if (ev.Prefab != null && !IsEventFinished(ev))
|
||||
{
|
||||
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); }
|
||||
}
|
||||
finishedEvents.Add(ev);
|
||||
AddFinishedEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
if (QueuedEvents.Count > 0)
|
||||
if (QueuedEvents.TryDequeue(out var queuedEvent))
|
||||
{
|
||||
activeEvents.Add(QueuedEvents.Dequeue());
|
||||
AddActiveEvent(queuedEvent);
|
||||
}
|
||||
|
||||
ProcessDeferredActions();
|
||||
}
|
||||
|
||||
public void EntitySpawned(Entity entity)
|
||||
{
|
||||
foreach (var ev in activeEvents)
|
||||
foreach (var ev in _activeEvents)
|
||||
{
|
||||
if (ev is ScriptedEvent scriptedEvent)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -60,47 +61,48 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
|
||||
private static volatile ImmutableDictionary<Identifier, EventPrefab> _allEventPrefabs =
|
||||
ImmutableDictionary<Identifier, EventPrefab>.Empty;
|
||||
|
||||
public static IEnumerable<EventPrefab> GetAllEventPrefabs()
|
||||
{
|
||||
return AllEventPrefabs.Values;
|
||||
return _allEventPrefabs.Values;
|
||||
}
|
||||
|
||||
/// <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),
|
||||
/// and adds them to <see cref="AllEventPrefabs"/>.
|
||||
/// and adds them to <see cref="_allEventPrefabs"/>.
|
||||
/// </summary>
|
||||
public static void RefreshAllEventPrefabs()
|
||||
{
|
||||
AllEventPrefabs.Clear();
|
||||
var builder = ImmutableDictionary.CreateBuilder<Identifier, EventPrefab>();
|
||||
foreach (var eventPrefab in EventPrefab.Prefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
foreach (var eventSet in Prefabs)
|
||||
{
|
||||
AddChildEventPrefabs(eventSet);
|
||||
AddChildEventPrefabs(eventSet, builder);
|
||||
}
|
||||
Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable());
|
||||
}
|
||||
|
||||
private static void AddChildEventPrefabs(EventSet set)
|
||||
private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary<Identifier, EventPrefab>.Builder builder)
|
||||
{
|
||||
foreach (var subEventPrefabs in set.EventPrefabs)
|
||||
{
|
||||
foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
|
||||
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); }
|
||||
}
|
||||
|
||||
public static EventPrefab GetEventPrefab(Identifier identifier)
|
||||
{
|
||||
return AllEventPrefabs.GetValueOrDefault(identifier);
|
||||
return _allEventPrefabs.GetValueOrDefault(identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
|
||||
protected override void InitEventSpecific(EventSet parentSet)
|
||||
{
|
||||
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
|
||||
var matchingItems = Item.ItemList.Where(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)).ToList();
|
||||
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
|
||||
for (int i = 0; i < itemAmount; i++)
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!itemTag.IsEmpty)
|
||||
{
|
||||
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
|
||||
var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList();
|
||||
if (!itemsToDestroy.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".",
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
destructibleItems.Clear();
|
||||
destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
|
||||
destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag)));
|
||||
if (destructibleItems.None())
|
||||
{
|
||||
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)
|
||||
{
|
||||
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
|
||||
if (!path.Unreachable)
|
||||
{
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Add(stackedItem);
|
||||
Item.MarkForDeconstruction(stackedItem);
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
|
||||
@@ -138,7 +138,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Remove(stackedItem);
|
||||
Item.UnmarkForDeconstruction(stackedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,7 +1089,7 @@ namespace Barotrauma
|
||||
|
||||
//Clear the grids to allow for garbage collection
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
Powered.ClearChangedConnections();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1146,6 +1146,7 @@ namespace Barotrauma
|
||||
EventManager?.EndRound();
|
||||
StatusEffect.StopAll();
|
||||
AfflictionPrefab.ClearAllEffects();
|
||||
PhysicsBodyQueue.Clear();
|
||||
IsRunning = false;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user