using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Items.Components { partial class Sonar : Powered, IServerSerializable, IClientSerializable { 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; /// Accessed through event actions. Do not remove even if there are no references in code. 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 connectedTransducers; public IEnumerable 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(); IsActive = true; InitProjSpecific(element); CurrentMode = Mode.Passive; } 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 ((Voltage >= MinVoltage) && (!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 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; } } } } /// /// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode. /// 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 Dictionary> targetGroups = new Dictionary>(); public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } foreach (List targetGroup in targetGroups.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 (!targetGroups.ContainsKey(directionName)) { targetGroups.Add(directionName, new List()); } targetGroups[directionName].Add(c); } foreach (KeyValuePair> targetGroup in targetGroups) { 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(); 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; } } } 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); } } } }