Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs
4 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
8 
9 namespace Barotrauma
10 {
11  partial class ScanMission : Mission
12  {
13  private readonly ContentXElement itemConfig;
14  private readonly List<Item> startingItems = new List<Item>();
15  private readonly List<Scanner> scanners = new List<Scanner>();
16  private readonly Dictionary<Item, ushort> parentInventoryIDs = new Dictionary<Item, ushort>();
17  private readonly Dictionary<Item, int> inventorySlotIndices = new Dictionary<Item, int>();
18  private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
19  private readonly int targetsToScan;
20  private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
21  private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
22  private readonly float minTargetDistance;
23 
24 
25  private Ruin TargetRuin { get; set; }
26 
27  private bool AllTargetsScanned
28  {
29  get
30  {
31  return scanTargets.Any() && scanTargets.All(kvp => kvp.Value);
32  }
33  }
34 
35  public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
36  {
37  get
38  {
39  if (State > 0 || scanTargets.None())
40  {
41  return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
42  }
43  else
44  {
45  return scanTargets
46  .Where(kvp => !kvp.Value)
47  .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition));
48  }
49  }
50  }
51 
52  public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
53  {
54  itemConfig = prefab.ConfigElement.GetChildElement("Items");
55  targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
56  minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f);
57  }
58 
59  protected override void StartMissionSpecific(Level level)
60  {
61  Reset();
62 
63  if (IsClient) { return; }
64 
65  if (itemConfig == null)
66  {
67  DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set",
68  contentPackage: Prefab.ContentPackage);
69  return;
70  }
71 
72  foreach (var element in itemConfig.Elements())
73  {
74  LoadItem(element, null);
75  }
76  GetScanners();
77 
78  TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient);
79  if (TargetRuin == null)
80  {
81  DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins",
82  contentPackage: Prefab.ContentPackage);
83  return;
84  }
85 
86  var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false);
87  ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null);
88  if (ruinWaypoints.Count < targetsToScan)
89  {
90  DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})",
91  contentPackage: Prefab.ContentPackage);
92  return;
93  }
94  var availableWaypoints = new List<WayPoint>();
95  float minTargetDistanceSquared = minTargetDistance * minTargetDistance;
96  for (int tries = 0; tries < 15; tries++)
97  {
98  scanTargets.Clear();
99  availableWaypoints.Clear();
100  availableWaypoints.AddRange(ruinWaypoints);
101  for (int i = 0; i < targetsToScan; i++)
102  {
103  var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient);
104  scanTargets.Add(selectedWaypoint, false);
105  availableWaypoints.Remove(selectedWaypoint);
106  if (i < (targetsToScan - 1))
107  {
108  availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull);
109  availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared);
110  if (availableWaypoints.None())
111  {
112 #if DEBUG
113  DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
114  contentPackage: Prefab.ContentPackage);
115 #endif
116  break;
117  }
118  }
119  }
120  if (scanTargets.Count >= targetsToScan)
121  {
122 #if DEBUG
123  DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green);
124 #endif
125  break;
126  }
127  if ((tries + 1) % 5 == 0)
128  {
129  float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance;
130  minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance;
131 #if DEBUG
132  DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow);
133 #endif
134  }
135  }
136  if (scanTargets.Count < targetsToScan)
137  {
138  DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
139  contentPackage: Prefab.ContentPackage);
140  }
141  }
142 
143  private void Reset()
144  {
145  startingItems.Clear();
146  parentInventoryIDs.Clear();
147  inventorySlotIndices.Clear();
148  parentItemContainerIndices.Clear();
149  scanners.Clear();
150  TargetRuin = null;
151  scanTargets.Clear();
152  }
153 
154  private void LoadItem(XElement element, Item parent)
155  {
156  var itemPrefab = FindItemPrefab(element);
157  Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub);
158  if (!position.HasValue) { return; }
159  var item = new Item(itemPrefab, position.Value, cargoRoomSub);
160  item.FindHull();
161  startingItems.Add(item);
162  if (parent?.GetComponent<ItemContainer>() is ItemContainer itemContainer)
163  {
164  parentInventoryIDs.Add(item, parent.ID);
165  parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer));
166  parent.Combine(item, user: null);
167  inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1);
168  }
169  foreach (XElement subElement in element.Elements())
170  {
171  int amount = subElement.GetAttributeInt("amount", 1);
172  for (int i = 0; i < amount; i++)
173  {
174  LoadItem(subElement, item);
175  }
176  }
177  }
178 
179  private void GetScanners()
180  {
181  foreach (var startingItem in startingItems)
182  {
183  if (startingItem.GetComponent<Scanner>() is Scanner scanner)
184  {
185  scanner.OnScanStarted += OnScanStarted;
186  if (!IsClient)
187  {
188  scanner.OnScanCompleted += OnScanCompleted;
189  }
190  scanners.Add(scanner);
191  }
192  }
193  }
194 
195  private void OnScanStarted(Scanner scanner)
196  {
197  float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
198  foreach (var kvp in scanTargets)
199  {
200  if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
201  scanner.DisplayProgressBar = true;
202  break;
203  }
204  }
205 
206  private void OnScanCompleted(Scanner scanner)
207  {
208  if (IsClient) { return; }
209  newTargetsScanned.Clear();
210  float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
211  foreach (var kvp in scanTargets)
212  {
213  if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
214  newTargetsScanned.Add(kvp.Key);
215  }
216  foreach (var wp in newTargetsScanned)
217  {
218  scanTargets[wp] = true;
219  }
220 #if SERVER
221  // Server should make sure that the clients' scan target status is in-sync
222  GameMain.Server?.UpdateMissionState(this);
223 #endif
224  }
225 
226  private static bool IsValidScanPosition(Scanner scanner, KeyValuePair<WayPoint, bool> scanStatus, float scanRadiusSquared)
227  {
228  if (scanStatus.Value) { return false; }
229  if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; }
230  if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; }
231  return true;
232  }
233 
234  protected override void UpdateMissionSpecific(float deltaTime)
235  {
236  if (IsClient) { return; }
237  switch (State)
238  {
239  case 0:
240  if (AllTargetsScanned)
241  {
242  State = 1;
243  }
244  break;
245  }
246  }
247 
248  protected override bool DetermineCompleted() => State > 0;
249 
250  protected override void EndMissionSpecific(bool completed)
251  {
252  foreach (var scanner in scanners)
253  {
254  if (scanner.Item != null && !scanner.Item.Removed)
255  {
256  scanner.OnScanStarted -= OnScanStarted;
257  scanner.OnScanCompleted -= OnScanCompleted;
258  scanner.Item.Remove();
259  }
260  }
261  Reset();
262  failed = !completed && state > 0;
263  }
264  }
265 }
float GetAttributeFloat(string key, float def)
ContentXElement? GetChildElement(string name)
int GetAttributeInt(string key, int def)
Submarine Submarine
Definition: Entity.cs:53
bool DisplayProgressBar
Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false.
Defines a point in the event that GoTo actions can jump to.
Definition: Label.cs:7
Vector2? GetCargoSpawnPosition(ItemPrefab itemPrefab, out Submarine cargoRoomSub)
ContentPackage? ContentPackage
Definition: Prefab.cs:37
ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub)
override bool DetermineCompleted()
override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
List< WayPoint > GetWaypoints(bool alsoFromConnectedSubs)