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.
475 lines
17 KiB
C#
475 lines
17 KiB
C#
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Sonar : Powered, IServerSerializable, IClientSerializable
|
|
{
|
|
private static readonly ConcurrentDictionary<Sonar, byte> _sonarDict = new ConcurrentDictionary<Sonar, byte>();
|
|
public static IEnumerable<Sonar> SonarList => _sonarDict.Keys;
|
|
|
|
public enum Mode
|
|
{
|
|
Active,
|
|
Passive
|
|
};
|
|
|
|
public const float DefaultSonarRange = 10000.0f;
|
|
|
|
public const float PassivePowerConsumption = 0.1f;
|
|
|
|
class ConnectedTransducer
|
|
{
|
|
public readonly SonarTransducer Transducer;
|
|
public float SignalStrength;
|
|
public float DisconnectTimer;
|
|
|
|
public ConnectedTransducer(SonarTransducer transducer, float signalStrength, float disconnectTimer)
|
|
{
|
|
Transducer = transducer;
|
|
SignalStrength = signalStrength;
|
|
DisconnectTimer = disconnectTimer;
|
|
}
|
|
}
|
|
|
|
private const float DirectionalPingSector = 30.0f;
|
|
private static readonly float DirectionalPingDotProduct;
|
|
|
|
static Sonar()
|
|
{
|
|
DirectionalPingDotProduct = (float)Math.Cos(MathHelper.ToRadians(DirectionalPingSector) * 0.5f);
|
|
}
|
|
|
|
private float range;
|
|
|
|
private const float PingFrequency = 0.5f;
|
|
|
|
private Mode currentMode = Mode.Passive;
|
|
|
|
private class ActivePing
|
|
{
|
|
public float State;
|
|
public bool IsDirectional;
|
|
public Vector2 Direction;
|
|
public float PrevPingRadius;
|
|
}
|
|
// rotating list of currently active pings
|
|
private ActivePing[] activePings = new ActivePing[8];
|
|
// total number of currently active pings, range [0, activePings.Length[
|
|
private int activePingsCount;
|
|
// currently active ping index on the above list
|
|
private int currentPingIndex = -1;
|
|
|
|
private const float MinZoom = 1.0f, MaxZoom = 4.0f;
|
|
private float zoom = 1.0f;
|
|
|
|
/// <remarks>Accessed through event actions. Do not remove even if there are no references in code.</remarks>
|
|
public bool UseDirectionalPing => useDirectionalPing;
|
|
private bool useDirectionalPing = false;
|
|
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
|
|
|
|
private bool aiPingCheckPending;
|
|
|
|
//the float value is a timer used for disconnecting the transducer if no signal is received from it for 1 second
|
|
private readonly List<ConnectedTransducer> connectedTransducers;
|
|
|
|
public IEnumerable<SonarTransducer> ConnectedTransducers
|
|
{
|
|
get { return connectedTransducers.Select(t => t.Transducer); }
|
|
}
|
|
|
|
[Serialize(DefaultSonarRange, IsPropertySaveable.No, description: "The maximum range of the sonar.")]
|
|
public float Range
|
|
{
|
|
get { return range; }
|
|
set
|
|
{
|
|
range = MathHelper.Clamp(value, 0.0f, 100000.0f);
|
|
if (item?.AiTarget != null && item.AiTarget.MaxSoundRange <= 0)
|
|
{
|
|
item.AiTarget.MaxSoundRange = range;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Should the sonar display the walls of the submarine it is inside.")]
|
|
public bool DetectSubmarineWalls
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have to be connected to external transducers to work.")]
|
|
public bool UseTransducers
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")]
|
|
public bool CenterOnTransducers
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private bool hasMineralScanner;
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. ")]
|
|
public bool HasMineralScanner
|
|
{
|
|
get => hasMineralScanner;
|
|
set
|
|
{
|
|
#if CLIENT
|
|
if (controlContainer != null && !hasMineralScanner && value)
|
|
{
|
|
AddMineralScannerSwitchToGUI();
|
|
}
|
|
#endif
|
|
hasMineralScanner = value;
|
|
}
|
|
}
|
|
|
|
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool UseMineralScanner { get; set; }
|
|
|
|
public float Zoom
|
|
{
|
|
get { return zoom; }
|
|
set
|
|
{
|
|
zoom = MathHelper.Clamp(value, MinZoom, MaxZoom);
|
|
#if CLIENT
|
|
zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public Mode CurrentMode
|
|
{
|
|
get => currentMode;
|
|
set
|
|
{
|
|
bool changed = currentMode != value;
|
|
currentMode = value;
|
|
#if CLIENT
|
|
if (changed) { prevPassivePingRadius = float.MaxValue; }
|
|
UpdateGUIElements();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public Sonar(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
connectedTransducers = new List<ConnectedTransducer>();
|
|
IsActive = true;
|
|
InitProjSpecific(element);
|
|
CurrentMode = Mode.Passive;
|
|
_sonarDict.TryAdd(this, 0);
|
|
}
|
|
|
|
partial void InitProjSpecific(ContentXElement element);
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateOnActiveEffects(deltaTime);
|
|
|
|
if (UseTransducers)
|
|
{
|
|
foreach (ConnectedTransducer transducer in connectedTransducers)
|
|
{
|
|
transducer.DisconnectTimer -= deltaTime;
|
|
}
|
|
connectedTransducers.RemoveAll(t => t.DisconnectTimer <= 0.0f);
|
|
}
|
|
|
|
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
|
|
{
|
|
activePings[pingIndex].State += deltaTime * PingFrequency;
|
|
}
|
|
|
|
if (currentMode == Mode.Active)
|
|
{
|
|
if (HasPower && (!UseTransducers || connectedTransducers.Count > 0))
|
|
{
|
|
if (currentPingIndex != -1)
|
|
{
|
|
var activePing = activePings[currentPingIndex];
|
|
if (activePing.State > 1.0f)
|
|
{
|
|
aiPingCheckPending = true;
|
|
currentPingIndex = -1;
|
|
}
|
|
}
|
|
if (currentPingIndex == -1 && activePingsCount < activePings.Length)
|
|
{
|
|
currentPingIndex = activePingsCount++;
|
|
if (activePings[currentPingIndex] == null)
|
|
{
|
|
activePings[currentPingIndex] = new ActivePing();
|
|
}
|
|
activePings[currentPingIndex].IsDirectional = useDirectionalPing;
|
|
activePings[currentPingIndex].Direction = pingDirection;
|
|
activePings[currentPingIndex].State = 0.0f;
|
|
activePings[currentPingIndex].PrevPingRadius = 0.0f;
|
|
foreach (AITarget aiTarget in GetAITargets())
|
|
{
|
|
aiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
|
|
aiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
|
|
}
|
|
item.Use(deltaTime);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
aiPingCheckPending = false;
|
|
}
|
|
}
|
|
|
|
for (var pingIndex = 0; pingIndex < activePingsCount;)
|
|
{
|
|
foreach (AITarget aiTarget in GetAITargets())
|
|
{
|
|
float range = MathUtils.InverseLerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
|
|
aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range));
|
|
}
|
|
if (activePings[pingIndex].State > 1.0f)
|
|
{
|
|
var lastIndex = --activePingsCount;
|
|
var oldActivePing = activePings[pingIndex];
|
|
activePings[pingIndex] = activePings[lastIndex];
|
|
activePings[lastIndex] = oldActivePing;
|
|
if (currentPingIndex == lastIndex)
|
|
{
|
|
currentPingIndex = pingIndex;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++pingIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<AITarget> GetAITargets()
|
|
{
|
|
if (!UseTransducers)
|
|
{
|
|
if (item.AiTarget != null) { yield return item.AiTarget; }
|
|
}
|
|
else
|
|
{
|
|
foreach (var transducer in connectedTransducers)
|
|
{
|
|
if (transducer.Transducer.Item.AiTarget != null)
|
|
{
|
|
yield return transducer.Transducer.Item.AiTarget;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode.
|
|
/// </summary>
|
|
public override float GetCurrentPowerConsumption(Connection connection = null)
|
|
{
|
|
if (connection != powerIn || !IsActive)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption;
|
|
}
|
|
|
|
public override bool Use(float deltaTime, Character character = null)
|
|
{
|
|
return currentPingIndex != -1 && (character == null || characterUsable);
|
|
}
|
|
|
|
private static readonly ThreadLocal<Dictionary<string, List<Character>>> targetGroups =
|
|
new ThreadLocal<Dictionary<string, List<Character>>>(() => new Dictionary<string, List<Character>>());
|
|
|
|
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
|
{
|
|
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
|
|
|
|
var groups = targetGroups.Value;
|
|
foreach (List<Character> targetGroup in groups.Values)
|
|
{
|
|
targetGroup.Clear();
|
|
}
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c.IsDead || c.Removed || !c.Enabled) { continue; }
|
|
if (c.AnimController.CurrentHull != null || c.Params.HideInSonar) { continue; }
|
|
if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; }
|
|
if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; }
|
|
|
|
#warning This is not the best key for a dictionary.
|
|
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value;
|
|
if (!groups.ContainsKey(directionName))
|
|
{
|
|
groups.Add(directionName, new List<Character>());
|
|
}
|
|
groups[directionName].Add(c);
|
|
}
|
|
|
|
foreach (KeyValuePair<string, List<Character>> targetGroup in groups)
|
|
{
|
|
if (!targetGroup.Value.Any()) { continue; }
|
|
string dialogTag = "DialogSonarTarget";
|
|
if (targetGroup.Value.Count > 1)
|
|
{
|
|
dialogTag = "DialogSonarTargetMultiple";
|
|
}
|
|
else if (targetGroup.Value[0].Mass > 100.0f)
|
|
{
|
|
dialogTag = "DialogSonarTargetLarge";
|
|
}
|
|
|
|
if (character.IsOnPlayerTeam)
|
|
{
|
|
character.Speak(TextManager.GetWithVariables(dialogTag,
|
|
("[direction]", targetGroup.Key.ToString(), FormatCapitals.Yes),
|
|
("[count]", targetGroup.Value.Count.ToString(), FormatCapitals.No)).Value,
|
|
null, 0, $"sonartarget{targetGroup.Value[0].ID}".ToIdentifier(), 60);
|
|
}
|
|
|
|
//prevent the character from reporting other targets in the group
|
|
for (int i = 1; i < targetGroup.Value.Count; i++)
|
|
{
|
|
character.DisableLine("sonartarget" + targetGroup.Value[i].ID);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private LocalizedString GetDirectionName(Vector2 dir)
|
|
{
|
|
float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2);
|
|
|
|
int clockDir = (int)Math.Round((angle / MathHelper.TwoPi) * 12);
|
|
if (clockDir == 0) clockDir = 12;
|
|
|
|
return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString());
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
base.ReceiveSignal(signal, connection);
|
|
|
|
if (connection.Name == "transducer_in")
|
|
{
|
|
var transducer = signal.source.GetComponent<SonarTransducer>();
|
|
if (transducer == null) { return; }
|
|
|
|
transducer.ConnectedSonar = this;
|
|
|
|
var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer);
|
|
if (connectedTransducer == null)
|
|
{
|
|
connectedTransducers.Add(new ConnectedTransducer(transducer, signal.strength, 1.0f));
|
|
}
|
|
else
|
|
{
|
|
connectedTransducer.SignalStrength = signal.strength;
|
|
connectedTransducer.DisconnectTimer = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
#if CLIENT
|
|
sonarBlip?.Remove();
|
|
pingCircle?.Remove();
|
|
directionalPingCircle?.Remove();
|
|
screenOverlay?.Remove();
|
|
screenBackground?.Remove();
|
|
lineSprite?.Remove();
|
|
|
|
foreach (var t in targetIcons.Values)
|
|
{
|
|
t.Item1.Remove();
|
|
}
|
|
targetIcons.Clear();
|
|
|
|
MineralClusters = null;
|
|
#endif
|
|
_sonarDict.TryRemove(this, out _);
|
|
}
|
|
|
|
|
|
public void ServerEventRead(IReadMessage msg, Client c)
|
|
{
|
|
bool isActive = msg.ReadBoolean();
|
|
bool directionalPing = useDirectionalPing;
|
|
float zoomT = zoom, pingDirectionT = 0.0f;
|
|
bool mineralScanner = UseMineralScanner;
|
|
if (isActive)
|
|
{
|
|
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
|
|
directionalPing = msg.ReadBoolean();
|
|
if (directionalPing)
|
|
{
|
|
pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
|
|
}
|
|
mineralScanner = msg.ReadBoolean();
|
|
}
|
|
|
|
if (!item.CanClientAccess(c)) { return; }
|
|
|
|
CurrentMode = isActive ? Mode.Active : Mode.Passive;
|
|
|
|
if (isActive)
|
|
{
|
|
zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT);
|
|
useDirectionalPing = directionalPing;
|
|
if (useDirectionalPing)
|
|
{
|
|
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
|
|
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
|
|
}
|
|
UseMineralScanner = mineralScanner;
|
|
#if CLIENT
|
|
zoomSlider.BarScroll = zoomT;
|
|
directionalModeSwitch.Selected = useDirectionalPing;
|
|
if (mineralScannerSwitch != null)
|
|
{
|
|
mineralScannerSwitch.Selected = UseMineralScanner;
|
|
}
|
|
#endif
|
|
}
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
|
|
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
|
{
|
|
msg.WriteBoolean(currentMode == Mode.Active);
|
|
if (currentMode == Mode.Active)
|
|
{
|
|
msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8);
|
|
msg.WriteBoolean(useDirectionalPing);
|
|
if (useDirectionalPing)
|
|
{
|
|
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
|
|
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
|
|
}
|
|
msg.WriteBoolean(UseMineralScanner);
|
|
}
|
|
}
|
|
}
|
|
}
|