Replaced static lists and dictionaries with thread-safe ConcurrentDictionary or ThreadLocal collections for various item components and systems. Updated all relevant code to use snapshots (ToArray, ToList) for safe iteration, and added helper methods for marking and clearing changed connections. These changes improve thread safety and prevent potential concurrency issues in multi-threaded scenarios.
403 lines
16 KiB
C#
403 lines
16 KiB
C#
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
|
|
{
|
|
private static readonly ConcurrentDictionary<WifiComponent, byte> _wifiDict = new ConcurrentDictionary<WifiComponent, byte>();
|
|
private static IEnumerable<WifiComponent> AllWifiComponents => _wifiDict.Keys;
|
|
|
|
const int ChannelMemorySize = 10;
|
|
|
|
private const int MinChannel = 0;
|
|
private const int MaxChannel = 10000;
|
|
|
|
private float range;
|
|
|
|
private int channel;
|
|
|
|
private float chatMsgCooldown;
|
|
|
|
private string prevSignal;
|
|
|
|
private int[] channelMemory = new int[ChannelMemorySize];
|
|
|
|
private Connection signalInConnection;
|
|
private Connection signalOutConnection;
|
|
|
|
[Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)]
|
|
public CharacterTeamType TeamID { get; set; }
|
|
|
|
[Editable, Serialize(20000.0f, IsPropertySaveable.No, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)]
|
|
public float Range
|
|
{
|
|
get { return range; }
|
|
set
|
|
{
|
|
range = Math.Max(value, 0.0f);
|
|
#if CLIENT
|
|
item.ResetCachedVisibleSize();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
[InGameEditable, Serialize(0, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)]
|
|
public int Channel
|
|
{
|
|
get { return channel; }
|
|
set
|
|
{
|
|
channel = MathHelper.Clamp(value, MinChannel, MaxChannel);
|
|
}
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)]
|
|
public bool AllowCrossTeamCommunication
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat, onlyInEditors: false)]
|
|
[Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
|
|
"as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)]
|
|
public bool LinkToChat
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " +
|
|
"Setting this to a very low value is not recommended, because it may cause an excessive amount of chat messages to be created " +
|
|
"if there are chat-linked wifi components that transmit a continuous signal.")]
|
|
public float MinChatMessageInterval
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the component will only create chat messages when the received signal changes.")]
|
|
public bool DiscardDuplicateChatMessages
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private float jamTimer;
|
|
public float JamTimer
|
|
{
|
|
get { return jamTimer; }
|
|
set
|
|
{
|
|
if (value > 0)
|
|
{
|
|
#if CLIENT
|
|
if (jamTimer <= 0)
|
|
{
|
|
HintManager.OnRadioJammed(Item);
|
|
}
|
|
#endif
|
|
IsActive = true;
|
|
}
|
|
jamTimer = Math.Max(0, value);
|
|
}
|
|
}
|
|
|
|
public WifiComponent(Item item, ContentXElement element)
|
|
: base (item, element)
|
|
{
|
|
_wifiDict.TryAdd(this, 0);
|
|
IsActive = true;
|
|
}
|
|
|
|
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
|
{
|
|
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
|
channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
|
|
if (channelMemory.Length != ChannelMemorySize)
|
|
{
|
|
DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing...");
|
|
Array.Resize(ref channelMemory, ChannelMemorySize);
|
|
}
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
if (item.Connections != null)
|
|
{
|
|
signalOutConnection = item.Connections.Find(c => c.Name == "signal_out");
|
|
signalInConnection = item.Connections.Find(c => c.Name == "signal_in");
|
|
}
|
|
if (channelMemory.All(m => m == 0))
|
|
{
|
|
for (int i = 0; i < channelMemory.Length; i++)
|
|
{
|
|
channelMemory[i] = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CanTransmit(bool ignoreJamming = false)
|
|
{
|
|
if (!ignoreJamming)
|
|
{
|
|
if (jamTimer > 0) { return false; }
|
|
}
|
|
return HasRequiredContainedItems(user: null, addMessage: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the wifi components that can receive signals from this one
|
|
/// </summary>
|
|
public IEnumerable<WifiComponent> GetReceiversInRange()
|
|
{
|
|
return AllWifiComponents.Where(w => w != this && w.CanReceive(this));
|
|
}
|
|
|
|
public bool CanReceive(WifiComponent sender)
|
|
{
|
|
if (sender == null || sender.channel != channel) { return false; }
|
|
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
|
if (jamTimer > 0) { return false; }
|
|
|
|
//if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing
|
|
// = no point in receiving
|
|
if (!LinkToChat)
|
|
{
|
|
if (signalOutConnection == null || !signalOutConnection.IsConnectedToSomething())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
|
|
|
|
return HasRequiredContainedItems(user: null, addMessage: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the wifi components that can transmit signals to this one
|
|
/// </summary>
|
|
public IEnumerable<WifiComponent> GetTransmittersInRange()
|
|
{
|
|
return AllWifiComponents.Where(w => w != this && w.CanTransmit(this));
|
|
}
|
|
|
|
public bool CanTransmit(WifiComponent sender)
|
|
{
|
|
if (sender == null || sender.channel != channel) { return false; }
|
|
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
|
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
|
|
if (jamTimer > 0) { return false; }
|
|
return HasRequiredContainedItems(user: null, addMessage: false);
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
chatMsgCooldown -= deltaTime;
|
|
JamTimer -= deltaTime;
|
|
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
|
if (chatMsgCooldown <= 0.0f && JamTimer <= 0.0f)
|
|
{
|
|
IsActive = false;
|
|
}
|
|
}
|
|
|
|
public int GetChannelMemory(int index)
|
|
{
|
|
if (index < 0 || index >= ChannelMemorySize)
|
|
{
|
|
return 0;
|
|
}
|
|
return channelMemory[index];
|
|
}
|
|
|
|
public void SetChannelMemory(int index, int value)
|
|
{
|
|
if (index < 0 || index >= ChannelMemorySize)
|
|
{
|
|
return;
|
|
}
|
|
channelMemory[index] = MathHelper.Clamp(value, 0, 10000);
|
|
}
|
|
|
|
public void TransmitSignal(Signal signal, bool sentFromChat)
|
|
{
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("wifiSignalTransmitted", this, signal, sentFromChat);
|
|
|
|
if (should != null && should.Value) { return; }
|
|
|
|
bool chatMsgSent = false;
|
|
|
|
var receivers = GetReceiversInRange();
|
|
if (sentFromChat)
|
|
{
|
|
//if sent from chat, we need to reset the "signal chain" at this point
|
|
//so we can correctly detect which components the signal has already passed through to avoid infinite loops
|
|
//only relevant for signals originating from the chat - normally this is handled in Item.SendSignal
|
|
item.LastSentSignalRecipients.Clear();
|
|
foreach (WifiComponent receiver in receivers)
|
|
{
|
|
receiver.item.LastSentSignalRecipients.Clear();
|
|
}
|
|
}
|
|
foreach (WifiComponent wifiComp in receivers)
|
|
{
|
|
if (sentFromChat && !wifiComp.LinkToChat) { continue; }
|
|
|
|
//signal strength diminishes by distance
|
|
float sentSignalStrength = signal.strength *
|
|
MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f);
|
|
Signal s = new Signal(signal.value, signal.stepsTaken + 1, sender: signal.sender, source: signal.source,
|
|
power: 0.0f, strength: sentSignalStrength);
|
|
|
|
if (wifiComp.signalOutConnection != null)
|
|
{
|
|
if (signal.source != null && wifiComp.signalInConnection != null)
|
|
{
|
|
if (signal.source.LastSentSignalRecipients.Contains(wifiComp.signalInConnection))
|
|
{
|
|
//signal already passed through this wifi component -> stop here to prevent an infinite loop
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
signal.source.LastSentSignalRecipients.Add(wifiComp.signalInConnection);
|
|
}
|
|
}
|
|
wifiComp.item.SendSignal(s, wifiComp.signalOutConnection);
|
|
}
|
|
|
|
if (signal.source != null)
|
|
{
|
|
// Use ToList() snapshot for thread-safe iteration
|
|
foreach (Connection receiver in wifiComp.item.LastSentSignalRecipients.ToList())
|
|
{
|
|
if (!signal.source.LastSentSignalRecipients.Contains(receiver))
|
|
{
|
|
signal.source.LastSentSignalRecipients.Add(receiver);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DiscardDuplicateChatMessages && signal.value == prevSignal) { continue; }
|
|
|
|
//create a chat message
|
|
if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && !sentFromChat)
|
|
{
|
|
if (wifiComp.item.ParentInventory != null &&
|
|
wifiComp.item.ParentInventory.Owner != null)
|
|
{
|
|
string chatMsg = signal.value;
|
|
if (sentSignalStrength <= 1.0f)
|
|
{
|
|
chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength);
|
|
}
|
|
if (chatMsg.Length > ChatMessage.MaxLength) { chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength); }
|
|
if (string.IsNullOrEmpty(chatMsg)) { continue; }
|
|
|
|
#if CLIENT
|
|
if (wifiComp.item.ParentInventory.Owner == Character.Controlled)
|
|
{
|
|
if (GameMain.Client == null)
|
|
{
|
|
GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(signal.source?.Name ?? "", signal.value, ChatMessageType.Radio, sender: item);
|
|
}
|
|
}
|
|
#elif SERVER
|
|
if (GameMain.Server != null)
|
|
{
|
|
Client recipientClient = GameMain.Server.ConnectedClients.Find(c => c.Character == wifiComp.item.ParentInventory.Owner);
|
|
if (recipientClient != null)
|
|
{
|
|
GameMain.Server.SendDirectChatMessage(
|
|
ChatMessage.Create(signal.source?.Name ?? "", chatMsg, ChatMessageType.Radio, item), recipientClient);
|
|
}
|
|
}
|
|
#endif
|
|
chatMsgSent = true;
|
|
}
|
|
}
|
|
}
|
|
if (chatMsgSent)
|
|
{
|
|
chatMsgCooldown = MinChatMessageInterval;
|
|
IsActive = true;
|
|
}
|
|
|
|
prevSignal = signal.value;
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
if (connection == null) { return; }
|
|
|
|
switch (connection.Name)
|
|
{
|
|
case "signal_in":
|
|
TransmitSignal(signal, false);
|
|
break;
|
|
case "set_channel":
|
|
if (int.TryParse(signal.value, out int newChannel))
|
|
{
|
|
int prevChannel = Channel;
|
|
Channel = newChannel;
|
|
if (prevChannel != Channel)
|
|
{
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
break;
|
|
case "set_range":
|
|
if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRange))
|
|
{
|
|
Range = newRange;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
_wifiDict.TryRemove(this, out _);
|
|
}
|
|
|
|
public override XElement Save(XElement parentElement)
|
|
{
|
|
var element = base.Save(parentElement);
|
|
element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory)));
|
|
return element;
|
|
}
|
|
|
|
protected void SharedEventWrite(IWriteMessage msg)
|
|
{
|
|
msg.WriteRangedInteger(Channel, MinChannel, MaxChannel);
|
|
|
|
for (int i = 0; i < ChannelMemorySize; i++)
|
|
{
|
|
msg.WriteRangedInteger(channelMemory[i], MinChannel, MaxChannel);
|
|
}
|
|
}
|
|
|
|
protected void SharedEventRead(IReadMessage msg)
|
|
{
|
|
Channel = msg.ReadRangedInteger(MinChannel, MaxChannel);
|
|
|
|
for (int i = 0; i < ChannelMemorySize; i++)
|
|
{
|
|
channelMemory[i] = msg.ReadRangedInteger(MinChannel, MaxChannel);
|
|
}
|
|
}
|
|
}
|
|
}
|