Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Submarine.cs
2 using Barotrauma.IO;
5 using FarseerPhysics;
6 using FarseerPhysics.Dynamics;
7 using Microsoft.Xna.Framework;
8 using System;
9 using System.Collections.Generic;
10 using System.Collections.Immutable;
11 using System.Diagnostics;
12 using System.Linq;
13 using System.Xml.Linq;
14 using Voronoi2;
15 
16 namespace Barotrauma
17 {
18  public enum Direction : byte
19  {
20  None = 0, Left = 1, Right = 2
21  }
22 
24  {
25  public SubmarineInfo Info { get; private set; }
26 
28 
29  public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f);
30  //position of the "actual submarine" which is rendered wherever the SubmarineBody is
31  //should be in an unreachable place
32  public Vector2 HiddenSubPosition
33  {
34  get;
35  private set;
36  }
37 
38  public ushort IdOffset
39  {
40  get;
41  private set;
42  }
43 
44  public static bool LockX, LockY;
45 
46  public static readonly Vector2 GridSize = new Vector2(16.0f, 16.0f);
47 
48  public static readonly Submarine[] MainSubs = new Submarine[2];
49  public static Submarine MainSub
50  {
51  get { return MainSubs[0]; }
52  set { MainSubs[0] = value; }
53  }
54  private static readonly List<Submarine> loaded = new List<Submarine>();
55 
56  private readonly Identifier upgradeEventIdentifier;
57 
58  private static List<MapEntity> visibleEntities;
59  public static IEnumerable<MapEntity> VisibleEntities
60  {
61  get { return visibleEntities; }
62  }
63 
64  private SubmarineBody subBody;
65 
66  public readonly Dictionary<Submarine, DockingPort> ConnectedDockingPorts;
67  public IEnumerable<Submarine> DockedTo
68  {
69  get
70  {
71  if (ConnectedDockingPorts == null) { yield break; }
72  foreach (Submarine sub in ConnectedDockingPorts.Keys)
73  {
74  yield return sub;
75  }
76  }
77  }
78 
79  private static Vector2 lastPickedPosition;
80  private static float lastPickedFraction;
81  private static Fixture lastPickedFixture;
82  private static Vector2 lastPickedNormal;
83 
84  private Vector2 prevPosition;
85 
86  private float networkUpdateTimer;
87 
88  private EntityGrid entityGrid = null;
89 
90  //properties ----------------------------------------------------
91 
92  public bool ShowSonarMarker = true;
93 
94  public static Vector2 LastPickedPosition
95  {
96  get { return lastPickedPosition; }
97  }
98 
99  public static float LastPickedFraction
100  {
101  get { return lastPickedFraction; }
102  }
103 
104  public static Fixture LastPickedFixture
105  {
106  get { return lastPickedFixture; }
107  }
108 
109  public static Vector2 LastPickedNormal
110  {
111  get { return lastPickedNormal; }
112  }
113 
114  public bool Loading
115  {
116  get;
117  private set;
118  }
119 
120  public bool GodMode
121  {
122  get;
123  set;
124  }
125 
126  public static List<Submarine> Loaded
127  {
128  get { return loaded; }
129  }
130 
132  {
133  get { return subBody; }
134  }
135 
137  {
138  get { return subBody?.Body; }
139  }
140 
145  {
146  get
147  {
148  return subBody == null ? Rectangle.Empty : subBody.Borders;
149  }
150  }
151 
156  {
157  get
158  {
159  return subBody == null ? Rectangle.Empty : subBody.VisibleBorders;
160  }
161  }
162 
163  public override Vector2 Position
164  {
165  get { return subBody == null ? Vector2.Zero : subBody.Position - HiddenSubPosition; }
166  }
167 
168  public override Vector2 WorldPosition
169  {
170  get
171  {
172  return subBody == null ? Vector2.Zero : subBody.Position;
173  }
174  }
175 
176  private float? realWorldCrushDepth;
177  public float RealWorldCrushDepth
178  {
179  get
180  {
181  if (!realWorldCrushDepth.HasValue)
182  {
183  realWorldCrushDepth = float.PositiveInfinity;
184  foreach (Structure structure in Structure.WallList)
185  {
186  if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; }
187  realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value);
188  }
189  }
190  return realWorldCrushDepth.Value;
191  }
192  }
193 
197  public float RealWorldDepth
198  {
199  get
200  {
201  if (Level.Loaded?.GenerationParams == null)
202  {
203  return -WorldPosition.Y * Physics.DisplayToRealWorldRatio;
204  }
206  }
207  }
208 
209  public bool AtEndExit
210  {
211  get
212  {
213  if (Level.Loaded == null) { return false; }
214  if (Level.Loaded.EndOutpost != null)
215  {
216  if (DockedTo.Contains(Level.Loaded.EndOutpost))
217  {
218  return true;
219  }
220  else if (Level.Loaded.EndOutpost.exitPoints.Any())
221  {
222  return IsAtOutpostExit(Level.Loaded.EndOutpost);
223  }
224  }
225  else if (Level.Loaded.Type == LevelData.LevelType.Outpost && Level.Loaded.StartOutpost != null)
226  {
227  //in outpost levels, the outpost is always the start outpost: check it if has an exit
228  return IsAtOutpostExit(Level.Loaded.StartOutpost);
229  }
230  return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance);
231  }
232  }
233 
234  public bool AtStartExit
235  {
236  get
237  {
238  if (Level.Loaded == null) { return false; }
239  if (Level.Loaded.StartOutpost != null)
240  {
241  if (DockedTo.Contains(Level.Loaded.StartOutpost))
242  {
243  return true;
244  }
245  else if (Level.Loaded.StartOutpost.exitPoints.Any())
246  {
247  return IsAtOutpostExit(Level.Loaded.StartOutpost);
248  }
249  }
251  }
252  }
253 
254  public bool AtEitherExit => AtStartExit || AtEndExit;
255 
256  private bool IsAtOutpostExit(Submarine outpost)
257  {
258  if (outpost.exitPoints.Any())
259  {
260  Rectangle worldBorders = GetDockedBorders();
261  worldBorders.Location += WorldPosition.ToPoint();
262  foreach (var exitPoint in outpost.exitPoints)
263  {
264  if (exitPoint.ExitPointSize != Point.Zero)
265  {
266  if (RectsOverlap(worldBorders, exitPoint.ExitPointWorldRect)) { return true; }
267  }
268  else
269  {
270  if (RectContains(worldBorders, exitPoint.WorldPosition)) { return true; }
271  }
272  }
273  }
274  return false;
275  }
276 
277 
278  public new Vector2 DrawPosition
279  {
280  get;
281  private set;
282  }
283 
284  public override Vector2 SimPosition
285  {
286  get
287  {
288  return ConvertUnits.ToSimUnits(Position);
289  }
290  }
291 
292  public Vector2 Velocity
293  {
294  get { return subBody == null ? Vector2.Zero : subBody.Velocity; }
295  set
296  {
297  if (subBody == null) { return; }
298  subBody.Velocity = value;
299  }
300  }
301 
302  public List<Vector2> HullVertices
303  {
304  get { return subBody?.HullVertices; }
305  }
306 
307  private int? submarineSpecificIDTag;
309  {
310  get
311  {
312  submarineSpecificIDTag ??= ToolBox.StringToInt((Level.Loaded?.Seed ?? "") + Info.Name);
313  return submarineSpecificIDTag.Value;
314  }
315  }
316 
317 
318  public bool AtDamageDepth
319  {
320  get
321  {
322  if (Level.Loaded == null || subBody == null) { return false; }
324  }
325  }
326 
327  private readonly List<WayPoint> exitPoints = new List<WayPoint>();
328  public IReadOnlyList<WayPoint> ExitPoints { get { return exitPoints; } }
329 
330  public override string ToString()
331  {
332  return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")";
333  }
334 
335  public int CalculateBasePrice()
336  {
337  int minPrice = 1000;
338  float volume = Hull.HullList.Where(h => h.Submarine == this).Sum(h => h.Volume);
339  float itemValue = Item.ItemList.Where(it => it.Submarine == this).Sum(it => it.Prefab.GetMinPrice() ?? 0);
340  float price = volume / 500.0f + itemValue / 100.0f;
341  System.Diagnostics.Debug.Assert(price >= 0);
342  return Math.Max(minPrice, (int)price);
343  }
344 
345  private float ballastFloraTimer;
346  public bool ImmuneToBallastFlora { get; set; }
347  public void AttemptBallastFloraInfection(Identifier identifier, float deltaTime, float probability)
348  {
349  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
350  if (ImmuneToBallastFlora) { return; }
351 
352  if (ballastFloraTimer < 1f)
353  {
354  ballastFloraTimer += deltaTime;
355  return;
356  }
357 
358  ballastFloraTimer = 0;
359  if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) >= probability) { return; }
360 
361  List<Pump> pumps = new List<Pump>();
362  List<Item> allItems = GetItems(true);
363 
364  bool anyHasTag = allItems.Any(i => i.HasTag(Tags.Ballast));
365 
366  foreach (Item item in allItems)
367  {
368  if ((!anyHasTag || item.HasTag(Tags.Ballast)) && item.GetComponent<Pump>() is { } pump)
369  {
370  pumps.Add(pump);
371  }
372  }
373 
374  if (!pumps.Any()) { return; }
375 
376  Pump randomPump = pumps.GetRandom(Rand.RandSync.Unsynced);
377  if (randomPump.IsOn && randomPump.HasPower && randomPump.FlowPercentage > 0 && randomPump.Item.Condition > 0.0f)
378  {
379  randomPump.InfectBallast(identifier);
380 #if SERVER
381  randomPump.Item.CreateServerEvent(randomPump);
382 #endif
383  }
384  }
385 
386  public void MakeWreck()
387  {
388  Info.Type = SubmarineType.Wreck;
389  ShowSonarMarker = false;
390  DockedTo.ForEach(s => s.ShowSonarMarker = false);
391  PhysicsBody.FarseerBody.BodyType = BodyType.Static;
392  TeamID = CharacterTeamType.None;
393  }
394 
395  public WreckAI WreckAI { get; private set; }
396  public SubmarineTurretAI TurretAI { get; private set; }
397 
398  public bool CreateWreckAI()
399  {
400  WreckAI = WreckAI.Create(this);
401  return WreckAI != null;
402  }
403 
407  public bool CreateTurretAI()
408  {
409  TurretAI = new SubmarineTurretAI(this);
410  return TurretAI != null;
411  }
412 
413  public void DisableWreckAI()
414  {
415  if (WreckAI == null)
416  {
418  }
419  else
420  {
421  WreckAI?.Remove();
422  WreckAI = null;
423  }
424  }
425 
426  private static readonly HashSet<Submarine> checkSubmarineBorders = new HashSet<Submarine>();
427 
431  public Rectangle GetDockedBorders(bool allowDifferentTeam = true)
432  {
433  checkSubmarineBorders.Clear();
434  return GetDockedBordersRecursive(allowDifferentTeam);
435  }
436 
437  private Rectangle GetDockedBordersRecursive(bool allowDifferentTeam)
438  {
439  Rectangle dockedBorders = Borders;
440  checkSubmarineBorders.Add(this);
441  var connectedSubs = DockedTo.Where(s =>
442  !checkSubmarineBorders.Contains(s) &&
443  !s.Info.IsOutpost &&
444  (allowDifferentTeam || s.TeamID == TeamID));
445  foreach (Submarine dockedSub in connectedSubs)
446  {
447  //use docking ports instead of world position to determine
448  //borders, as world position will not necessarily match where
449  //the subs are supposed to go
450  Vector2? expectedLocation = CalculateDockOffset(this, dockedSub);
451  if (expectedLocation == null) { continue; }
452 
453  Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive(allowDifferentTeam);
454  dockedSubBorders.Location += MathUtils.ToPoint(expectedLocation.Value);
455 
456  dockedBorders.Y = -dockedBorders.Y;
457  dockedSubBorders.Y = -dockedSubBorders.Y;
458  dockedBorders = Rectangle.Union(dockedBorders, dockedSubBorders);
459  dockedBorders.Y = -dockedBorders.Y;
460  }
461 
462  return dockedBorders;
463  }
464 
465  private readonly HashSet<Submarine> connectedSubs;
469  public IEnumerable<Submarine> GetConnectedSubs()
470  {
471  return connectedSubs;
472  }
473 
474  public void RefreshConnectedSubs()
475  {
476  connectedSubs.Clear();
477  connectedSubs.Add(this);
478  GetConnectedSubsRecursive(connectedSubs);
479  }
480 
481  private void GetConnectedSubsRecursive(HashSet<Submarine> subs)
482  {
483  foreach (Submarine dockedSub in DockedTo)
484  {
485  if (subs.Contains(dockedSub)) { continue; }
486  subs.Add(dockedSub);
487  dockedSub.GetConnectedSubsRecursive(subs);
488  }
489  }
490 
494  public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f, int verticalMoveDir = 0)
495  {
496  Rectangle dockedBorders = GetDockedBorders();
497  Vector2 diffFromDockedBorders =
498  new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)
499  - new Vector2(Borders.Center.X, Borders.Y - Borders.Height / 2);
500 
501  int minWidth = Math.Max(submarineSize.HasValue ? submarineSize.Value.X : dockedBorders.Width, 500);
502  int minHeight = Math.Max(submarineSize.HasValue ? submarineSize.Value.Y : dockedBorders.Height, 1000);
503  //a bit of extra padding to prevent the sub from spawning in a super tight gap between walls
504  int padding = 100;
505  minWidth += padding;
506  minHeight += padding;
507 
508  int iterations = 0;
509  const int maxIterations = 5;
510  do
511  {
512  Vector2 potentialPos = spawnPos;
513  if (verticalMoveDir != 0)
514  {
515  verticalMoveDir = Math.Sign(verticalMoveDir);
516  //do a raycast towards the top/bottom of the level depending on direction
517  Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
518 
519  Vector2 closestPickedPos = rayEnd;
520  //multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub)
521  for (float x = -1; x <= 1; x += 0.2f)
522  {
523  Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
524  xOffset.X += subDockingPortOffset;
525  if (PickBody(
526  ConvertUnits.ToSimUnits(potentialPos + xOffset),
527  ConvertUnits.ToSimUnits(rayEnd + xOffset),
528  collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
529  customPredicate: (Fixture f) =>
530  {
531  return f.UserData is not VoronoiCell { IsDestructible: true };
532  }) != null)
533  {
534  //if the raycast hit a wall, attempt to place the spawnpos there
535  int offsetFromWall = 10 * -verticalMoveDir;
536  float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall;
537  closestPickedPos.Y =
538  verticalMoveDir > 0 ?
539  Math.Min(closestPickedPos.Y, pickedPos) :
540  Math.Max(closestPickedPos.Y, pickedPos);
541  }
542  }
543  potentialPos.Y = closestPickedPos.Y;
544  }
545 
546  Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)),
547  maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding);
548  if (limits.Y - limits.X >= minWidth)
549  {
550  Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir));
551  bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir);
552  if (!couldMoveInVerticalMoveDir) { break; }
553  spawnPos = ClampToHorizontalLimits(newSpawnPos, limits);
554  }
555 
556  iterations++;
557  } while (iterations < maxIterations);
558 
559  Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding)
560  {
561  Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir);
562 
563  float minX = float.MinValue, maxX = float.MaxValue;
564  foreach (VoronoiCell cell in Level.Loaded.GetAllCells())
565  {
566  foreach (GraphEdge e in cell.Edges)
567  {
568  if ((e.Point1.Y < refPos.Y - minHeight * 0.5f && e.Point2.Y < refPos.Y - minHeight * 0.5f) ||
569  (e.Point1.Y > refPos.Y + minHeight * 0.5f && e.Point2.Y > refPos.Y + minHeight * 0.5f))
570  {
571  continue;
572  }
573 
574  if (cell.Site.Coord.X < refPos.X)
575  {
576  minX = Math.Max(minX, Math.Max(e.Point1.X, e.Point2.X));
577  }
578  else
579  {
580  maxX = Math.Min(maxX, Math.Min(e.Point1.X, e.Point2.X));
581  }
582  }
583  }
584 
585  foreach (var ruin in Level.Loaded.Ruins)
586  {
587  if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; }
588  if (ruin.Area.Center.X < refPos.X)
589  {
590  minX = Math.Max(minX, ruin.Area.Right + padding);
591  }
592  else
593  {
594  maxX = Math.Min(maxX, ruin.Area.X - padding);
595  }
596  }
597 
598  minX += subDockingPortOffset;
599  maxX += subDockingPortOffset;
600 
601  return new Vector2(
602  Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0),
603  Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X));
604  }
605 
606  Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits)
607  {
608  if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
609  {
610  //no walls found at either side, just use the initial spawnpos and hope for the best
611  }
612  else if (limits.X < 0)
613  {
614  //no wall found at the left side, spawn to the left from the right-side wall
615  spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
616  }
617  else if (limits.Y > Level.Loaded.Size.X)
618  {
619  //no wall found at right side, spawn to the right from the left-side wall
620  spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
621  }
622  else
623  {
624  //walls found at both sides, use their midpoint
625  spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
626  }
627  return spawnPos;
628  }
629 
630  spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
631  return spawnPos - diffFromDockedBorders;
632  }
633 
634  public void UpdateTransform(bool interpolate = true)
635  {
636  DrawPosition = interpolate ?
637  Timing.Interpolate(prevPosition, Position) :
638  Position;
639  if (!interpolate) { prevPosition = Position; }
640  }
641 
642  //math/physics stuff ----------------------------------------------------
643 
644  public static Vector2 VectorToWorldGrid(Vector2 position, Submarine sub = null, bool round = false)
645  {
646  if (round)
647  {
648  position.X = MathF.Round(position.X / GridSize.X) * GridSize.X;
649  position.Y = MathF.Round(position.Y / GridSize.Y) * GridSize.Y;
650  }
651  else
652  {
653  position.X = MathF.Floor(position.X / GridSize.X) * GridSize.X;
654  position.Y = MathF.Ceiling(position.Y / GridSize.Y) * GridSize.Y;
655  }
656 
657  if (sub != null)
658  {
659  position.X += sub.Position.X % GridSize.X;
660  position.Y += sub.Position.Y % GridSize.Y;
661  }
662  return position;
663  }
664 
665  public Rectangle CalculateDimensions(bool onlyHulls = true)
666  {
667  List<MapEntity> entities = onlyHulls ?
668  Hull.HullList.FindAll(h => h.Submarine == this).Cast<MapEntity>().ToList() :
669  MapEntity.MapEntityList.FindAll(me => me.Submarine == this);
670 
671  //ignore items whose body is disabled (wires, items inside cabinets)
672  entities.RemoveAll(e =>
673  {
674  if (e is Item item)
675  {
676  if (item.GetComponent<Turret>() != null) { return false; }
677  if (item.body != null && !item.body.Enabled) { return true; }
678  }
679  if (e.IsHidden) { return true; }
680  return false;
681  });
682 
683  if (entities.Count == 0) { return Rectangle.Empty; }
684 
685  float minX = entities[0].Rect.X, minY = entities[0].Rect.Y - entities[0].Rect.Height;
686  float maxX = entities[0].Rect.Right, maxY = entities[0].Rect.Y;
687 
688  for (int i = 1; i < entities.Count; i++)
689  {
690  if (entities[i] is Item item)
691  {
692  var turret = item.GetComponent<Turret>();
693  if (turret != null)
694  {
695  minX = Math.Min(minX, entities[i].Rect.X + turret.TransformedBarrelPos.X * 2f);
696  minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height - turret.TransformedBarrelPos.Y * 2f);
697  maxX = Math.Max(maxX, entities[i].Rect.Right + turret.TransformedBarrelPos.X * 2f);
698  maxY = Math.Max(maxY, entities[i].Rect.Y - turret.TransformedBarrelPos.Y * 2f);
699  }
700  }
701  minX = Math.Min(minX, entities[i].Rect.X);
702  minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height);
703  maxX = Math.Max(maxX, entities[i].Rect.Right);
704  maxY = Math.Max(maxY, entities[i].Rect.Y);
705  }
706 
707  return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
708  }
709 
710  public static Rectangle AbsRect(Vector2 pos, Vector2 size)
711  {
712  if (size.X < 0.0f)
713  {
714  pos.X += size.X;
715  size.X = -size.X;
716  }
717  if (size.Y < 0.0f)
718  {
719  pos.Y -= size.Y;
720  size.Y = -size.Y;
721  }
722 
723  return new Rectangle((int)pos.X, (int)pos.Y, (int)size.X, (int)size.Y);
724  }
725 
726  public static RectangleF AbsRectF(Vector2 pos, Vector2 size)
727  {
728  if (size.X < 0.0f)
729  {
730  pos.X += size.X;
731  size.X = -size.X;
732  }
733  if (size.Y < 0.0f)
734  {
735  pos.Y += size.Y;
736  size.Y = -size.Y;
737  }
738 
739  return new RectangleF(pos.X, pos.Y, size.X, size.Y);
740  }
741 
742  public static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive = false)
743  {
744  if (inclusive)
745  {
746  return (pos.X >= rect.X && pos.X <= rect.X + rect.Width
747  && pos.Y <= rect.Y && pos.Y >= rect.Y - rect.Height);
748  }
749  else
750  {
751  return (pos.X > rect.X && pos.X < rect.X + rect.Width
752  && pos.Y < rect.Y && pos.Y > rect.Y - rect.Height);
753  }
754  }
755 
756  public static bool RectsOverlap(Rectangle rect1, Rectangle rect2, bool inclusive = true)
757  {
758  if (inclusive)
759  {
760  return !(rect1.X > rect2.X + rect2.Width || rect1.X + rect1.Width < rect2.X ||
761  rect1.Y < rect2.Y - rect2.Height || rect1.Y - rect1.Height > rect2.Y);
762  }
763  else
764  {
765  return !(rect1.X >= rect2.X + rect2.Width || rect1.X + rect1.Width <= rect2.X ||
766  rect1.Y <= rect2.Y - rect2.Height || rect1.Y - rect1.Height >= rect2.Y);
767  }
768  }
769 
770  public static bool RectsOverlap(RectangleF rect1, RectangleF rect2, bool inclusive = true)
771  {
772  if (inclusive)
773  {
774  return !(rect1.X > rect2.X + rect2.Width || rect1.X + rect1.Width < rect2.X ||
775  rect1.Y < rect2.Y - rect2.Height || rect1.Y - rect1.Height > rect2.Y);
776  }
777 
778  return !(rect1.X >= rect2.X + rect2.Width || rect1.X + rect1.Width <= rect2.X ||
779  rect1.Y <= rect2.Y - rect2.Height || rect1.Y - rect1.Height >= rect2.Y);
780  }
781 
782  public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable<Body> ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate<Fixture> customPredicate = null, bool allowInsideFixture = false)
783  {
784  if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.0001f)
785  {
786  return null;
787  }
788 
789  float closestFraction = 1.0f;
790  Vector2 closestNormal = Vector2.Zero;
791  Fixture closestFixture = null;
792  Body closestBody = null;
793  if (allowInsideFixture)
794  {
795  var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
796  GameMain.World.QueryAABB((fixture) =>
797  {
798  if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; }
799 
800  fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
801  if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
802 
803  closestFraction = 0.0f;
804  closestNormal = Vector2.Normalize(rayEnd - rayStart);
805  closestFixture = fixture;
806  if (fixture.Body != null) { closestBody = fixture.Body; }
807  return false;
808  }, ref aabb);
809  if (closestFraction <= 0.0f)
810  {
811  lastPickedPosition = rayStart;
812  lastPickedFraction = closestFraction;
813  lastPickedFixture = closestFixture;
814  lastPickedNormal = closestNormal;
815  return closestBody;
816  }
817  }
818 
819  GameMain.World.RayCast((fixture, point, normal, fraction) =>
820  {
821  if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return -1; }
822 
823  if (fraction < closestFraction)
824  {
825  closestFraction = fraction;
826  closestNormal = normal;
827  closestFixture = fixture;
828  if (fixture.Body != null) closestBody = fixture.Body;
829  }
830  return fraction;
831  }, rayStart, rayEnd, collisionCategory ?? Category.All);
832 
833  lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
834  lastPickedFraction = closestFraction;
835  lastPickedFixture = closestFixture;
836  lastPickedNormal = closestNormal;
837 
838  return closestBody;
839  }
840 
841  private static readonly Dictionary<Body, float> bodyDist = new Dictionary<Body, float>();
842  private static readonly List<Body> bodies = new List<Body>();
843 
844  public static float LastPickedBodyDist(Body body)
845  {
846  if (!bodyDist.ContainsKey(body)) { return 0.0f; }
847  return bodyDist[body];
848  }
849 
855  public static IEnumerable<Body> PickBodies(Vector2 rayStart, Vector2 rayEnd, IEnumerable<Body> ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate<Fixture> customPredicate = null, bool allowInsideFixture = false)
856  {
857  if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.00001f)
858  {
859  rayEnd += Vector2.UnitX * 0.001f;
860  }
861 
862  float closestFraction = 1.0f;
863  bodies.Clear();
864  bodyDist.Clear();
865  GameMain.World.RayCast((fixture, point, normal, fraction) =>
866  {
867  if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return -1; }
868 
869  if (fixture.Body != null)
870  {
871  bodies.Add(fixture.Body);
872  bodyDist[fixture.Body] = fraction;
873  }
874  if (fraction < closestFraction)
875  {
876  lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction;
877  lastPickedFraction = fraction;
878  lastPickedNormal = normal;
879  lastPickedFixture = fixture;
880  }
881  //continue
882  return -1;
883  }, rayStart, rayEnd, collisionCategory ?? Category.All);
884 
885  if (allowInsideFixture)
886  {
887  var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
888  GameMain.World.QueryAABB((fixture) =>
889  {
890  if (bodies.Contains(fixture.Body) || fixture.Body == null) { return true; }
891  if (!CheckFixtureCollision(fixture, ignoredBodies, collisionCategory, ignoreSensors, customPredicate)) { return true; }
892 
893  fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
894  if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
895 
896  closestFraction = 0.0f;
897  lastPickedPosition = rayStart;
898  lastPickedFraction = 0.0f;
899  lastPickedNormal = Vector2.Normalize(rayEnd - rayStart);
900  lastPickedFixture = fixture;
901  bodies.Add(fixture.Body);
902  bodyDist[fixture.Body] = 0.0f;
903  return false;
904  }, ref aabb);
905  }
906 
907  bodies.Sort((b1, b2) => { return bodyDist[b1].CompareTo(bodyDist[b2]); });
908  return bodies;
909  }
910 
911  private static bool CheckFixtureCollision(Fixture fixture, IEnumerable<Body> ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate<Fixture> customPredicate = null)
912  {
913  if (fixture == null ||
914  (ignoreSensors && fixture.IsSensor) ||
915  fixture.CollisionCategories == Category.None ||
916  fixture.CollisionCategories == Physics.CollisionItem)
917  {
918  return false;
919  }
920 
921  if (customPredicate != null && !customPredicate(fixture))
922  {
923  return false;
924  }
925 
926  if (collisionCategory != null &&
927  !fixture.CollisionCategories.HasFlag((Category)collisionCategory) &&
928  !((Category)collisionCategory).HasFlag(fixture.CollisionCategories))
929  {
930  return false;
931  }
932 
933  if (ignoredBodies != null && ignoredBodies.Contains(fixture.Body))
934  {
935  return false;
936  }
937 
938  if (fixture.Body.UserData is Structure structure)
939  {
940  if (structure.IsPlatform && collisionCategory != null && !((Category)collisionCategory).HasFlag(Physics.CollisionPlatform))
941  {
942  return false;
943  }
944  }
945 
946  return true;
947  }
948 
949 
954 
958  public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true,
959  Predicate<Fixture> blocksVisibilityPredicate = null)
960  {
961  Body closestBody = null;
962  float closestFraction = 1.0f;
963  Fixture closestFixture = null;
964  Vector2 closestNormal = Vector2.Zero;
965 
966  if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f)
967  {
968  lastPickedPosition = rayEnd;
969  return null;
970  }
971 
972  GameMain.World.RayCast((fixture, point, normal, fraction) =>
973  {
974  if (fixture == null) { return -1; }
975  if (ignoreSensors && fixture.IsSensor) { return -1; }
976  if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; }
977  if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)
978  && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall)
979  && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepairableWall)) { return -1; }
980  if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; }
981  if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; }
982  if (fixture.Body.UserData as string == "ruinroom") { return -1; }
983  //the hulls have solid fixtures in the submarine's world space collider, ignore them
984  if (fixture.UserData is Hull) { return -1; }
985  if (fixture.Body.UserData is Structure structure)
986  {
987  if (structure.IsPlatform || structure.StairDirection != Direction.None) { return -1; }
988  if (ignoreDisabledWalls)
989  {
990  int sectionIndex = structure.FindSectionIndex(ConvertUnits.ToDisplayUnits(point));
991  if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; }
992  }
993  }
994  if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture))
995  {
996  return -1;
997  }
998  if (fraction < closestFraction)
999  {
1000  closestBody = fixture.Body;
1001  closestFraction = fraction;
1002  closestFixture = fixture;
1003  closestNormal = normal;
1004  }
1005  return closestFraction;
1006  }
1007  , rayStart, rayEnd);
1008 
1009 
1010  lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
1011  lastPickedFraction = closestFraction;
1012  lastPickedFixture = closestFixture;
1013  lastPickedNormal = closestNormal;
1014  return closestBody;
1015  }
1016 
1017  //movement ----------------------------------------------------
1018 
1019  private bool flippedX;
1020  public bool FlippedX
1021  {
1022  get { return flippedX; }
1023  }
1024 
1025  public void FlipX(List<Submarine> parents = null)
1026  {
1027  if (parents == null) { parents = new List<Submarine>(); }
1028  parents.Add(this);
1029 
1030  flippedX = !flippedX;
1031 
1032  Item.UpdateHulls();
1033 
1034  List<Item> bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null);
1035  List<MapEntity> subEntities = MapEntity.MapEntityList.FindAll(me => me.Submarine == this);
1036 
1037  foreach (MapEntity e in subEntities)
1038  {
1039  if (e is Item) continue;
1040  if (e is LinkedSubmarine linkedSub)
1041  {
1042  Submarine sub = linkedSub.Sub;
1043  if (sub == null)
1044  {
1045  Vector2 relative1 = linkedSub.Position - SubBody.Position;
1046  relative1.X = -relative1.X;
1047  linkedSub.Rect = new Rectangle((relative1 + SubBody.Position).ToPoint(), linkedSub.Rect.Size);
1048  }
1049  else if (!parents.Contains(sub))
1050  {
1051  Vector2 relative1 = sub.SubBody.Position - SubBody.Position;
1052  relative1.X = -relative1.X;
1053  sub.SetPosition(relative1 + SubBody.Position, new List<Submarine>(parents));
1054  sub.FlipX(parents);
1055  }
1056  }
1057  else
1058  {
1059  e.FlipX(true);
1060  }
1061  }
1062 
1063  foreach (MapEntity mapEntity in subEntities)
1064  {
1065  mapEntity.Move(-HiddenSubPosition);
1066  }
1067 
1068  var prevBodyType = subBody.Body.BodyType;
1069  Vector2 pos = new Vector2(subBody.Position.X, subBody.Position.Y);
1070  subBody.Body.Remove();
1071  subBody = new SubmarineBody(this);
1072  subBody.Body.BodyType = prevBodyType;
1073  SetPosition(pos, new List<Submarine>(parents.Where(p => p != this)));
1074 
1075  if (entityGrid != null)
1076  {
1077  Hull.EntityGrids.Remove(entityGrid);
1078  entityGrid = null;
1079  }
1080  entityGrid = Hull.GenerateEntityGrid(this);
1081 
1082  SubBody.FlipX();
1083 
1084  foreach (MapEntity mapEntity in subEntities)
1085  {
1086  mapEntity.Move(HiddenSubPosition);
1087  }
1088 
1089  for (int i = 0; i < 2; i++)
1090  {
1091  foreach (Item item in Item.ItemList)
1092  {
1093  //two passes: flip docking ports on the 2nd pass because the doors need to be correctly flipped for the port's orientation to be determined correctly
1094  if ((item.GetComponent<DockingPort>() != null) == (i == 0)) { continue; }
1095  if (bodyItems.Contains(item))
1096  {
1097  item.Submarine = this;
1098  if (Position == Vector2.Zero) { item.Move(-HiddenSubPosition); }
1099  }
1100  else if (item.Submarine != this)
1101  {
1102  continue;
1103  }
1104  item.FlipX(true);
1105 
1106  if (!item.Prefab.CanFlipX && item.Prefab.AllowRotatingInEditor)
1107  {
1108  item.Rotation = -item.Rotation;
1109  }
1110  }
1111  }
1112 
1113  Item.UpdateHulls();
1114  Gap.UpdateHulls();
1115 #if CLIENT
1116  Lights.ConvexHull.RecalculateAll(this);
1117 #endif
1118  }
1119 
1120  public void EnableFactionSpecificEntities(Identifier factionIdentifier)
1121  {
1122  foreach (var faction in FactionPrefab.Prefabs)
1123  {
1124  SetLayerEnabled(faction.Identifier, faction.Identifier == factionIdentifier);
1125  }
1126  }
1127 
1128  public static bool LayerExistsInAnySub(Identifier layer)
1129  {
1130  foreach (MapEntity me in MapEntity.MapEntityList)
1131  {
1132  if (me.Layer == layer) { return true; }
1133  }
1134  return false;
1135  }
1136 
1137  public bool LayerExists(Identifier layer)
1138  {
1139  foreach (MapEntity me in MapEntity.MapEntityList)
1140  {
1141  if (me.Submarine == this || me.Layer == layer) { return true; }
1142  }
1143  return false;
1144  }
1145 
1146  public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false)
1147  {
1148  foreach (MapEntity entity in MapEntity.MapEntityList)
1149  {
1150  if (string.IsNullOrEmpty(entity.Layer) || entity.Submarine != this || entity.Layer != layer) { continue; }
1151  entity.IsLayerHidden = !enabled;
1152 
1153  if (entity is WayPoint wp)
1154  {
1155  if (enabled)
1156  {
1157  wp.SpawnType = wp.SpawnType.RemoveFlag(SpawnType.Disabled);
1158  }
1159  else
1160  {
1161  wp.SpawnType = wp.SpawnType.AddFlag(SpawnType.Disabled);
1162  }
1163  }
1164  else if (entity is Item item)
1165  {
1166  foreach (var connectionPanel in item.GetComponents<ConnectionPanel>())
1167  {
1168  foreach (var connection in connectionPanel.Connections)
1169  {
1170  foreach (var wire in connection.Wires)
1171  {
1172  wire.Item.IsLayerHidden = entity.IsLayerHidden;
1173  }
1174  }
1175  }
1176 #if CLIENT
1177  if (entity.IsLayerHidden)
1178  {
1179  //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that
1180  foreach (var lightComponent in item.GetComponents<LightComponent>())
1181  {
1182  lightComponent.Light.Enabled = false;
1183  }
1184  }
1185 #endif
1186  }
1187  }
1188 #if SERVER
1189  if (sendNetworkEvent)
1190  {
1191  GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled));
1192  }
1193 #endif
1194  }
1195 
1196  public void Update(float deltaTime)
1197  {
1198  RefreshConnectedSubs();
1199 
1200  if (Info.IsWreck)
1201  {
1202  WreckAI?.Update(deltaTime);
1203  }
1204  TurretAI?.Update(deltaTime);
1205 
1206  if (subBody?.Body == null) { return; }
1207 
1208  if (Level.Loaded != null &&
1209  WorldPosition.Y < Level.MaxEntityDepth &&
1210  subBody.Body.Enabled &&
1211  (GameMain.NetworkMember?.RespawnManager == null || this != GameMain.NetworkMember.RespawnManager.RespawnShuttle))
1212  {
1213  subBody.Body.ResetDynamics();
1214  subBody.Body.Enabled = false;
1215 
1216  foreach (Character c in Character.CharacterList)
1217  {
1218  if (c.Submarine == this)
1219  {
1220  c.Kill(CauseOfDeathType.Pressure, null);
1221  c.Enabled = false;
1222  }
1223  }
1224 
1225  return;
1226  }
1227 
1228 
1229  subBody.Body.LinearVelocity = new Vector2(
1230  LockX ? 0.0f : subBody.Body.LinearVelocity.X,
1231  LockY ? 0.0f : subBody.Body.LinearVelocity.Y);
1232 
1233  subBody.Update(deltaTime);
1234 
1235  for (int i = 0; i < 2; i++)
1236  {
1237  if (MainSubs[i] == null) { continue; }
1238  if (this != MainSubs[i] && MainSubs[i].DockedTo.Contains(this)) { return; }
1239  }
1240 
1241  //send updates more frequently if moving fast
1242  networkUpdateTimer -= MathHelper.Clamp(Velocity.Length() * 10.0f, 0.1f, 5.0f) * deltaTime;
1243 
1244  if (networkUpdateTimer < 0.0f)
1245  {
1246  networkUpdateTimer = 1.0f;
1247  }
1248  }
1249 
1250  public void ApplyForce(Vector2 force)
1251  {
1252  if (subBody != null) { subBody.ApplyForce(force); }
1253  }
1254 
1256  {
1257  foreach (Item item in Item.ItemList)
1258  {
1259  if (item.Submarine != this) { continue; }
1260  var steering = item.GetComponent<Steering>();
1261  if (steering == null || item.Connections == null) { continue; }
1262 
1263  //find all the engines and pumps the nav terminal is connected to
1264  List<Item> connectedItems = new List<Item>();
1265  foreach (Connection c in item.Connections)
1266  {
1267  if (c.IsPower) { continue; }
1268  connectedItems.AddRange(item.GetConnectedComponentsRecursive<Engine>(c).Select(engine => engine.Item));
1269  connectedItems.AddRange(item.GetConnectedComponentsRecursive<Pump>(c).Select(pump => pump.Item));
1270  }
1271 
1272  //if more than 50% of the connected engines/pumps are in another sub,
1273  //assume this terminal is used to remotely control something and don't automatically enable autopilot
1274  if (connectedItems.Count(it => it.Submarine != item.Submarine) > connectedItems.Count / 2)
1275  {
1276  continue;
1277  }
1278 
1279  steering.MaintainPos = true;
1280  steering.PosToMaintain = WorldPosition;
1281  steering.AutoPilot = true;
1282 #if SERVER
1283  steering.UnsentChanges = true;
1284 #endif
1285  }
1286  }
1287 
1288  public void NeutralizeBallast()
1289  {
1290  if (PhysicsBody.BodyType != BodyType.Dynamic) { return; }
1291 
1292  float neutralBallastLevel = 0.5f;
1293  int selectedSteeringValue = 0;
1294  foreach (Item item in Item.ItemList)
1295  {
1296  if (item.Submarine != this) { continue; }
1297  var steering = item.GetComponent<Steering>();
1298  if (steering == null) { continue; }
1299 
1300  //find how many pumps/engines in this sub the steering item is connected to
1301  int steeringValue = 1;
1302  Connection connectionX = item.GetComponent<ConnectionPanel>()?.Connections.Find(static c => c.Name == "velocity_x_out");
1303  Connection connectionY = item.GetComponent<ConnectionPanel>()?.Connections.Find(static c => c.Name == "velocity_y_out");
1304  if (connectionX != null)
1305  {
1306  foreach (Engine engine in steering.Item.GetConnectedComponentsRecursive<Engine>(connectionX))
1307  {
1308  if (engine.Item.Submarine == this) { steeringValue++; }
1309  }
1310  }
1311  if (connectionY != null)
1312  {
1313  foreach (Pump pump in steering.Item.GetConnectedComponentsRecursive<Pump>(connectionY))
1314  {
1315  if (pump.Item.Submarine == this) { steeringValue++; }
1316  }
1317  }
1318  //the nav terminal that's connected to the most engines/pumps in the sub most likely controls the sub (instead of a shuttle or some other system)
1319  if (steeringValue > selectedSteeringValue)
1320  {
1321  neutralBallastLevel = steering.NeutralBallastLevel;
1322  }
1323  }
1324 
1325  HashSet<Hull> ballastHulls = new HashSet<Hull>();
1326  foreach (Item item in Item.ItemList)
1327  {
1328  if (item.Submarine != this) { continue; }
1329  var pump = item.GetComponent<Pump>();
1330  if (pump == null || item.CurrentHull == null) { continue; }
1331  if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
1332  pump.FlowPercentage = 0.0f;
1333  ballastHulls.Add(item.CurrentHull);
1334  }
1335 
1336  float waterVolume = 0.0f;
1337  float volume = 0.0f;
1338  float excessWater = 0.0f;
1339  foreach (Hull hull in Hull.HullList)
1340  {
1341  if (hull.Submarine != this) { continue; }
1342  waterVolume += hull.WaterVolume;
1343  volume += hull.Volume;
1344  if (!ballastHulls.Contains(hull)) { excessWater += hull.WaterVolume; }
1345  }
1346 
1347  neutralBallastLevel -= excessWater / volume;
1348  //reduce a bit to be on the safe side (better to float up than sink)
1349  neutralBallastLevel *= 0.9f;
1350 
1351  foreach (Hull hull in ballastHulls)
1352  {
1353  hull.WaterVolume = hull.Volume * neutralBallastLevel;
1354  }
1355  }
1356 
1357  public void SetPrevTransform(Vector2 position)
1358  {
1359  prevPosition = position;
1360  }
1361 
1362  public void SetPosition(Vector2 position, List<Submarine> checkd = null, bool forceUndockFromStaticSubmarines = true)
1363  {
1364  if (!MathUtils.IsValid(position)) { return; }
1365 
1366  if (checkd == null) { checkd = new List<Submarine>(); }
1367  if (checkd.Contains(this)) { return; }
1368 
1369  checkd.Add(this);
1370 
1371  subBody.SetPosition(position);
1372  UpdateTransform(interpolate: false);
1373 
1374  foreach (Submarine dockedSub in DockedTo)
1375  {
1376  if (dockedSub.PhysicsBody.BodyType == BodyType.Static && forceUndockFromStaticSubmarines)
1377  {
1378  if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port))
1379  {
1380  port.Undock(applyEffects: false);
1381  continue;
1382  }
1383  }
1384  Vector2? expectedLocation = CalculateDockOffset(this, dockedSub);
1385  if (expectedLocation == null) { continue; }
1386  dockedSub.SetPosition(position + expectedLocation.Value, checkd, forceUndockFromStaticSubmarines);
1387  dockedSub.UpdateTransform(interpolate: false);
1388  }
1389  }
1390 
1391  public static Vector2? CalculateDockOffset(Submarine sub, Submarine dockedSub)
1392  {
1393  Item myPort = sub.ConnectedDockingPorts.ContainsKey(dockedSub) ? sub.ConnectedDockingPorts[dockedSub].Item : null;
1394  if (myPort == null) { return null; }
1395  Item theirPort = dockedSub.ConnectedDockingPorts.ContainsKey(sub) ? dockedSub.ConnectedDockingPorts[sub].Item : null;
1396  if (theirPort == null) { return null; }
1397  return (myPort.Position - sub.HiddenSubPosition) - (theirPort.Position - dockedSub.HiddenSubPosition);
1398  }
1399 
1400  public void Translate(Vector2 amount)
1401  {
1402  if (amount == Vector2.Zero || !MathUtils.IsValid(amount)) return;
1403 
1404  subBody.SetPosition(subBody.Position + amount);
1405  }
1406 
1408  public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true, bool ignoreRespawnShuttle = false, CharacterTeamType? teamType = null)
1409  {
1410  Submarine closest = null;
1411  float closestDist = 0.0f;
1412  foreach (Submarine sub in loaded)
1413  {
1414  if (ignoreOutposts && sub.Info.IsOutpost) { continue; }
1415  if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
1416  if (ignoreRespawnShuttle)
1417  {
1418  if (sub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
1419  }
1420  if (teamType.HasValue && sub.TeamID != teamType) { continue; }
1421  float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition);
1422  if (closest == null || dist < closestDist)
1423  {
1424  closest = sub;
1425  closestDist = dist;
1426  }
1427  }
1428 
1429  return closest;
1430  }
1431 
1435  public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub);
1436 
1437  public List<Hull> GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.HullList);
1438  public List<Gap> GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList);
1439  public List<Item> GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList);
1440  public List<WayPoint> GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList);
1441  public List<Structure> GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList);
1442 
1443  public List<T> GetEntities<T>(bool includingConnectedSubs, List<T> list) where T : MapEntity
1444  {
1445  return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
1446  }
1447 
1448  public List<(ItemContainer container, int freeSlots)> GetCargoContainers()
1449  {
1450  List<(ItemContainer container, int freeSlots)> containers = new List<(ItemContainer container, int freeSlots)>();
1451  var connectedSubs = GetConnectedSubs().Where(sub => sub.Info?.Type == Info.Type);
1452  foreach (Item item in Item.ItemList.ToList())
1453  {
1454  if (!connectedSubs.Contains(item.Submarine)) { continue; }
1455  if (!item.HasTag(Tags.CargoContainer)) { continue; }
1456  if (item.HasTag(Tags.DisallowCargo)) { continue; }
1457  if (item.NonInteractable || item.IsHidden) { continue; }
1458  var itemContainer = item.GetComponent<ItemContainer>();
1459  if (itemContainer == null) { continue; }
1460  int emptySlots = 0;
1461  for (int i = 0; i < itemContainer.Inventory.Capacity; i++)
1462  {
1463  if (itemContainer.Inventory.GetItemAt(i) == null) { emptySlots++; }
1464  }
1465  containers.Add((itemContainer, emptySlots));
1466  }
1467  return containers;
1468  }
1469 
1470  public IEnumerable<T> GetEntities<T>(bool includingConnectedSubs, IEnumerable<T> list) where T : MapEntity
1471  {
1472  return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs));
1473  }
1474 
1475  public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam = false, bool allowDifferentType = false)
1476  {
1477  if (entity == null) { return false; }
1478  if (entity.Submarine == this) { return true; }
1479  if (entity.Submarine == null) { return false; }
1480  if (includingConnectedSubs)
1481  {
1482  return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type));
1483  }
1484  return false;
1485  }
1486 
1491  public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f)
1492  {
1493  foreach (Submarine sub in Loaded)
1494  {
1495  Rectangle subBorders = sub.Borders;
1496  subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height);
1497  subBorders.Inflate(inflate, inflate);
1498  if (subBorders.Contains(position)) { return sub; }
1499  }
1500 
1501  return null;
1502  }
1503 
1507  public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f)
1508  {
1509  foreach (Submarine sub in Loaded)
1510  {
1511  Rectangle worldBorders = sub.Borders;
1512  worldBorders.Location += sub.WorldPosition.ToPoint() - new Point(0, sub.Borders.Height);
1513  worldBorders.Inflate(inflate, inflate);
1514  if (worldBorders.Contains(worldPosition)) { return sub; }
1515  }
1516  return null;
1517  }
1518 
1519 
1520  public static Rectangle GetBorders(XElement submarineElement)
1521  {
1522  Vector4 bounds = new Vector4(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue);
1523  foreach (XElement element in submarineElement.Elements())
1524  {
1525  if (element.Name == "Structure")
1526  {
1527  string name = element.GetAttributeString("name", "");
1528  Identifier identifier = element.GetAttributeIdentifier("identifier", "");
1529  StructurePrefab prefab = Structure.FindPrefab(name, identifier);
1530  if (prefab == null || !prefab.Body) { continue; }
1531 
1532  var rect = element.GetAttributeRect("rect", Rectangle.Empty);
1533  bounds = new Vector4(
1534  Math.Min(rect.X, bounds.X),
1535  Math.Max(rect.Y, bounds.Y),
1536  Math.Max(rect.Right, bounds.Z),
1537  Math.Min(rect.Y - rect.Height, bounds.W));
1538  }
1539  else if (element.Name == "LinkedSubmarine")
1540  {
1541  Point dimensions = element.GetAttributePoint("dimensions", Point.Zero);
1542  Point pos = element.GetAttributeVector2("pos", Vector2.Zero).ToPoint();
1543  bounds = new Vector4(
1544  Math.Min(pos.X - dimensions.X / 2, bounds.X),
1545  Math.Max(pos.Y + dimensions.Y / 2, bounds.Y),
1546  Math.Max(pos.X + dimensions.X / 2, bounds.Z),
1547  Math.Min(pos.Y - dimensions.Y / 2, bounds.W));
1548  }
1549  }
1550 
1551  if (bounds.X == float.MaxValue || bounds.Y == float.MinValue || bounds.Z == float.MinValue || bounds.W == float.MaxValue)
1552  {
1553  //no bounds found
1554  return Rectangle.Empty;
1555  }
1556 
1557  return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
1558  }
1559 
1560  public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
1561  {
1562  Stopwatch sw = Stopwatch.StartNew();
1563 
1564  connectedSubs = new HashSet<Submarine>(2)
1565  {
1566  this
1567  };
1568 
1569  upgradeEventIdentifier = new Identifier($"Submarine{ID}");
1570  Loading = true;
1571  GameMain.World.Enabled = false;
1572  try
1573  {
1574  loaded.Add(this);
1575 
1576  Info = new SubmarineInfo(info);
1577 
1578  ConnectedDockingPorts = new Dictionary<Submarine, DockingPort>();
1579 
1580  //place the sub above the top of the level
1581  HiddenSubPosition = HiddenSubStartPosition;
1582  if (GameMain.GameSession?.LevelData != null)
1583  {
1584  HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y;
1585  }
1586 
1587  for (int i = 0; i < loaded.Count; i++)
1588  {
1589  Submarine sub = loaded[i];
1590  HiddenSubPosition =
1591  new Vector2(
1592  //1st sub on the left side, 2nd on the right, etc
1593  -HiddenSubPosition.X,
1594  HiddenSubPosition.Y + sub.Borders.Height + 5000.0f);
1595  }
1596 
1597  IdOffset = IdRemap.DetermineNewOffset();
1598 
1599  List<MapEntity> newEntities = new List<MapEntity>();
1600  if (loadEntities == null)
1601  {
1602  if (Info.SubmarineElement != null)
1603  {
1604  newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset);
1605  }
1606  }
1607  else
1608  {
1609  newEntities = loadEntities(this);
1610  newEntities.ForEach(me => me.Submarine = this);
1611  }
1612 
1613  if (newEntities != null)
1614  {
1615  foreach (var e in newEntities)
1616  {
1617  if (linkedRemap != null) { e.ResolveLinks(linkedRemap); }
1618  e.unresolvedLinkedToID = null;
1619  }
1620  }
1621 
1622  Vector2 center = Vector2.Zero;
1623  var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this);
1624 
1625  if (matchingHulls.Any())
1626  {
1627  Vector2 topLeft = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
1628  Vector2 bottomRight = new Vector2(matchingHulls[0].Rect.X, matchingHulls[0].Rect.Y);
1629  foreach (Hull hull in matchingHulls)
1630  {
1631  if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X;
1632  if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y;
1633 
1634  if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right;
1635  if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height;
1636  }
1637 
1638  center = (topLeft + bottomRight) / 2.0f;
1639  center.X -= center.X % GridSize.X;
1640  center.Y -= center.Y % GridSize.Y;
1641 
1642  RepositionEntities(-center, MapEntity.MapEntityList.Where(me => me.Submarine == this));
1643  }
1644 
1645  subBody = new SubmarineBody(this, showErrorMessages);
1646  Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
1647  subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
1648 
1649  if (info.IsOutpost)
1650  {
1651  ShowSonarMarker = false;
1652  PhysicsBody.FarseerBody.BodyType = BodyType.Static;
1653  TeamID = CharacterTeamType.FriendlyNPC;
1654 
1655  bool indestructible =
1656  GameMain.NetworkMember != null &&
1657  !GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
1658  !(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
1659 
1660  foreach (MapEntity me in MapEntity.MapEntityList)
1661  {
1662  if (me.Submarine != this) { continue; }
1663  if (me is Item item)
1664  {
1665  item.AllowStealing = true;
1666  if (info.OutpostGenerationParams != null)
1667  {
1668  item.SpawnedInCurrentOutpost = true;
1669  item.AllowStealing =
1671  item.RootContainer is { Prefab: { AllowStealingContainedItems: true } };
1672  }
1673  if (item.GetComponent<Repairable>() != null && indestructible)
1674  {
1675  item.Indestructible = true;
1676  }
1677  foreach (ItemComponent ic in item.Components)
1678  {
1679  if (ic is ConnectionPanel connectionPanel)
1680  {
1681  //prevent rewiring
1683  {
1684  connectionPanel.Locked = true;
1685  }
1686  }
1687  else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
1688  {
1689  //prevent deattaching items from walls
1690 #if CLIENT
1691  if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
1692 #endif
1693  holdable.CanBePicked = false;
1694  holdable.CanBeSelected = false;
1695  }
1696  }
1697  }
1698  else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
1699  {
1700  structure.Indestructible = true;
1701  }
1702  }
1703  }
1704  else if (info.IsRuin)
1705  {
1706  ShowSonarMarker = false;
1707  PhysicsBody.FarseerBody.BodyType = BodyType.Static;
1708  }
1709 
1710  if (entityGrid != null)
1711  {
1712  Hull.EntityGrids.Remove(entityGrid);
1713  entityGrid = null;
1714  }
1715  entityGrid = Hull.GenerateEntityGrid(this);
1716 
1717  for (int i = 0; i < MapEntity.MapEntityList.Count; i++)
1718  {
1719  if (MapEntity.MapEntityList[i].Submarine != this) { continue; }
1720  MapEntity.MapEntityList[i].Move(HiddenSubPosition, ignoreContacts: true);
1721  }
1722 
1723  Loading = false;
1724 
1725  MapEntity.MapLoaded(newEntities, true);
1726  foreach (MapEntity me in MapEntity.MapEntityList)
1727  {
1728  if (me.Submarine != this) { continue; }
1729  if (me is LinkedSubmarine linkedSub)
1730  {
1731  linkedSub.LinkDummyToMainSubmarine();
1732  }
1733  else if (me is WayPoint wayPoint && wayPoint.SpawnType.HasFlag(SpawnType.ExitPoint))
1734  {
1735  exitPoints.Add(wayPoint);
1736  }
1737  }
1738 
1739  foreach (Hull hull in matchingHulls)
1740  {
1741  if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase))
1742  {
1743  hull.RoomName = hull.CreateRoomName();
1744  }
1745  }
1746 
1747  if (Screen.Selected is { IsEditor : false })
1748  {
1749  foreach (Identifier layer in Info.LayersHiddenByDefault)
1750  {
1751  SetLayerEnabled(layer, enabled: false);
1752  }
1753  }
1754 
1755  GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged.Register(upgradeEventIdentifier, _ => ResetCrushDepth());
1756 
1757 #if CLIENT
1758  GameMain.LightManager.OnMapLoaded();
1759  Lights.ConvexHull.RecalculateAll(this);
1760 #endif
1761  //if the sub was made using an older version,
1762  //halve the brightness of the lights to make them look (almost) right on the new lighting formula
1763  if (showErrorMessages &&
1764  !string.IsNullOrEmpty(Info.FilePath) &&
1766  (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
1767  {
1768  DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. "
1769  + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion.");
1770  foreach (Item item in Item.ItemList)
1771  {
1772  if (item.Submarine != this) continue;
1773  if (item.ParentInventory != null || item.body != null) continue;
1774  foreach (var light in item.GetComponents<LightComponent>())
1775  {
1776  light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f);
1777  }
1778  }
1779  }
1780  GenerateOutdoorNodes();
1781  }
1782  finally
1783  {
1784  Loading = false;
1785  GameMain.World.Enabled = true;
1786  }
1787  sw.Stop();
1788  string debugMsg = $"Loading {Info?.Name ?? "unknown"} took {sw.ElapsedMilliseconds} ms.";
1789  DebugConsole.Log(debugMsg);
1790  System.Diagnostics.Debug.WriteLine(debugMsg);
1791  }
1792 
1793  protected override ushort DetermineID(ushort id, Submarine submarine)
1794  {
1795  return (ushort)(ReservedIDStart - Submarine.loaded.Count);
1796  }
1797 
1798  public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null)
1799  {
1800  if (unloadPrevious) { Unload(); }
1801  return new Submarine(info, false, linkedRemap: linkedRemap);
1802  }
1803 
1804  private void ResetCrushDepth()
1805  {
1806  realWorldCrushDepth = null;
1807  }
1808 
1815  public void SetCrushDepth(float realWorldCrushDepth)
1816  {
1817  foreach (Structure structure in Structure.WallList)
1818  {
1819  if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; }
1820  structure.CrushDepth = realWorldCrushDepth;
1821  }
1822  this.realWorldCrushDepth = realWorldCrushDepth;
1823  }
1824 
1825  public static void RepositionEntities(Vector2 moveAmount, IEnumerable<MapEntity> entities)
1826  {
1827  if (moveAmount.LengthSquared() < 0.00001f) { return; }
1828  foreach (MapEntity entity in entities)
1829  {
1830  if (entity is Item item)
1831  {
1832  item.GetComponent<Wire>()?.MoveNodes(moveAmount);
1833  }
1834  entity.Move(moveAmount);
1835  }
1836  }
1837 
1838  public bool CheckFuel()
1839  {
1840  float fuel = GetItems(true).Where(i => i.HasTag(Tags.Fuel)).Sum(i => i.Condition);
1841  Info.LowFuel = fuel < 200;
1842  return !Info.LowFuel;
1843  }
1844 
1845  public void SaveToXElement(XElement element)
1846  {
1847  element.Add(new XAttribute("name", Info.Name));
1848  element.Add(new XAttribute("description", Info.Description ?? ""));
1849  element.Add(new XAttribute("checkval", Rand.Int(int.MaxValue)));
1850  element.Add(new XAttribute("price", Info.Price));
1851  element.Add(new XAttribute("tier", Info.Tier));
1852  element.Add(new XAttribute("initialsuppliesspawned", Info.InitialSuppliesSpawned));
1853  element.Add(new XAttribute("noitems", Info.NoItems));
1854  element.Add(new XAttribute("lowfuel", !CheckFuel()));
1855  element.Add(new XAttribute("type", Info.Type.ToString()));
1856  element.Add(new XAttribute("ismanuallyoutfitted", Info.IsManuallyOutfitted));
1857  if (Info.IsPlayer && !Info.HasTag(SubmarineTag.Shuttle))
1858  {
1859  element.Add(new XAttribute("class", Info.SubmarineClass.ToString()));
1860  }
1861  element.Add(new XAttribute("tags", Info.Tags.ToString()));
1862  element.Add(new XAttribute("gameversion", GameMain.Version.ToString()));
1863 
1864  Rectangle dimensions = VisibleBorders;
1865  element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2())));
1866  var cargoContainers = GetCargoContainers();
1867  int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity);
1868  foreach (MapEntity me in MapEntity.MapEntityList)
1869  {
1870  if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this)
1871  {
1872  cargoCapacity += linkedSub.CargoCapacity;
1873  }
1874  }
1875 
1876  element.Add(new XAttribute("cargocapacity", cargoCapacity));
1877  element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin));
1878  element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax));
1879  element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString()));
1880  element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
1881  if (Info.LayersHiddenByDefault.Any())
1882  {
1883  element.Add(new XAttribute("layerhiddenbydefault", string.Join(", ", Info.LayersHiddenByDefault)));
1884  }
1885 
1886  if (Info.WreckInfo != null)
1887  {
1888  bool hasThalamus = false;
1889 
1890  var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet();
1891  var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet();
1892 
1893  foreach (ItemPrefab prefab in prefabsOnSub)
1894  {
1895  foreach (Identifier entity in wreckAiEntities)
1896  {
1897  if (WreckAI.IsThalamus(prefab, entity))
1898  {
1899  hasThalamus = true;
1900  break;
1901  }
1902  }
1903  if (hasThalamus) { break; }
1904  }
1905 
1906  element.Add(new XAttribute(nameof(WreckInfo.WreckContainsThalamus), hasThalamus ? WreckInfo.HasThalamus.Yes : WreckInfo.HasThalamus.No));
1907  }
1908 
1909  if (Info.Type == SubmarineType.OutpostModule)
1910  {
1911  Info.OutpostModuleInfo?.Save(element);
1912  }
1913  if (Info.GetExtraSubmarineInfo is { } extraSubInfo)
1914  {
1915  extraSubInfo.Save(element);
1916  }
1917 
1918  foreach (Item item in Item.ItemList)
1919  {
1920  if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap is not { } connectedItemsToSwap) { continue; }
1921  foreach (var (requiredTag, swapTo) in connectedItemsToSwap)
1922  {
1923  List<Item> itemsToSwap = new List<Item>();
1924  itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast<Item>());
1925  if (item.GetComponent<ConnectionPanel>() is ConnectionPanel connectionPanel)
1926  {
1927  foreach (Connection c in connectionPanel.Connections)
1928  {
1929  foreach (var connectedComponent in item.GetConnectedComponentsRecursive<ItemComponent>(c))
1930  {
1931  if (!itemsToSwap.Contains(connectedComponent.Item) && connectedComponent.Item.HasTag(requiredTag))
1932  {
1933  itemsToSwap.Add(connectedComponent.Item);
1934  }
1935  }
1936  }
1937  }
1938  ItemPrefab itemPrefab = ItemPrefab.Find("", swapTo);
1939  if (itemPrefab == null)
1940  {
1941  DebugConsole.ThrowError($"Failed to swap an item connected to \"{item.Name}\" into \"{swapTo}\".");
1942  continue;
1943  }
1944  foreach (Item itemToSwap in itemsToSwap)
1945  {
1946  itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap;
1947  if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; }
1948  }
1949  }
1950  }
1951 
1952  Dictionary<int, MapEntity> savedEntities = new Dictionary<int, MapEntity>();
1953  foreach (MapEntity e in MapEntity.MapEntityList.OrderBy(e => e.ID))
1954  {
1955  if (!e.ShouldBeSaved) { continue; }
1956 
1957  if (e.Removed)
1958  {
1959  GameAnalyticsManager.AddErrorEventOnce(
1960  "Submarine.SaveToXElement:Removed" + e.Name,
1961  GameAnalyticsManager.ErrorSeverity.Error,
1962  $"Attempted to save a removed entity (\"{e.Name}\"). Duplicate ID: {savedEntities.ContainsKey(e.ID)}");
1963  DebugConsole.ThrowError($"Error while saving the submarine. Attempted to save a removed entity (\"{e.Name} ({e.ID})\"). The entity will not be saved to avoid corrupting the submarine file.");
1964  continue;
1965  }
1966  if (savedEntities.TryGetValue(e.ID, out MapEntity duplicateEntity))
1967  {
1968  GameAnalyticsManager.AddErrorEventOnce(
1969  "Submarine.SaveToXElement:DuplicateId" + e.Name,
1970  GameAnalyticsManager.ErrorSeverity.Error,
1971  $"Attempted to save an entity with a duplicate ID ({e.Name}, {duplicateEntity.Name}).");
1972  DebugConsole.ThrowError($"Error while saving the submarine. The entity \"{e.Name}\" has the same ID as \"{duplicateEntity.Name}\" ({e.ID}). The entity will not be saved to avoid corrupting the submarine file.");
1973  continue;
1974  }
1975 
1976  if (e is Item item)
1977  {
1978  if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; }
1979 #if CLIENT
1981  {
1982  e.Submarine = this;
1983  }
1984 #endif
1985  if (e.Submarine != this) { continue; }
1986  if (item.RootContainer != null && item.RootContainer.Submarine != this) { continue; }
1987  }
1988  else
1989  {
1990  if (e.Submarine != this) { continue; }
1991  }
1992 
1993  e.Save(element);
1994  savedEntities.Add(e.ID, e);
1995  }
1996  Info.CheckSubsLeftBehind(element);
1997  }
1998 
1999  public bool TrySaveAs(string filePath, System.IO.MemoryStream previewImage = null)
2000  {
2001  var newInfo = new SubmarineInfo(this)
2002  {
2003  Type = Info.Type,
2004  FilePath = filePath,
2005  OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
2006  BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null,
2007  WreckInfo = Info.WreckInfo != null ? new WreckInfo(Info.WreckInfo) : null,
2008  Name = Path.GetFileNameWithoutExtension(filePath)
2009  };
2010 #if CLIENT
2011  //remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture)
2012  Info.PreviewImage = null;
2013 #endif
2014  Info.Dispose();
2015  Info = newInfo;
2016 
2017  try
2018  {
2019  newInfo.SaveAs(filePath, previewImage);
2020  }
2021  catch (Exception e)
2022  {
2023  DebugConsole.ThrowError($"Saving submarine \"{filePath}\" failed!", e);
2024  return false;
2025  }
2026  return true;
2027  }
2028 
2029  public static bool Unloading
2030  {
2031  get;
2032  private set;
2033  }
2034 
2035  public static void Unload()
2036  {
2037  if (Unloading)
2038  {
2039  DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading.");
2040  return;
2041  }
2042 
2043  Unloading = true;
2044  try
2045  {
2046 #if CLIENT
2048  GameMain.LightManager?.ClearLights();
2049  depthSortedDamageable.Clear();
2050 #endif
2051  var _loaded = new List<Submarine>(loaded);
2052  foreach (Submarine sub in _loaded)
2053  {
2054  sub.Remove();
2055  }
2056 
2057  loaded.Clear();
2058 
2059  visibleEntities = null;
2060 
2061  if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; }
2062 
2063  RemoveAll();
2064 
2065  if (Item.ItemList.Count > 0)
2066  {
2067  List<Item> items = new List<Item>(Item.ItemList);
2068  foreach (Item item in items)
2069  {
2070  DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed");
2071  try
2072  {
2073  item.Remove();
2074  }
2075  catch (Exception e)
2076  {
2077  DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e);
2078  }
2079  }
2080  Item.ItemList.Clear();
2081  }
2082 
2083  Ragdoll.RemoveAll();
2085  StatusEffect.StopAll();
2086  GameMain.World = null;
2087 
2088  Powered.Grids.Clear();
2089  Powered.ChangedConnections.Clear();
2090 
2091  GC.Collect();
2092 
2093  }
2094  finally
2095  {
2096  Unloading = false;
2097  }
2098  }
2099 
2100  public override void Remove()
2101  {
2102  base.Remove();
2103 
2104  subBody?.Remove();
2105  subBody = null;
2106 
2107  outdoorNodes?.Clear();
2108  outdoorNodes = null;
2109  obstructedNodes.Clear();
2110 
2111  GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier);
2112 
2113  if (entityGrid != null)
2114  {
2115  Hull.EntityGrids.Remove(entityGrid);
2116  entityGrid = null;
2117  }
2118 
2119  visibleEntities = null;
2120 
2121  bodyDist.Clear();
2122  bodies.Clear();
2123 
2124  if (MainSub == this) { MainSub = null; }
2125  if (MainSubs[1] == this) { MainSubs[1] = null; }
2126 
2127  ConnectedDockingPorts?.Clear();
2128 
2129  Powered.ChangedConnections.Clear();
2130  Powered.Grids.Clear();
2131 
2132  loaded.Remove(this);
2133  }
2134 
2135  public void Dispose()
2136  {
2137  Remove();
2138  }
2139 
2140  private List<PathNode> outdoorNodes;
2141  private List<PathNode> OutdoorNodes
2142  {
2143  get
2144  {
2145  if (outdoorNodes == null)
2146  {
2147  GenerateOutdoorNodes();
2148  }
2149  return outdoorNodes;
2150  }
2151  }
2152 
2153  private void GenerateOutdoorNodes()
2154  {
2155  var waypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path && wp.Submarine == this && wp.CurrentHull == null);
2156  outdoorNodes = PathNode.GenerateNodes(waypoints, removeOrphans: false);
2157  }
2158 
2159  private readonly Dictionary<Submarine, HashSet<PathNode>> obstructedNodes = new Dictionary<Submarine, HashSet<PathNode>>();
2160 
2165  {
2166  // Check collisions to level
2167  foreach (var node in OutdoorNodes)
2168  {
2169  if (node == null || node.Waypoint == null) { continue; }
2170  var wp = node.Waypoint;
2171  if (wp.IsObstructed) { continue; }
2172  foreach (var connection in node.connections)
2173  {
2174  var connectedWp = connection.Waypoint;
2175  if (connectedWp.IsObstructed) { continue; }
2176  Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition);
2177  Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition);
2178  var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false);
2179  if (body != null)
2180  {
2181  connectedWp.IsObstructed = true;
2182  wp.IsObstructed = true;
2183  break;
2184  }
2185  }
2186  }
2187  }
2188 
2193  {
2194  if (otherSub == null) { return; }
2195  if (otherSub == this) { return; }
2196  // Check collisions to other subs.
2197  foreach (var node in OutdoorNodes)
2198  {
2199  if (node == null || node.Waypoint == null) { continue; }
2200  var wp = node.Waypoint;
2201  if (wp.IsObstructed) { continue; }
2202  foreach (var connection in node.connections)
2203  {
2204  var connectedWp = connection.Waypoint;
2205  if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; }
2206  bool isObstructed = wp.CurrentHull is Hull h && h.Submarine != this;
2207  if (!isObstructed)
2208  {
2209  Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition;
2210  Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition;
2211  var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true);
2212  if (body != null)
2213  {
2214  if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
2215  {
2216  isObstructed = true;
2217  }
2218  }
2219  }
2220  if (isObstructed)
2221  {
2222  connectedWp.IsObstructed = true;
2223  wp.IsObstructed = true;
2224  if (!obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
2225  {
2226  nodes = new HashSet<PathNode>();
2227  obstructedNodes.Add(otherSub, nodes);
2228  }
2229  nodes.Add(node);
2230  nodes.Add(connection);
2231  break;
2232  }
2233  }
2234  }
2235  }
2236 
2240  public void EnableObstructedWaypoints(Submarine otherSub)
2241  {
2242  if (obstructedNodes.TryGetValue(otherSub, out HashSet<PathNode> nodes))
2243  {
2244  nodes.ForEach(n => n.Waypoint.IsObstructed = false);
2245  nodes.Clear();
2246  obstructedNodes.Remove(otherSub);
2247  }
2248  }
2249 
2250  public void RefreshOutdoorNodes() => OutdoorNodes.ForEach(n => n?.Waypoint?.FindHull());
2251 
2252  public Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions = false, bool allowConnectedSubs = false)
2253  {
2254  var connectedSubs = GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet();
2255  Item selectedContainer = null;
2256  foreach (Item potentialContainer in Item.ItemList)
2257  {
2258  if (potentialContainer.Removed) { continue; }
2259  if (potentialContainer.NonInteractable) { continue; }
2260  if (potentialContainer.IsHidden) { continue; }
2261  if (allowConnectedSubs)
2262  {
2263  if (!connectedSubs.Contains(potentialContainer.Submarine)) { continue; }
2264  }
2265  else
2266  {
2267  if (potentialContainer.Submarine != this) { continue; }
2268  }
2269  if (potentialContainer == item) { continue; }
2270  if (potentialContainer.Condition <= 0) { continue; }
2271  if (potentialContainer.OwnInventory == null) { continue; }
2272  if (potentialContainer.GetRootInventoryOwner() != potentialContainer) { continue; }
2273  var container = potentialContainer.GetComponent<ItemContainer>();
2274  if (container == null) { continue; }
2275  if (!potentialContainer.OwnInventory.CanBePut(item)) { continue; }
2276  if (!container.ShouldBeContained(item, out _)) { continue; }
2277  if (!item.Prefab.IsContainerPreferred(item, container, out bool isPreferencesDefined, out bool isSecondary, checkTransferConditions: checkTransferConditions) || !isPreferencesDefined || onlyPrimary && isSecondary) { continue; }
2278  if (potentialContainer.Submarine == this && !isSecondary)
2279  {
2280  //valid primary container in the same sub -> perfect, let's use that one
2281  return potentialContainer;
2282  }
2283  selectedContainer = potentialContainer;
2284 
2285  }
2286  return selectedContainer;
2287  }
2288 
2289  public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos = null)
2290  {
2291  return targetWorldPos.HasValue ?
2292  GetRelativeSimPositionFromWorldPosition(targetWorldPos.Value, from.Submarine, to.Submarine) :
2293  GetRelativeSimPosition(to.SimPosition, from.Submarine, to.Submarine);
2294  }
2295 
2296  public static Vector2 GetRelativeSimPositionFromWorldPosition(Vector2 targetWorldPos, Submarine fromSub, Submarine toSub)
2297  {
2298  Vector2 worldPos = targetWorldPos;
2299  if (toSub != null)
2300  {
2301  worldPos -= toSub.Position;
2302  }
2303  return GetRelativeSimPosition(ConvertUnits.ToSimUnits(worldPos), fromSub, toSub);
2304  }
2305 
2306  public static Vector2 GetRelativeSimPosition(Vector2 targetSimPos, Submarine fromSub, Submarine toSub)
2307  {
2308  Vector2 targetPos = targetSimPos;
2309  if (fromSub == null && toSub != null)
2310  {
2311  // outside and targeting inside
2312  targetPos += toSub.SimPosition;
2313  }
2314  else if (fromSub != null && toSub == null)
2315  {
2316  // inside and targeting outside
2317  targetPos -= fromSub.SimPosition;
2318  }
2319  else if (fromSub != toSub)
2320  {
2321  if (fromSub != null && toSub != null)
2322  {
2323  // both inside, but in different subs
2324  Vector2 diff = fromSub.SimPosition - toSub.SimPosition;
2325  targetPos -= diff;
2326  }
2327  }
2328  return targetPos;
2329  }
2330  }
2331 }
BeaconStationInfo(SubmarineInfo submarineInfo, XElement element)
Vector2 TargetPos
Definition: Camera.cs:156
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static GameSession?? GameSession
Definition: GameMain.cs:88
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static World World
Definition: GameMain.cs:105
static readonly Version Version
Definition: GameMain.cs:46
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static readonly List< EntityGrid > EntityGrids
static EntityGrid GenerateEntityGrid(Rectangle worldRect)
static readonly List< Hull > HullList
static ushort DetermineNewOffset()
Definition: IdRemap.cs:125
bool CanBePut(Item item)
Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be p...
override void Move(Vector2 amount, bool ignoreContacts=true)
static void UpdateHulls()
goes through every item and re-checks which hull they are in
override void FlipX(bool relativeToSub)
Flip the entity horizontally
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static readonly List< Item > ItemList
static ItemPrefab Find(string name, Identifier identifier)
bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement=false, bool checkTransferConditions=false)
The base class for components holding the different functionalities of the item
void InfectBallast(Identifier identifier, bool allowMultiplePerShip=false)
readonly Point Size
Definition: LevelData.cs:52
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
virtual void FlipX(bool relativeToSub)
Flip the entity horizontally
static void MapLoaded(List< MapEntity > entities, bool updateHulls)
static readonly List< MapEntity > MapEntityList
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
virtual void Move(Vector2 amount, bool ignoreContacts=true)
bool IsLayerHidden
Is the layer this entity is in currently hidden? If it is, the entity is not updated and should do no...
virtual XElement Save(XElement parentElement)
static List< MapEntity > LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset)
OutpostModuleInfo(SubmarineInfo submarineInfo, XElement element)
static void RemoveAllRoundSounds()
Definition: RoundSound.cs:139
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static StructurePrefab FindPrefab(string name, Identifier identifier)
Rectangle VisibleBorders
Extents of all the visible items/structures/hulls (including ones without a physics body)
Rectangle Borders
Extents of the solid items/structures (ones with a physics body) and hulls
static bool RectsOverlap(RectangleF rect1, RectangleF rect2, bool inclusive=true)
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
Item FindContainerFor(Item item, bool onlyPrimary, bool checkTransferConditions=false, bool allowConnectedSubs=false)
override ushort DetermineID(ushort id, Submarine submarine)
List< Item > GetItems(bool alsoFromConnectedSubs)
static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate=500.0f)
Finds the sub whose borders contain the position. Note that this method uses the "actual" position of...
static void RepositionEntities(Vector2 moveAmount, IEnumerable< MapEntity > entities)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
Rectangle CalculateDimensions(bool onlyHulls=true)
static Submarine FindContaining(Vector2 worldPosition, float inflate=500.0f)
Finds the sub whose world borders contain the position.
static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? targetWorldPos=null)
Rectangle? VisibleBorders
Extents of all the visible items/structures/hulls (including ones without a physics body)
void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent=false)
static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel=false, bool ignoreSubs=false, bool ignoreSensors=true, bool ignoreDisabledWalls=true, bool ignoreBranches=true, Predicate< Fixture > blocksVisibilityPredicate=null)
Check visibility between two points (in sim units).
static IEnumerable< MapEntity > VisibleEntities
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static ? Vector2 CalculateDockOffset(Submarine sub, Submarine dockedSub)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
static RectangleF AbsRectF(Vector2 pos, Vector2 size)
static Vector2 GetRelativeSimPositionFromWorldPosition(Vector2 targetWorldPos, Submarine fromSub, Submarine toSub)
static bool LayerExistsInAnySub(Identifier layer)
List< Structure > GetWalls(bool alsoFromConnectedSubs)
void SetCrushDepth(float realWorldCrushDepth)
Normally crush depth is determined by the crush depths of the walls and upgrades applied on them....
static bool RectsOverlap(Rectangle rect1, Rectangle rect2, bool inclusive=true)
List< WayPoint > GetWaypoints(bool alsoFromConnectedSubs)
static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap=null)
void EnableFactionSpecificEntities(Identifier factionIdentifier)
void FlipX(List< Submarine > parents=null)
void AttemptBallastFloraInfection(Identifier identifier, float deltaTime, float probability)
static IEnumerable< Body > PickBodies(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Returns a list of physics bodies the ray intersects with, sorted according to distance (the closest b...
void DisableObstructedWayPoints(Submarine otherSub)
Temporarily disables waypoints obstructed by the other sub.
void EnableObstructedWaypoints(Submarine otherSub)
Only affects temporarily disabled waypoints.
bool IsConnectedTo(Submarine otherSub)
Returns true if the sub is same as the other, or connected to it via docking ports.
List<(ItemContainer container, int freeSlots)> GetCargoContainers()
static Rectangle GetBorders(XElement submarineElement)
Rectangle? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
static Vector2 VectorToWorldGrid(Vector2 position, Submarine sub=null, bool round=false)
bool CreateTurretAI()
Creates an AI that operates all the turrets on a sub, same as Thalamus but only operates the turrets.
static Rectangle AbsRect(Vector2 pos, Vector2 size)
List< Gap > GetGaps(bool alsoFromConnectedSubs)
float? RealWorldDepth
How deep down the sub is from the surface of Europa in meters (affected by level type,...
static Vector2 GetRelativeSimPosition(Vector2 targetSimPos, Submarine fromSub, Submarine toSub)
List< Hull > GetHulls(bool alsoFromConnectedSubs)
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
void DisableObstructedWayPoints()
Permanently disables obstructed waypoints obstructed by the level.
void SetPosition(Vector2 position, List< Submarine > checkd=null, bool forceUndockFromStaticSubmarines=true)
Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize=null, float subDockingPortOffset=0.0f, int verticalMoveDir=0)
Attempt to find a spawn position close to the specified position where the sub doesn't collide with w...
bool TrySaveAs(string filePath, System.IO.MemoryStream previewImage=null)
static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts=false, bool ignoreOutsideLevel=true, bool ignoreRespawnShuttle=false, CharacterTeamType? teamType=null)
If has value, the sub must match the team type.
List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap
static readonly PrefabCollection< WreckAIConfig > Prefabs
HasThalamus WreckContainsThalamus
WreckInfo(SubmarineInfo submarineInfo, XElement element)
DoubleVector2 Coord
List< GraphEdge > Edges
Interface for entities that handle ServerNetObject.ENTITY_POSITION