Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs
3 using Microsoft.Xna.Framework;
4 using System.Collections.Generic;
6 using System.Linq;
7 using System;
8 
9 namespace Barotrauma
10 {
11  internal class SubmarineTurretAI
12  {
13  public Submarine Submarine { get; protected set; }
14  protected readonly List<Turret> turrets = new List<Turret>();
15  public Identifier FriendlyTag;
16 
17  public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default)
18  {
19  FriendlyTag = friendlyTag;
20  Submarine = submarine;
21  foreach (Item item in Item.ItemList)
22  {
23  if (item.Submarine != Submarine) { continue; }
24  var turret = item.GetComponent<Turret>();
25  if (turret != null)
26  {
27  turrets.Add(turret);
28  // Set false, because we manage the turrets in the Update method.
29  turret.AutoOperate = false;
30  // Set to full condition, because items don't work when they are broken.
31  turret.Item.Condition = turret.Item.MaxCondition;
32  foreach (MapEntity linkedEntity in turret.Item.linkedTo)
33  {
34  if (linkedEntity is Item linkedItem)
35  {
36  linkedItem.Condition = linkedItem.MaxCondition;
37  }
38  }
39  }
40  }
41  LoadAllTurrets();
42  }
43 
44  public virtual void Update(float deltaTime)
45  {
46  if (Submarine == null || Submarine.Removed) { return; }
47  OperateTurrets(deltaTime, FriendlyTag);
48  }
49 
50  protected virtual void LoadAllTurrets()
51  {
52  foreach (var turret in turrets)
53  {
54  LoadTurret(turret);
55  }
56  }
57 
58  protected void LoadTurret(Turret turret, Func<ItemPrefab, bool> ammoFilter = null)
59  {
60  foreach (var linkedItem in turret.Item.GetLinkedEntities<Item>())
61  {
62  var container = linkedItem.GetComponent<ItemContainer>();
63  if (container == null) { continue; }
64  for (int i = 0; i < container.Inventory.Capacity; i++)
65  {
66  if (container.Inventory.GetItemAt(i) != null) { continue; }
67  if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab)
68  {
69  Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine);
70  if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
71  {
72  turret.Item.Remove();
73  }
74  }
75  }
76  }
77  }
78 
79  protected void OperateTurrets(float deltaTime, Identifier friendlyTag)
80  {
81  foreach (var turret in turrets)
82  {
83  turret.UpdateAutoOperate(deltaTime, ignorePower: true, friendlyTag);
84  }
85  }
86  }
87 
88  partial class WreckAI : SubmarineTurretAI, IServerSerializable
89  {
90  public bool IsAlive { get; private set; }
91 
92  private readonly List<Item> allItems;
93  private readonly List<Item> thalamusItems;
94  private readonly List<Structure> thalamusStructures;
95  private readonly List<WayPoint> wayPoints = new List<WayPoint>();
96  private readonly List<Hull> hulls = new List<Hull>();
97  private readonly List<Item> spawnOrgans = new List<Item>();
98  private readonly Item brain;
99 
100  private bool initialCellsSpawned;
101 
102  public WreckAIConfig Config { get; private set; }
103 
104  private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
105 
106  private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
107 
108  private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
109 
110  private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
111 
112  public static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
113 
114  public static WreckAI Create(Submarine wreck)
115  {
116  var wreckAI = new WreckAI(wreck);
117  if (wreckAI.Config == null) { return null; }
118  return wreckAI;
119  }
120 
121  private WreckAI(Submarine wreck) : base(wreck)
122  {
123  GetConfig();
124  if (Config == null) { return; }
125  var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
126  var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
127  if (brainPrefab == null)
128  {
129  DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
130  return;
131  }
132  allItems = wreck.GetItems(false);
133  thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
134  hulls.AddRange(wreck.GetHulls(false));
135  var potentialBrainHulls = new List<(Hull hull, float weight)>();
136  brain = new Item(brainPrefab, Vector2.Zero, wreck);
137  thalamusItems.Add(brain);
138  Point minSize = brain.Rect.Size.Multiply(brain.Scale);
139  // Bigger hulls are allowed, but not preferred more than what's sufficent.
140  Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
141  // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
142  Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height));
143  foreach (Hull hull in hulls)
144  {
145  float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
146  float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
147  float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
148  float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
149  float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
150  if (hull.GetLinkedEntities<Hull>().Any())
151  {
152  // Ignore hulls that have any linked hulls to keep the calculations simple.
153  continue;
154  }
155  else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
156  {
157  // Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
158  continue;
159  }
160  else if (thalamusItems.Any(i => i.CurrentHull == hull))
161  {
162  // Don't create the brain in a room that already has thalamus items inside it.
163  continue;
164  }
165  else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
166  {
167  // Don't select too small rooms.
168  continue;
169  }
170  if (weight > 0)
171  {
172  potentialBrainHulls.Add((hull, weight));
173  }
174  }
175  Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient);
176  var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus);
177  if (brainHull == null)
178  {
179  DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
180  brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient);
181  }
182  if (brainHull == null)
183  {
184  DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
185  return;
186  }
187  brainHull.WaterVolume = brainHull.Volume;
188  brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
189  brain.CurrentHull = brainHull;
190  var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
191  if (backgroundPrefab != null)
192  {
193  new Structure(brainHull.Rect, backgroundPrefab, wreck);
194  }
195  var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
196  if (horizontalWallPrefab != null)
197  {
198  int height = (int)horizontalWallPrefab.Size.Y;
199  int halfHeight = height / 2;
200  int quarterHeight = halfHeight / 2;
201  new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
202  new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
203  }
204  var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
205  if (verticalWallPrefab != null)
206  {
207  int width = (int)verticalWallPrefab.Size.X;
208  int halfWidth = width / 2;
209  int quarterWidth = halfWidth / 2;
210  new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
211  new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
212  }
213  foreach (Item item in thalamusItems)
214  {
215  // Ensure that thalamus items are visible
216  item.IsLayerHidden = false;
217  if (item.HasTag(Config.Spawner))
218  {
219  if (!spawnOrgans.Contains(item))
220  {
221  spawnOrgans.Add(item);
222  if (item.CurrentHull != null)
223  {
224  // Try to flood the hull so that the spawner won't die.
225  item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
226  }
227  }
228  }
229  }
230  wayPoints.AddRange(wreck.GetWaypoints(false));
231  IsAlive = true;
232  thalamusStructures = GetThalamusEntities<Structure>(wreck, Config.Entity).ToList();
233  }
234 
235  private void GetConfig()
236  {
237  Config ??= WreckAIConfig.GetRandom();
238  if (Config == null)
239  {
240  DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
241  }
242  }
243 
244  protected override void LoadAllTurrets()
245  {
246  GetConfig();
247  foreach (var turret in turrets)
248  {
249  LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier));
250  }
251  }
252 
253  private readonly List<Item> destroyedOrgans = new List<Item>();
254  public override void Update(float deltaTime)
255  {
256  if (!IsAlive) { return; }
257  if (Submarine == null || Submarine.Removed)
258  {
259  Remove();
260  return;
261  }
262  if (brain == null || brain.Removed || brain.Condition <= 0)
263  {
264  Kill();
265  return;
266  }
267  destroyedOrgans.Clear();
268  foreach (var organ in spawnOrgans)
269  {
270  if (organ.Condition <= 0)
271  {
272  destroyedOrgans.Add(organ);
273  }
274  }
275  destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
276  if (!IsClient)
277  {
278  if (!initialCellsSpawned) { SpawnInitialCells(); }
279  }
280  bool isSomeoneNearby = false;
281  float minDist = Sonar.DefaultSonarRange * 2.0f;
282 #if SERVER
283  foreach (var client in GameMain.Server.ConnectedClients)
284  {
285  var spectatePos = client.SpectatePos;
286  if (spectatePos.HasValue)
287  {
288  if (IsCloseEnough(spectatePos.Value, minDist))
289  {
290  isSomeoneNearby = true;
291  break;
292  }
293  }
294  }
295 #else
296  if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist))
297  {
298  isSomeoneNearby = true;
299  }
300 #endif
301  if (!isSomeoneNearby)
302  {
303  foreach (Submarine submarine in Submarine.Loaded)
304  {
305  if (submarine.Info.Type != SubmarineType.Player) { continue; }
306  if (IsCloseEnough(submarine.WorldPosition, minDist))
307  {
308  isSomeoneNearby = true;
309  break;
310  }
311  }
312  }
313  if (!isSomeoneNearby)
314  {
315  foreach (Character c in Character.CharacterList)
316  {
317  if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; }
318  if (IsCloseEnough(c.WorldPosition, minDist))
319  {
320  isSomeoneNearby = true;
321  break;
322  }
323  }
324  }
325  if (!isSomeoneNearby) { return; }
326  OperateTurrets(deltaTime, Config.Entity);
327  if (!IsClient)
328  {
329  UpdateReinforcements(deltaTime);
330  }
331  }
332  private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist;
333 
334  private void SpawnInitialCells()
335  {
336  int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom + 1);
337  if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel)
338  {
339  for (int i = 0; i < brainRoomCells; i++)
340  {
341  if (!TrySpawnCell(out _, brain.CurrentHull)) { break; }
342  }
343  }
344  int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside + 1);
345  for (int i = 0; i < cellsInside; i++)
346  {
347  if (!TrySpawnCell(out _)) { break; }
348  }
349  int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside + 1);
350  // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside.
351  cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside);
352  for (int i = 0; i < cellsOutside; i++)
353  {
354  ISpatialEntity targetEntity = wayPoints.GetRandomUnsynced(wp => wp.CurrentHull == null);
355  if (targetEntity == null) { break; }
356  if (!TrySpawnCell(out _, targetEntity)) { break; }
357  }
358  initialCellsSpawned = true;
359  }
360 
361  public void Kill()
362  {
363  thalamusItems.ForEach(i => i.Condition = 0);
364  foreach (var turret in turrets)
365  {
366  // Snap all tendons
367  foreach (Item item in turret.ActiveProjectiles)
368  {
369  if (item.GetComponent<Projectile>() is { IsStuckToTarget: true })
370  {
371  item.Condition = 0;
372  }
373  }
374  }
375  FadeOutColors();
376  protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
377  if (!IsClient)
378  {
379  if (Config != null)
380  {
382  {
383  protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
384  if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
385  {
386  foreach (var character in Character.CharacterList)
387  {
388  // Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
389  // but as long as spawning is handled via status effects, I don't know if there is any better way.
390  // In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
391  // And if there was, the distance check should prevent killing the agents of a different organism.
392  if (character.SpeciesName == Config.OffensiveAgent)
393  {
394  // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
395  float maxDistance = Sonar.DefaultSonarRange;
396  if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
397  {
398  character.Kill(CauseOfDeathType.Unknown, null);
399  }
400  }
401  }
402  }
403  }
404  }
405  }
406  protectiveCells.Clear();
407  IsAlive = false;
408  }
409 
410  partial void FadeOutColors();
411 
412  public void Remove()
413  {
414  Kill();
416  thalamusItems?.Clear();
417  thalamusStructures?.Clear();
418  }
419 
420  public static void RemoveThalamusItems(Submarine wreck)
421  {
422  List<MapEntity> thalamusItems = new List<MapEntity>();
423  foreach (var wreckAiConfig in WreckAIConfig.Prefabs)
424  {
425  thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity));
426  }
427  thalamusItems = thalamusItems.Distinct().ToList();
428  foreach (MapEntity thalamusItem in thalamusItems)
429  {
430  thalamusItem.Remove();
431  wreck.PhysicsBody.FarseerBody.FixtureList.Where(f => f.UserData == thalamusItem).ForEachMod(f => wreck.PhysicsBody.FarseerBody.Remove(f));
432  }
433  }
434 
435  // The client doesn't use these, so we don't have to sync them.
436  private readonly List<Character> protectiveCells = new List<Character>();
437  // Intentionally contains duplicates.
438  private readonly List<Hull> populatedHulls = new List<Hull>();
439  private float cellSpawnTimer;
440 
441  private int MinCellsPerBrainRoom => CalculateCellCount(0, Config.MinAgentsPerBrainRoom);
442  private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
443  private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
444  private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
445  private int MinCellsInside => CalculateCellCount(3, Config.MinAgentsInside);
446  private int MaxCellsInside => CalculateCellCount(5, Config.MaxAgentsInside);
447  private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
448  private float MinWaterLevel => Config.MinWaterLevel;
449 
450  private int CalculateCellCount(int minValue, int maxValue)
451  {
452  if (maxValue == 0) { return 0; }
453  float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
454  float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
455  return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
456  }
457 
458  private float GetSpawnTime()
459  {
460  float randomFactor = Config.AgentSpawnDelayRandomFactor;
461  float delay = Config.AgentSpawnDelay;
462  float min = delay;
463  float max = delay * 6;
464  float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
465  float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
466  return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
467  }
468 
469  private void UpdateReinforcements(float deltaTime)
470  {
471  if (spawnOrgans.Count == 0) { return; }
472  cellSpawnTimer -= deltaTime;
473  if (cellSpawnTimer < 0)
474  {
475  TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced());
476  cellSpawnTimer = GetSpawnTime();
477  }
478  }
479 
480  private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
481  {
482  cell = null;
483  if (protectiveCells.Count >= MaxCellCount) { return false; }
484  if (targetEntity == null)
485  {
486  targetEntity =
487  wayPoints.GetRandomUnsynced(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ??
488  hulls.GetRandomUnsynced(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity;
489  }
490  if (targetEntity == null) { return false; }
491  if (targetEntity is Hull h)
492  {
493  populatedHulls.Add(h);
494  }
495  else if (targetEntity is WayPoint wp && wp.CurrentHull != null)
496  {
497  populatedHulls.Add(wp.CurrentHull);
498  }
499  // Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs.
500  cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
501  protectiveCells.Add(cell);
502  cell.OnDeath += OnCellDeath;
503  cellSpawnTimer = GetSpawnTime();
504  return true;
505  }
506 
507  void OnCellDeath(Character character, CauseOfDeath causeOfDeath)
508  {
509  protectiveCells.Remove(character);
510  }
511 
512 #if SERVER
513  public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null)
514  {
515  msg.WriteBoolean(IsAlive);
516  }
517 #endif
518  }
519 }
Vector2 Position
Definition: Camera.cs:398
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Entity(Submarine submarine, ushort id)
Definition: Entity.cs:90
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static readonly List< MapEntity > MapEntityList
List< Item > GetItems(bool alsoFromConnectedSubs)
List< WayPoint > GetWaypoints(bool alsoFromConnectedSubs)
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
List< Hull > GetHulls(bool alsoFromConnectedSubs)
float AgentSpawnCountDifficultyMultiplier
static readonly PrefabCollection< WreckAIConfig > Prefabs
readonly Identifier[] ForbiddenAmmunition
float AgentSpawnDelayDifficultyMultiplier
static WreckAIConfig GetRandom()
static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag)
Interface for entities that the server can send events to the clients