Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 
8 {
10  {
11  public enum Mode
12  {
13  Active,
14  Passive
15  };
16 
17  public const float DefaultSonarRange = 10000.0f;
18 
19  public const float PassivePowerConsumption = 0.1f;
20 
21  class ConnectedTransducer
22  {
23  public readonly SonarTransducer Transducer;
24  public float SignalStrength;
25  public float DisconnectTimer;
26 
27  public ConnectedTransducer(SonarTransducer transducer, float signalStrength, float disconnectTimer)
28  {
29  Transducer = transducer;
30  SignalStrength = signalStrength;
31  DisconnectTimer = disconnectTimer;
32  }
33  }
34 
35  private const float DirectionalPingSector = 30.0f;
36  private static readonly float DirectionalPingDotProduct;
37 
38  static Sonar()
39  {
40  DirectionalPingDotProduct = (float)Math.Cos(MathHelper.ToRadians(DirectionalPingSector) * 0.5f);
41  }
42 
43  private float range;
44 
45  private const float PingFrequency = 0.5f;
46 
47  private Mode currentMode = Mode.Passive;
48 
49  private class ActivePing
50  {
51  public float State;
52  public bool IsDirectional;
53  public Vector2 Direction;
54  public float PrevPingRadius;
55  }
56  // rotating list of currently active pings
57  private ActivePing[] activePings = new ActivePing[8];
58  // total number of currently active pings, range [0, activePings.Length[
59  private int activePingsCount;
60  // currently active ping index on the above list
61  private int currentPingIndex = -1;
62 
63  private const float MinZoom = 1.0f, MaxZoom = 4.0f;
64  private float zoom = 1.0f;
65 
67  public bool UseDirectionalPing => useDirectionalPing;
68  private bool useDirectionalPing = false;
69  private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
70 
71  private bool aiPingCheckPending;
72 
73  //the float value is a timer used for disconnecting the transducer if no signal is received from it for 1 second
74  private readonly List<ConnectedTransducer> connectedTransducers;
75 
76  public IEnumerable<SonarTransducer> ConnectedTransducers
77  {
78  get { return connectedTransducers.Select(t => t.Transducer); }
79  }
80 
81  [Serialize(DefaultSonarRange, IsPropertySaveable.No, description: "The maximum range of the sonar.")]
82  public float Range
83  {
84  get { return range; }
85  set
86  {
87  range = MathHelper.Clamp(value, 0.0f, 100000.0f);
88  if (item?.AiTarget != null && item.AiTarget.MaxSoundRange <= 0)
89  {
90  item.AiTarget.MaxSoundRange = range;
91  }
92  }
93  }
94 
95  [Serialize(false, IsPropertySaveable.No, description: "Should the sonar display the walls of the submarine it is inside.")]
97  {
98  get;
99  set;
100  }
101 
102  [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have to be connected to external transducers to work.")]
103  public bool UseTransducers
104  {
105  get;
106  set;
107  }
108 
109  [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.")]
111  {
112  get;
113  set;
114  }
115 
116  private bool hasMineralScanner;
117 
118  [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. ")]
119  public bool HasMineralScanner
120  {
121  get => hasMineralScanner;
122  set
123  {
124 #if CLIENT
125  if (controlContainer != null && !hasMineralScanner && value)
126  {
127  AddMineralScannerSwitchToGUI();
128  }
129 #endif
130  hasMineralScanner = value;
131  }
132  }
133 
134  [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
135  public bool UseMineralScanner { get; set; }
136 
137  public float Zoom
138  {
139  get { return zoom; }
140  set
141  {
142  zoom = MathHelper.Clamp(value, MinZoom, MaxZoom);
143 #if CLIENT
144  zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom);
145 #endif
146  }
147  }
148 
150  {
151  get => currentMode;
152  set
153  {
154  bool changed = currentMode != value;
155  currentMode = value;
156 #if CLIENT
157  if (changed) { prevPassivePingRadius = float.MaxValue; }
158  UpdateGUIElements();
159 #endif
160  }
161  }
162 
163  public Sonar(Item item, ContentXElement element)
164  : base(item, element)
165  {
166  connectedTransducers = new List<ConnectedTransducer>();
167  IsActive = true;
168  InitProjSpecific(element);
169  CurrentMode = Mode.Passive;
170  }
171 
172  partial void InitProjSpecific(ContentXElement element);
173 
174  public override void Update(float deltaTime, Camera cam)
175  {
176  UpdateOnActiveEffects(deltaTime);
177 
178  if (UseTransducers)
179  {
180  foreach (ConnectedTransducer transducer in connectedTransducers)
181  {
182  transducer.DisconnectTimer -= deltaTime;
183  }
184  connectedTransducers.RemoveAll(t => t.DisconnectTimer <= 0.0f);
185  }
186 
187  for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
188  {
189  activePings[pingIndex].State += deltaTime * PingFrequency;
190  }
191 
192  if (currentMode == Mode.Active)
193  {
194  if ((Voltage >= MinVoltage) &&
195  (!UseTransducers || connectedTransducers.Count > 0))
196  {
197  if (currentPingIndex != -1)
198  {
199  var activePing = activePings[currentPingIndex];
200  if (activePing.State > 1.0f)
201  {
202  aiPingCheckPending = true;
203  currentPingIndex = -1;
204  }
205  }
206  if (currentPingIndex == -1 && activePingsCount < activePings.Length)
207  {
208  currentPingIndex = activePingsCount++;
209  if (activePings[currentPingIndex] == null)
210  {
211  activePings[currentPingIndex] = new ActivePing();
212  }
213  activePings[currentPingIndex].IsDirectional = useDirectionalPing;
214  activePings[currentPingIndex].Direction = pingDirection;
215  activePings[currentPingIndex].State = 0.0f;
216  activePings[currentPingIndex].PrevPingRadius = 0.0f;
217  foreach (AITarget aiTarget in GetAITargets())
218  {
219  aiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f;
220  aiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
221  }
222  item.Use(deltaTime);
223  }
224  }
225  else
226  {
227  aiPingCheckPending = false;
228  }
229  }
230 
231  for (var pingIndex = 0; pingIndex < activePingsCount;)
232  {
233  foreach (AITarget aiTarget in GetAITargets())
234  {
235  float range = MathUtils.InverseLerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
236  aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range));
237  }
238  if (activePings[pingIndex].State > 1.0f)
239  {
240  var lastIndex = --activePingsCount;
241  var oldActivePing = activePings[pingIndex];
242  activePings[pingIndex] = activePings[lastIndex];
243  activePings[lastIndex] = oldActivePing;
244  if (currentPingIndex == lastIndex)
245  {
246  currentPingIndex = pingIndex;
247  }
248  }
249  else
250  {
251  ++pingIndex;
252  }
253  }
254  }
255 
256  private IEnumerable<AITarget> GetAITargets()
257  {
258  if (!UseTransducers)
259  {
260  if (item.AiTarget != null) { yield return item.AiTarget; }
261  }
262  else
263  {
264  foreach (var transducer in connectedTransducers)
265  {
266  if (transducer.Transducer.Item.AiTarget != null)
267  {
268  yield return transducer.Transducer.Item.AiTarget;
269  }
270  }
271  }
272  }
273 
277  public override float GetCurrentPowerConsumption(Connection connection = null)
278  {
279  if (connection != powerIn || !IsActive)
280  {
281  return 0;
282  }
283 
284  return (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption;
285  }
286 
287  public override bool Use(float deltaTime, Character character = null)
288  {
289  return currentPingIndex != -1 && (character == null || characterUsable);
290  }
291 
292  private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
293 
294  public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
295  {
296  if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
297 
298  foreach (List<Character> targetGroup in targetGroups.Values)
299  {
300  targetGroup.Clear();
301  }
302  foreach (Character c in Character.CharacterList)
303  {
304  if (c.IsDead || c.Removed || !c.Enabled) { continue; }
305  if (c.AnimController.CurrentHull != null || c.Params.HideInSonar) { continue; }
306  if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; }
307  if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; }
308 
309  #warning This is not the best key for a dictionary.
310  string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value;
311  if (!targetGroups.ContainsKey(directionName))
312  {
313  targetGroups.Add(directionName, new List<Character>());
314  }
315  targetGroups[directionName].Add(c);
316  }
317 
318  foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups)
319  {
320  if (!targetGroup.Value.Any()) { continue; }
321  string dialogTag = "DialogSonarTarget";
322  if (targetGroup.Value.Count > 1)
323  {
324  dialogTag = "DialogSonarTargetMultiple";
325  }
326  else if (targetGroup.Value[0].Mass > 100.0f)
327  {
328  dialogTag = "DialogSonarTargetLarge";
329  }
330 
331  if (character.IsOnPlayerTeam)
332  {
333  character.Speak(TextManager.GetWithVariables(dialogTag,
334  ("[direction]", targetGroup.Key.ToString(), FormatCapitals.Yes),
335  ("[count]", targetGroup.Value.Count.ToString(), FormatCapitals.No)).Value,
336  null, 0, $"sonartarget{targetGroup.Value[0].ID}".ToIdentifier(), 60);
337  }
338 
339  //prevent the character from reporting other targets in the group
340  for (int i = 1; i < targetGroup.Value.Count; i++)
341  {
342  character.DisableLine("sonartarget" + targetGroup.Value[i].ID);
343  }
344  }
345 
346  return true;
347  }
348 
349  private LocalizedString GetDirectionName(Vector2 dir)
350  {
351  float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2);
352 
353  int clockDir = (int)Math.Round((angle / MathHelper.TwoPi) * 12);
354  if (clockDir == 0) clockDir = 12;
355 
356  return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString());
357  }
358 
359  public override void ReceiveSignal(Signal signal, Connection connection)
360  {
361  base.ReceiveSignal(signal, connection);
362 
363  if (connection.Name == "transducer_in")
364  {
365  var transducer = signal.source.GetComponent<SonarTransducer>();
366  if (transducer == null) { return; }
367 
368  transducer.ConnectedSonar = this;
369 
370  var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer);
371  if (connectedTransducer == null)
372  {
373  connectedTransducers.Add(new ConnectedTransducer(transducer, signal.strength, 1.0f));
374  }
375  else
376  {
377  connectedTransducer.SignalStrength = signal.strength;
378  connectedTransducer.DisconnectTimer = 1.0f;
379  }
380  }
381  }
382 
383  public void ServerEventRead(IReadMessage msg, Client c)
384  {
385  bool isActive = msg.ReadBoolean();
386  bool directionalPing = useDirectionalPing;
387  float zoomT = zoom, pingDirectionT = 0.0f;
388  bool mineralScanner = UseMineralScanner;
389  if (isActive)
390  {
391  zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
392  directionalPing = msg.ReadBoolean();
393  if (directionalPing)
394  {
395  pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
396  }
397  mineralScanner = msg.ReadBoolean();
398  }
399 
400  if (!item.CanClientAccess(c)) { return; }
401 
402  CurrentMode = isActive ? Mode.Active : Mode.Passive;
403 
404  if (isActive)
405  {
406  zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT);
407  useDirectionalPing = directionalPing;
408  if (useDirectionalPing)
409  {
410  float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
411  pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
412  }
413  UseMineralScanner = mineralScanner;
414 #if CLIENT
415  zoomSlider.BarScroll = zoomT;
416  directionalModeSwitch.Selected = useDirectionalPing;
417  if (mineralScannerSwitch != null)
418  {
419  mineralScannerSwitch.Selected = UseMineralScanner;
420  }
421 #endif
422  }
423 #if SERVER
424  item.CreateServerEvent(this);
425 #endif
426  }
427 
428  public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
429  {
430  msg.WriteBoolean(currentMode == Mode.Active);
431  if (currentMode == Mode.Active)
432  {
433  msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8);
434  msg.WriteBoolean(useDirectionalPing);
435  if (useDirectionalPing)
436  {
437  float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
438  msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
439  }
441  }
442  }
443  }
444 }
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
float powerConsumption
The maximum amount of power the item can draw from connected items
void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData=null)
override void ReceiveSignal(Signal signal, Connection connection)
override bool Use(float deltaTime, Character character=null)
override float GetCurrentPowerConsumption(Connection connection=null)
Power consumption of the sonar. Only consume power when active and adjust the consumption based on th...
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
Interface for entities that the clients can send events to the server
Single ReadRangedSingle(Single min, Single max, int bitCount)
Interface for entities that the server can send events to the clients
void WriteRangedSingle(Single val, Single min, Single max, int bitCount)