Server LuaCsForBarotrauma
WayPoint.cs
2 using FarseerPhysics;
3 using FarseerPhysics.Dynamics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Xml.Linq;
10 
11 namespace Barotrauma
12 {
13  [Flags]
14  public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32, Disabled = 64 };
15 
16  partial class WayPoint : MapEntity
17  {
18  public static List<WayPoint> WayPointList = new List<WayPoint>();
19 
20  public static bool ShowWayPoints = true, ShowSpawnPoints = true;
21 
22  public const float LadderWaypointInterval = 75.0f;
23 
24  protected SpawnType spawnType;
25  private string[] idCardTags;
26  private ushort ladderId;
27  public Ladder Ladders;
28  public Structure Stairs;
29 
30  private HashSet<Identifier> tags;
31 
32  public bool IsObstructed;
33 
34  public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y;
35 
36  // Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open.
37  // The only way to add the open gaps should be by calling OnGapStateSchanged.
38  public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater);
39 
40  private HashSet<Gap> openGaps;
46  public void OnGapStateChanged(bool open, Gap gap)
47  {
48  openGaps ??= new HashSet<Gap>();
49  if (open)
50  {
51  openGaps.Add(gap);
52  }
53  else
54  {
55  openGaps.Remove(gap);
56  }
57  }
58 
59  private ushort gapId;
61  {
62  get;
63  set;
64  }
65 
67  {
68  get { return ConnectedGap?.ConnectedDoor; }
69  }
70 
71  public Hull CurrentHull { get; private set; }
72 
74  public RuinGeneration.Ruin Ruin;
75 
76  public Level.Cave Cave;
77 
79  {
80  get { return spawnType; }
81  set { spawnType = value; }
82  }
83 
84  public Point ExitPointSize { get; private set; }
85 
86  public Rectangle ExitPointWorldRect => new Rectangle(
87  (int)WorldPosition.X - ExitPointSize.X / 2, (int)WorldPosition.Y + ExitPointSize.Y / 2,
89 
90  public Action<WayPoint> OnLinksChanged { get; set; }
91 
92  public override string Name
93  {
94  get
95  {
96  return spawnType == SpawnType.Path ? "WayPoint" : "SpawnPoint";
97  }
98  }
99 
100  public string IdCardDesc { get; private set; }
101  public string[] IdCardTags
102  {
103  get { return idCardTags; }
104  private set
105  {
106  idCardTags = value;
107  for (int i = 0; i < idCardTags.Length; i++)
108  {
109  idCardTags[i] = idCardTags[i].Trim().ToLowerInvariant();
110  }
111  }
112  }
113 
114  public IEnumerable<Identifier> Tags => tags;
115 
116  public JobPrefab AssignedJob { get; private set; }
117 
118  public WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap = null)
119  : this(new Rectangle((int)position.X - 3, (int)position.Y + 3, 6, 6), submarine)
120  {
121  this.spawnType = spawnType;
122  ConnectedGap = gap;
123  }
124 
125  public WayPoint(MapEntityPrefab prefab, Rectangle rectangle)
126  : this (rectangle, Submarine.MainSub)
127  {
128  if (prefab.Identifier.Contains("spawn"))
129  {
130  spawnType = SpawnType.Human;
131  }
132  else
133  {
134  SpawnType = SpawnType.Path;
135  }
136 
137 #if CLIENT
138  if (SubEditorScreen.IsSubEditor())
139  {
140  SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
141  }
142 #endif
143  }
144 
145  public enum Type
146  {
147  WayPoint,
148  SpawnPoint
149  }
150 
151  public WayPoint(Rectangle newRect, Submarine submarine)
152  : this (Type.WayPoint, newRect, submarine)
153  {
154  }
155 
156  public WayPoint(Type type, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID)
157  : base (type is Type.WayPoint
158  ? CoreEntityPrefab.WayPointPrefab
159  : CoreEntityPrefab.SpawnPointPrefab, submarine, id)
160  {
161  rect = newRect;
162  idCardTags = Array.Empty<string>();
163  tags = new HashSet<Identifier>();
164 
165 #if CLIENT
166  if (iconSprites == null)
167  {
168  iconSprites = new Dictionary<string, Sprite>()
169  {
170  { "Path", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) },
171  { "Human", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) },
172  { "Enemy", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) },
173  { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) },
174  { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) },
175  { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) },
176  { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) },
177  { "Submarine", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) },
178  { "ExitPoint", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) }
179  };
180  }
181 #endif
182 
183  InsertToList();
184  WayPointList.Add(this);
185 
186  DebugConsole.Log("Created waypoint (" + ID + ")");
187 
188  FindHull();
189  }
190 
191  public override MapEntity Clone()
192  {
193  var clone = new WayPoint(rect, Submarine)
194  {
196  idCardTags = idCardTags,
197  tags = tags,
200  };
201 
202  return clone;
203  }
204 
205  public static bool GenerateSubWaypoints(Submarine submarine)
206  {
207  if (!Hull.HullList.Any())
208  {
209  DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found.");
210  return false;
211  }
212 
213  List<WayPoint> existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path);
214  foreach (WayPoint wayPoint in existingWaypoints)
215  {
216  wayPoint.Remove();
217  }
218 
219  //find all open doors and temporarily activate their bodies to prevent visibility checks
220  //from ignoring the doors and generating waypoint connections that go straight through the door
221  List<Door> openDoors = new List<Door>();
222  foreach (Item item in Item.ItemList)
223  {
224  var door = item.GetComponent<Door>();
225  if (door != null && !door.Body.Enabled)
226  {
227  openDoors.Add(door);
228  door.Body.Enabled = true;
229  }
230  }
231  bool isRuin = submarine.Info.ShouldBeRuin;
232  float diffFromHullEdge = 50;
233  float minDist = 100.0f;
234  float heightFromFloor = 110.0f;
235  float hullMinHeight = 100;
236 
237  var removals = new HashSet<WayPoint>();
238  foreach (Hull hull in Hull.HullList)
239  {
240  if (isRuin)
241  {
242  diffFromHullEdge = 75;
243  var hullWaypoints = new List<WayPoint>();
244  float top = hull.Rect.Y;
245  float bottom = hull.Rect.Y - hull.Rect.Height;
246  if (hull.Rect.Width < 300 || hull.Rect.Height < 300)
247  {
248  // For narrow hulls, create one line of waypoints either horizontally or vertically
249  if (hull.Rect.Width > hull.Rect.Height)
250  {
251  // Horizontal
252  float y = hull.Rect.Y - hull.Rect.Height / 2;
253  for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
254  {
255  hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
256  }
257  }
258  else
259  {
260  // Vertical
261  float x = hull.Rect.X + hull.Rect.Width / 2;
262  for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist)
263  {
264  hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
265  }
266  }
267  }
268  if (hullWaypoints.None())
269  {
270  // Try to create a grid-like network of waypoints
271  for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
272  {
273  for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist)
274  {
275  hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
276  }
277  }
278  if (hullWaypoints.None())
279  {
280  // If that fails, just create one waypoint at the center.
281  hullWaypoints.Add(new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height / 2), SpawnType.Path, submarine));
282  }
283  foreach (WayPoint wp in hullWaypoints)
284  {
285  foreach (Structure wall in Structure.WallList)
286  {
287  if (wall.HasBody)
288  {
289  // Remove waypoints that are too close/inside the walls.
290  Rectangle rect = wall.Rect;
291  rect.Inflate(10, 10);
292  if (rect.ContainsWorld(wp.Position))
293  {
294  removals.Add(wp);
295  }
296  }
297  }
298  }
299  }
300  // Connect the waypoints
301  foreach (var wayPoint in hullWaypoints)
302  {
303  for (int dir = -1; dir <= 1; dir += 2)
304  {
305  WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.9f, minDist));
306  if (closest != null && closest.CurrentHull == wayPoint.CurrentHull)
307  {
308  wayPoint.ConnectTo(closest);
309  }
310  closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(minDist, minDist * 1.9f));
311  if (closest != null && closest.CurrentHull == wayPoint.CurrentHull)
312  {
313  wayPoint.ConnectTo(closest);
314  }
315  }
316  }
317  }
318  else
319  {
320  if (hull.Rect.Height < hullMinHeight) { continue; }
321  // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor.
322  Body floor = null;
323  for (int i = 0; i < 5; i++)
324  {
325  float horizontalOffset = 0;
326  switch (i)
327  {
328  case 1:
329  horizontalOffset = hull.RectWidth * 0.2f;
330  break;
331  case 2:
332  horizontalOffset = hull.RectWidth * 0.4f;
333  break;
334  case 3:
335  horizontalOffset = -hull.RectWidth * 0.2f;
336  break;
337  case 4:
338  horizontalOffset = -hull.RectWidth * 0.4f;
339  break;
340  }
341  horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset);
342  Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
343  floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
344  if (floor != null) { break; }
345  }
346  if (floor == null) { continue; }
347  float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
348  if (hull.Rect.Width < diffFromHullEdge * 3.0f)
349  {
350  new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
351  }
352  else
353  {
354  WayPoint previousWaypoint = null;
355  for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
356  {
357  var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
358  // Too close to stairs, will be assigned as a stair point -> remove
359  if (wayPoint.FindStairs() != null)
360  {
361  removals.Add(wayPoint);
362  continue;
363  }
364  if (previousWaypoint != null)
365  {
366  wayPoint.ConnectTo(previousWaypoint);
367  }
368  previousWaypoint = wayPoint;
369  }
370  if (previousWaypoint == null)
371  {
372  // Ensure that we always create at least one waypoint per hull.
373  new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
374  }
375  }
376  }
377  }
378 
379  // Platforms
380  foreach (Structure platform in Structure.WallList)
381  {
382  if (!platform.IsPlatform) { continue; }
383  float waypointHeight = heightFromFloor;
384  WayPoint prevWaypoint = null;
385  for (float x = platform.Rect.X + diffFromHullEdge; x <= platform.Rect.Right - diffFromHullEdge; x += minDist)
386  {
387  WayPoint wayPoint = new WayPoint(new Vector2(x, platform.Rect.Y + waypointHeight), SpawnType.Path, submarine);
388  if (prevWaypoint != null)
389  {
390  wayPoint.ConnectTo(prevWaypoint);
391  }
392  // If the waypoint is close to hull waypoints, remove it.
393  if (wayPoint != null)
394  {
395  for (int dir = -1; dir <= 1; dir += 2)
396  {
397  if (wayPoint.FindClosest(dir, horizontalSearch: true, tolerance: new Vector2(minDist, heightFromFloor), ignored: prevWaypoint.ToEnumerable()) != null)
398  {
399  wayPoint.Remove();
400  wayPoint = null;
401  break;
402  }
403  }
404  }
405  prevWaypoint = wayPoint;
406  }
407  }
408 
409  float outSideWaypointInterval = 100.0f;
410  if (!isRuin && submarine.Info.Type != SubmarineType.OutpostModule)
411  {
412  List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>();
413 
414  Rectangle borders = Hull.GetBorders();
415  int originalWidth = borders.Width;
416  int originalHeight = borders.Height;
417  borders.X -= Math.Min(500, originalWidth / 4);
418  borders.Y += Math.Min(500, originalHeight / 4);
419  borders.Width += Math.Min(1500, originalWidth / 2);
420  borders.Height += Math.Min(1000, originalHeight / 2);
421  borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
422 
423  if (borders.Width <= outSideWaypointInterval * 2)
424  {
425  borders.Inflate(outSideWaypointInterval * 2 - borders.Width, 0);
426  }
427 
428  if (borders.Height <= outSideWaypointInterval * 2)
429  {
430  int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
431  borders.Y += inflateAmount / 2;
432  borders.Height += inflateAmount;
433  }
434 
435  WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
436  for (int i = 0; i < 2; i++)
437  {
438  for (float x = borders.X + outSideWaypointInterval; x < borders.Right - outSideWaypointInterval; x += outSideWaypointInterval)
439  {
440  var wayPoint = new WayPoint(
441  new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
442  SpawnType.Path, submarine);
443 
444  outsideWaypoints.Add((wayPoint, i));
445 
446  if (x == borders.X + outSideWaypointInterval)
447  {
448  cornerWaypoint[i, 0] = wayPoint;
449  }
450  else
451  {
452  wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
453  }
454  }
455 
456  cornerWaypoint[i, 1] = WayPointList[WayPointList.Count - 1];
457  }
458 
459  for (int i = 0; i < 2; i++)
460  {
461  WayPoint wayPoint = null;
462  for (float y = borders.Y - borders.Height; y < borders.Y; y += outSideWaypointInterval)
463  {
464  wayPoint = new WayPoint(
465  new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
466  SpawnType.Path, submarine);
467 
468  outsideWaypoints.Add((wayPoint, i));
469 
470  if (y == borders.Y - borders.Height)
471  {
472  wayPoint.ConnectTo(cornerWaypoint[1, i]);
473  }
474  else
475  {
476  wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
477  }
478  }
479 
480  wayPoint.ConnectTo(cornerWaypoint[0, i]);
481  }
482 
483  Vector2 center = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition);
484  float halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2);
485  // Try to move the waypoints so that they are near the walls, roughly following the shape of the sub.
486  foreach (var wayPoint in outsideWaypoints)
487  {
488  WayPoint wp = wayPoint.Item1;
489  float xDiff = center.X - wp.SimPosition.X;
490  Vector2 targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y);
491  Body wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
492  if (wall == null)
493  {
494  // Try again, and shoot to the center now. It happens with some subs that the first, offset raycast don't hit the walls.
495  targetPos = new Vector2(center.X - xDiff, center.Y);
496  wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
497  }
498  if (wall != null)
499  {
500  float distanceFromWall = 1;
501  if (xDiff > 0 && !submarine.Info.HasTag(SubmarineTag.Shuttle))
502  {
503  // We don't want to move the waypoints near the tail too close to the engine.
504  float yDist = Math.Abs(center.Y - wp.SimPosition.Y);
505  distanceFromWall = MathHelper.Lerp(1, 3, MathUtils.InverseLerp(halfHeight, 0, yDist));
506  }
507  Vector2 newPos = Submarine.LastPickedPosition + Submarine.LastPickedNormal * distanceFromWall;
508  wp.rect = new Rectangle(ConvertUnits.ToDisplayUnits(newPos).ToPoint(), wp.rect.Size);
509  wp.FindHull();
510  }
511  }
512  // Remove unwanted points
513  WayPoint previous = null;
514  float tooClose = outSideWaypointInterval / 2;
515  foreach (var wayPoint in outsideWaypoints)
516  {
517  WayPoint wp = wayPoint.Item1;
518  if (wp.CurrentHull != null ||
519  Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null)
520  {
521  // Remove waypoints that got inside/too near the sub.
522  removals.Add(wp);
523  previous = wp;
524  continue;
525  }
526  foreach (var otherWayPoint in outsideWaypoints)
527  {
528  WayPoint otherWp = otherWayPoint.Item1;
529  if (otherWp == wp) { continue; }
530  if (removals.Contains(otherWp)) { continue; }
531  float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position);
532  // Remove waypoints that are too close to each other.
533  if (!removals.Contains(previous) && sqrDist < tooClose * tooClose)
534  {
535  removals.Add(wp);
536  }
537  }
538  previous = wp;
539  }
540  foreach (WayPoint wp in removals)
541  {
542  outsideWaypoints.RemoveAll(w => w.Item1 == wp);
543  }
544  removals.ForEach(wp => wp.Remove());
545  for (int i = 0; i < outsideWaypoints.Count; i++)
546  {
547  WayPoint current = outsideWaypoints[i].Item1;
548  if (current.linkedTo.Count(l => !removals.Contains(l)) > 1) { continue; }
549  WayPoint next = null;
550  int maxConnections = 2;
551  float tooFar = outSideWaypointInterval * 5;
552  for (int j = 0; j < maxConnections; j++)
553  {
554  if (current.linkedTo.Count >= maxConnections) { break; }
555  tooFar /= current.linkedTo.Count(l => !removals.Contains(l));
556  next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp.Item1 != next && wp.Item1.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.Item1.linkedTo.Count < 2 && wp.Item2 < i);
557  if (next != null)
558  {
559  current.ConnectTo(next);
560  }
561  }
562  }
563  }
564  removals.ForEach(wp => wp.Remove());
565  removals.Clear();
566  // Stairs
567  foreach (MapEntity mapEntity in MapEntityList.ToList())
568  {
569  if (!(mapEntity is Structure structure)) { continue; }
570  if (structure.StairDirection == Direction.None) { continue; }
571  WayPoint[] stairPoints = new WayPoint[3];
572  float margin = -32;
573 
574  stairPoints[0] = new WayPoint(new Vector2(
575  structure.Rect.X + 5,
576  structure.Rect.Y - (structure.StairDirection == Direction.Left ? margin : structure.Rect.Height - 100)), SpawnType.Path, submarine);
577 
578  stairPoints[1] = new WayPoint(new Vector2(
579  structure.Rect.Right - 5,
580  structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height - 100 : margin)), SpawnType.Path, submarine);
581 
582  for (int i = 0; i < 2; i++)
583  {
584  for (int dir = -1; dir <= 1; dir += 2)
585  {
586  WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2));
587  if (closest == null) { continue; }
588  stairPoints[i].ConnectTo(closest);
589  }
590  }
591 
592  stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
593  stairPoints[0].ConnectTo(stairPoints[2]);
594  stairPoints[2].ConnectTo(stairPoints[1]);
595  stairPoints.ForEach(wp => wp.FindStairs());
596  }
597 
598  // Ladders
599  foreach (Item item in Item.ItemList)
600  {
601  var ladders = item.GetComponent<Ladder>();
602  if (ladders == null) { continue; }
603 
604  Vector2 bottomPoint = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10);
605  List<(WayPoint wp, bool connectHullPoints)> ladderPoints = new List<(WayPoint, bool)>
606  {
607  (new WayPoint(bottomPoint, SpawnType.Path, submarine), true)
608  };
609 
610  List<Body> ignoredBodies = new List<Body>();
611  // Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either.
612  // Start point is where the bots normally grasp the ladder when they stand on ground.
613  WayPoint lowestPoint = ladderPoints[0].wp;
614  WayPoint prevPoint = lowestPoint;
615  Vector2 prevPos = prevPoint.SimPosition;
616  Body ground = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies,
617  collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform | Physics.CollisionStairs,
618  customPredicate: f => !(f.Body.UserData is Submarine));
619  float startHeight = ground != null ? ConvertUnits.ToDisplayUnits(ground.Position.Y) : bottomPoint.Y;
620  startHeight += heightFromFloor;
621  WayPoint startPoint = lowestPoint;
622  Vector2 nextPos = new Vector2(item.Rect.Center.X, startHeight);
623  // Don't create the start point if it's too close to the lowest point or if it's outside of the sub.
624  // If we skip creating the start point, the lowest point is used instead.
625  if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null)
626  {
627  startPoint = new WayPoint(nextPos, SpawnType.Path, submarine);
628  ladderPoints.Add((startPoint, true));
629  if (lowestPoint != null)
630  {
631  startPoint.ConnectTo(lowestPoint);
632  }
633  prevPoint = startPoint;
634  prevPos = prevPoint.SimPosition;
635  }
636  for (float y = startPoint.Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
637  {
638  //first check if there's a door in the way
639  //(we need to create a waypoint linked to the door for NPCs to open it)
640  Body pickedBody = Submarine.PickBody(
641  ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)),
642  prevPos, ignoredBodies, Physics.CollisionWall, false,
643  (Fixture f) => f.Body.UserData is Item pickedItem && pickedItem.GetComponent<Door>() != null);
644 
645  Door pickedDoor = null;
646  if (pickedBody != null)
647  {
648  pickedDoor = (pickedBody?.UserData as Item).GetComponent<Door>();
649  }
650  else
651  {
652  //no door, check for platforms/walls
653  pickedBody = Submarine.PickBody(
654  ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false,
655  (Fixture f) => f.Body.UserData is Structure);
656  }
657 
658  if (pickedBody != null)
659  {
660  ignoredBodies.Add(pickedBody);
661  }
662 
663  if (pickedDoor != null)
664  {
665  WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine);
666  ladderPoints.Add((newPoint, true));
667  newPoint.ConnectedGap = pickedDoor.LinkedGap;
668  // TODO: Prevent the waypoint below being too close to the door
669  newPoint.ConnectTo(prevPoint);
670  prevPoint = newPoint;
671  prevPos = new Vector2(prevPos.X, ConvertUnits.ToSimUnits(pickedDoor.Item.Position.Y - pickedDoor.Item.Rect.Height));
672  // Adjust y to prevent waypoints clamping up together
673  y = Math.Max(pickedDoor.Item.Position.Y, y);
674  }
675  else
676  {
677  Vector2 pos = pickedBody == null ? new Vector2(startPoint.Position.X, y) :
678  ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor;
679  WayPoint newPoint = new WayPoint(pos, SpawnType.Path, submarine);
680  ladderPoints.Add((newPoint, pickedBody != null));
681  newPoint.ConnectTo(prevPoint);
682  prevPoint = newPoint;
683  prevPos = ConvertUnits.ToSimUnits(newPoint.Position);
684  if (pickedBody != null)
685  {
686  // Adjust y to prevent waypoints clamping up together
687  y = Math.Max(newPoint.Position.Y, y);
688  }
689  }
690  }
691 
692  // Cap
693  if (prevPoint.rect.Y < item.Rect.Y - 40)
694  {
695  WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
696  ladderPoints.Add((wayPoint, true));
697  wayPoint.ConnectTo(prevPoint);
698  }
699 
700  // Connect ladder waypoints to hull points at the right and left side
701  var ladderWaypoints = ladderPoints.Select(lp => lp.wp);
702  foreach (var ladderPoint in ladderPoints)
703  {
704  var wp = ladderPoint.wp;
705  wp.Ladders = ladders;
706  if (!ladderPoint.connectHullPoints) { continue; }
707  bool isHatch = wp.ConnectedGap != null && !wp.ConnectedGap.IsRoomToRoom;
708  for (int dir = -1; dir <= 1; dir += 2)
709  {
710  WayPoint closest = isHatch ?
711  wp.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), wp.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderWaypoints) :
712  wp.FindClosest(dir, horizontalSearch: true, new Vector2(150, 100), wp.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderWaypoints);
713  if (closest == null) { continue; }
714  wp.ConnectTo(closest);
715  }
716  }
717  }
718 
719  // Another ladder pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders)
720  foreach (Item item in Item.ItemList)
721  {
722  var ladders = item.GetComponent<Ladder>();
723  if (ladders == null) { continue; }
724  var wps = WayPointList.Where(wp => wp.Ladders == ladders).OrderByDescending(wp => wp.Rect.Y);
725  WayPoint cap = wps.First();
726  WayPoint above = cap.FindClosest(1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
727  above?.ConnectTo(cap);
728  WayPoint bottom = wps.Last();
729  WayPoint below = bottom.FindClosest(-1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
730  below?.ConnectTo(bottom);
731  }
732 
733  foreach (Gap gap in Gap.GapList)
734  {
735  if (gap.IsHorizontal)
736  {
737  if ( isRuin)
738  {
739  // Too small to swim through
740  if (gap.Rect.Height < 50) { continue; }
741  }
742  else
743  {
744  // Too small to walk through
745  if (gap.Rect.Height < hullMinHeight) { continue; }
746  }
747 
748  Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
749  if (isRuin)
750  {
751  pos.Y = gap.Rect.Y - gap.Rect.Height / 2;
752  }
753  var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
754  // The closest waypoint can be quite far if the gap is at an exterior door.
755  Vector2 tolerance = gap.IsRoomToRoom && !isRuin ? new Vector2(150, 70) : new Vector2(1000, 1000);
756  for (int dir = -1; dir <= 1; dir += 2)
757  {
758  WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
759  if (closest != null)
760  {
761  wayPoint.ConnectTo(closest);
762  }
763  }
764  }
765  else
766  {
767  // Create waypoints on vertical gaps on the outer walls, also hatches.
768  if (!isRuin && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; }
769  // Too small to swim through
770  if (gap.Rect.Width < 50.0f) { continue; }
771  Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
772  // Some hatches are created in the block above where we handle the ladder waypoints. So we need to check for duplicates.
773  if (WayPointList.Any(wp => wp.ConnectedGap == gap)) { continue; }
774  var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
775  Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
776  int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
777  WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
778  if (closest != null)
779  {
780  wayPoint.ConnectTo(closest);
781  }
782  if (isRuin)
783  {
784  closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isRuin ? new Vector2(500, 500) : new Vector2(50, 100));
785  if (closest != null)
786  {
787  wayPoint.ConnectTo(closest);
788  }
789  }
790  // Link to outside
791  for (dir = -1; dir <= 1; dir += 2)
792  {
793  closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null);
794  if (closest != null)
795  {
796  wayPoint.ConnectTo(closest);
797  }
798  }
799  }
800  }
801 
802  var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && w.linkedTo.None());
803  foreach (WayPoint wp in orphans)
804  {
805  wp.Remove();
806  }
807 
808  foreach (WayPoint wp in WayPointList)
809  {
810  if (wp.SpawnType == SpawnType.Path && wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2)
811  {
812  DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color.");
813  }
814  }
815 
816  //re-disable the bodies of the doors that are supposed to be open
817  foreach (Door door in openDoors)
818  {
819  door.Body.Enabled = false;
820  }
821 
822  return true;
823  }
824 
825  private WayPoint FindClosestOutside(IEnumerable<(WayPoint, int)> waypointList, float tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<(WayPoint, int), bool> filter = null)
826  {
827  float closestDist = 0;
828  WayPoint closest = null;
829  foreach (var wayPoint in waypointList)
830  {
831  WayPoint wp = wayPoint.Item1;
832  if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
833  // Ignore if already linked
834  if (linkedTo.Contains(wp)) { continue; }
835  if (ignored != null && ignored.Contains(wp)) { continue; }
836  if (filter != null && !filter(wayPoint)) { continue; }
837  float sqrDist = Vector2.DistanceSquared(Position, wp.Position);
838  if (sqrDist > tolerance * tolerance) { continue; }
839  if (closest == null || sqrDist < closestDist)
840  {
841  var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
842  if (body != null && body != ignoredBody && !(body.UserData is Submarine))
843  {
844  if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
845  {
846  continue;
847  }
848  }
849  closestDist = sqrDist;
850  closest = wp;
851  }
852  }
853  return closest;
854  }
855 
856  private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<WayPoint, bool> filter = null)
857  {
858  if (dir != -1 && dir != 1) { return null; }
859 
860  float closestDist = 0.0f;
861  WayPoint closest = null;
862 
863  foreach (WayPoint wp in WayPointList)
864  {
865  if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
866 
867  float xDiff = wp.Position.X - Position.X;
868  float yDiff = wp.Position.Y - Position.Y;
869  float xDist = Math.Abs(xDiff);
870  float yDist = Math.Abs(yDiff);
871  if (tolerance.X < xDist) { continue; }
872  if (tolerance.Y < yDist) { continue; }
873 
874  float dist = 0.0f;
875  float diff = 0.0f;
876  if (horizontalSearch)
877  {
878  diff = xDiff;
879  dist = xDist + yDist / 5.0f;
880  }
881  else
882  {
883  diff = yDiff;
884  dist = yDist + xDist / 5.0f;
885  //prefer ladder waypoints when moving vertically
886  if (wp.Ladders != null) { dist *= 0.5f; }
887  }
888 
889  if (Math.Sign(diff) != dir) { continue; }
890  // Ignore if already linked
891  if (linkedTo.Contains(wp)) { continue; }
892  if (ignored != null && ignored.Contains(wp)) { continue; }
893  if (filter != null && !filter(wp)) { continue; }
894 
895  if (closest == null || dist < closestDist)
896  {
897  var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
898  if (body != null && body != ignoredBody && !(body.UserData is Submarine))
899  {
900  if (body.UserData is Structure)
901  {
902  continue;
903  }
904  if (body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall) && body.UserData is Item i && i.GetComponent<Door>() != null)
905  {
906  continue;
907  }
908  }
909 
910  closestDist = dist;
911  closest = wp;
912  }
913  }
914 
915  return closest;
916  }
917 
918  public void ConnectTo(WayPoint wayPoint2)
919  {
920  System.Diagnostics.Debug.Assert(this != wayPoint2);
921  if (!linkedTo.Contains(wayPoint2))
922  {
923  linkedTo.Add(wayPoint2);
924  OnLinksChanged?.Invoke(this);
925  }
926  if (!wayPoint2.linkedTo.Contains(this))
927  {
928  wayPoint2.linkedTo.Add(this);
929  wayPoint2.OnLinksChanged?.Invoke(wayPoint2);
930  }
931  }
932 
933  public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null, bool ignoreSubmarine = false)
934  {
935  return WayPointList.GetRandom(wp =>
936  (ignoreSubmarine || wp.Submarine == sub) &&
937  //checking for the disabled flag is not strictly necessary because we check for equality of the spawn type,
938  //but lets do that anyway in case we change the handling of the spawn type at some point
939  !wp.spawnType.HasFlag(SpawnType.Disabled) &&
940  wp.spawnType == spawnType &&
941  (spawnPointTag.IsNullOrEmpty() || wp.Tags.Any(t => t == spawnPointTag)) &&
942  (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)),
943  useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced);
944  }
945 
946  public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
947  {
948  List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
949  if (submarine.ForcedOutpostModuleWayPoints != null && submarine.ForcedOutpostModuleWayPoints.Any())
950  {
951  // narrow selection of spawn points to within the module
952  subWayPoints = new List<WayPoint>(submarine.ForcedOutpostModuleWayPoints);
953  submarine.ForcedOutpostModuleWayPoints.Clear();
954  }
955  subWayPoints.Shuffle(Rand.RandSync.Unsynced);
956 
957  List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
958 
959  WayPoint[] assignedWayPoints = new WayPoint[crew.Count];
960 
961  for (int i = 0; i < crew.Count; i++ )
962  {
963  //try to give the crew member a spawnpoint that hasn't been assigned to anyone and matches their job
964  for (int n = 0; n < unassignedWayPoints.Count; n++)
965  {
966  if (crew[i].Job.Prefab != unassignedWayPoints[n].AssignedJob) { continue; }
967  assignedWayPoints[i] = unassignedWayPoints[n];
968  unassignedWayPoints.RemoveAt(n);
969 
970  break;
971  }
972  }
973 
974  //go through the crewmembers that don't have a spawnpoint yet (if any)
975  for (int i = 0; i < crew.Count; i++)
976  {
977  if (assignedWayPoints[i] != null) { continue; }
978 
979  //try to assign a spawnpoint that matches the job, even if the spawnpoint is already assigned to someone else
980  foreach (WayPoint wp in subWayPoints)
981  {
982  if (wp.spawnType != SpawnType.Human || wp.AssignedJob != crew[i].Job.Prefab) { continue; }
983 
984  assignedWayPoints[i] = wp;
985  break;
986  }
987  if (assignedWayPoints[i] != null) { continue; }
988 
989  //try to assign a spawnpoint that isn't meant for any specific job
990  var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.AssignedJob == null);
991  if (nonJobSpecificPoints.Any())
992  {
993  assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.ServerAndClient)];
994  }
995 
996  if (assignedWayPoints[i] != null) { continue; }
997 
998  //everything else failed -> just give a random spawnpoint inside the sub
999  assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, useSyncedRand: true);
1000  }
1001 
1002  for (int i = 0; i < assignedWayPoints.Length; i++)
1003  {
1004  if (assignedWayPoints[i] == null)
1005  {
1006  DebugConsole.AddWarning("Couldn't find a waypoint for " + crew[i].Name + "!");
1007  assignedWayPoints[i] = WayPointList[0];
1008  }
1009  }
1010 
1011  return assignedWayPoints;
1012  }
1013 
1014  public static List<WayPoint> GetOutpostSpawnPoints(CharacterTeamType teamID)
1015  {
1016  List<WayPoint> spawnWaypoints = WayPointList.FindAll(wp =>
1017  wp.SpawnType == SpawnType.Human &&
1018  wp.Submarine == Level.Loaded.StartOutpost);
1020  {
1021  Identifier teamSpawnTag = ("deathmatch" + teamID).ToIdentifier();
1022  if (spawnWaypoints.Any(wp => wp.Tags.Contains(teamSpawnTag)))
1023  {
1024  spawnWaypoints = spawnWaypoints.FindAll(wp => wp.Tags.Contains(teamSpawnTag));
1025  }
1026  }
1027  else
1028  {
1029  spawnWaypoints = spawnWaypoints.FindAll(wp =>
1030  wp.CurrentHull?.OutpostModuleTags != null &&
1031  wp.CurrentHull.OutpostModuleTags.Contains(Barotrauma.Tags.Airlock));
1032  }
1033  return spawnWaypoints;
1034  }
1035 
1036  public void FindHull()
1037  {
1039 #if CLIENT
1040  //we may not be able to find the hull with the optimized method in the sub editor if new hulls have been added, use the unoptimized method
1042  {
1044  }
1045 #endif
1046  }
1047 
1048  public override void OnMapLoaded()
1049  {
1050  if (Submarine == null)
1051  {
1052  // Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc.
1053  // Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost.
1054  return;
1055  }
1056  InitializeLinks();
1057  FindHull();
1058  FindStairs();
1059  }
1060 
1061  private Structure FindStairs()
1062  {
1063  Stairs = null;
1064  Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0, 1.2f), null, Physics.CollisionStairs);
1065  if (pickedBody != null && pickedBody.UserData is Structure structure && structure.StairDirection != Direction.None)
1066  {
1067  Stairs = structure;
1068  }
1069  return Stairs;
1070  }
1071 
1072  public void InitializeLinks()
1073  {
1074  if (gapId > 0)
1075  {
1076  ConnectedGap = FindEntityByID(gapId) as Gap;
1077  gapId = 0;
1078  }
1079  if (ladderId > 0)
1080  {
1081  if (FindEntityByID(ladderId) is Item ladderItem) { Ladders = ladderItem.GetComponent<Ladder>(); }
1082  ladderId = 0;
1083  }
1084  }
1085 
1086  public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
1087  {
1088  Rectangle rect = new Rectangle(
1089  int.Parse(element.GetAttribute("x").Value),
1090  int.Parse(element.GetAttribute("y").Value),
1091  (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y);
1092 
1093  Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType);
1094  WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element))
1095  {
1096  spawnType = spawnType,
1097  Layer = element.GetAttributeString(nameof(Layer), null)
1098  };
1099 
1100  string idCardDescString = element.GetAttributeString("idcarddesc", "");
1101  if (!string.IsNullOrWhiteSpace(idCardDescString))
1102  {
1103  w.IdCardDesc = idCardDescString;
1104  }
1105  string idCardTagString = element.GetAttributeString("idcardtags", "");
1106  if (!string.IsNullOrWhiteSpace(idCardTagString))
1107  {
1108  w.IdCardTags = idCardTagString.Split(',');
1109  }
1110 
1111  w.ExitPointSize = element.GetAttributePoint("exitpointsize", Point.Zero);
1112 
1113  w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
1114 
1115  Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty);
1116  if (!jobIdentifier.IsEmpty)
1117  {
1118  w.AssignedJob = JobPrefab.Get(jobIdentifier);
1119  }
1120 
1121  w.linkedToID = new List<ushort>();
1122  w.ladderId = idRemap.GetOffsetId(element.GetAttributeInt("ladders", 0));
1123  w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0));
1124 
1125  int i = 0;
1126  while (element.GetAttribute("linkedto" + i) != null)
1127  {
1128  int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value);
1129  int destId = idRemap.GetOffsetId(srcId);
1130  if (destId > 0)
1131  {
1132  w.linkedToID.Add((ushort)destId);
1133  }
1134  else
1135  {
1136  w.unresolvedLinkedToID ??= new List<ushort>();
1137  w.unresolvedLinkedToID.Add((ushort)srcId);
1138  }
1139  i += 1;
1140  }
1141  return w;
1142  }
1143 
1144  public override XElement Save(XElement parentElement)
1145  {
1146  if (!ShouldBeSaved) return null;
1147  XElement element = new XElement("WayPoint");
1148 
1149  element.Add(new XAttribute("ID", ID),
1150  new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)),
1151  new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)),
1152  new XAttribute("spawn", spawnType),
1153  new XAttribute(nameof(Layer), Layer ?? string.Empty));
1154  if (SpawnType == SpawnType.ExitPoint)
1155  {
1156  element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize)));
1157  }
1158 
1159  if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc));
1160  if (idCardTags.Length > 0)
1161  {
1162  element.Add(new XAttribute("idcardtags", string.Join(",", idCardTags)));
1163  }
1164  if (tags.Count > 0)
1165  {
1166  element.Add(new XAttribute("tags", string.Join(",", tags)));
1167  }
1168 
1169  if (AssignedJob != null) element.Add(new XAttribute("job", AssignedJob.Identifier));
1170  if (ConnectedGap != null) element.Add(new XAttribute("gap", ConnectedGap.ID));
1171  if (Ladders != null) element.Add(new XAttribute("ladders", Ladders.Item.ID));
1172 
1173  parentElement.Add(element);
1174 
1175  if (linkedTo != null)
1176  {
1177  int i = 0;
1178  foreach (MapEntity e in linkedTo)
1179  {
1180  if (!e.ShouldBeSaved || (e.Removed != Removed)) { continue; }
1181  if (e.Submarine?.Info.Type != Submarine?.Info.Type) { continue; }
1182  element.Add(new XAttribute("linkedto" + i, e.ID));
1183  i += 1;
1184  }
1185  }
1186 
1187  return element;
1188  }
1189 
1190  public override void ShallowRemove()
1191  {
1192  base.ShallowRemove();
1193  WayPointList.Remove(this);
1194  }
1195 
1196  public override void Remove()
1197  {
1198  base.Remove();
1199  CurrentHull = null;
1200  ConnectedGap = null;
1201  Tunnel = null;
1202  Ruin = null;
1203  Stairs = null;
1204  Ladders = null;
1205  OnLinksChanged = null;
1206  WayPointList.Remove(this);
1207  }
1208  }
1209 }
string? GetAttributeString(string key, string? def)
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
Point GetAttributePoint(string key, in Point def)
int GetAttributeInt(string key, int def)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
static void RemoveAll()
Definition: Entity.cs:212
Submarine Submarine
Definition: Entity.cs:53
const ushort NullEntityID
Definition: Entity.cs:14
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 Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static GameSession GameSession
Definition: GameMain.cs:45
static readonly Screen SubEditorScreen
Definition: GameMain.cs:62
bool IsHorizontal
Definition: Gap.cs:25
Door ConnectedDoor
Definition: Gap.cs:117
static List< Gap > GapList
Definition: Gap.cs:15
override Rectangle Rect
Definition: Gap.cs:150
bool IsRoomToRoom
Definition: Gap.cs:142
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
static readonly List< Hull > HullList
static Hull FindHullUnoptimized(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any). The difference to FindHul...
ushort GetOffsetId(XElement element)
Definition: IdRemap.cs:86
static readonly List< Item > ItemList
JobPrefab Prefab
Definition: Job.cs:18
static JobPrefab Get(Identifier identifier)
Definition: JobPrefab.cs:85
virtual Sprite Sprite
Definition: MapEntity.cs:110
static readonly List< MapEntity > MapEntityList
Definition: MapEntity.cs:15
virtual Rectangle Rect
Definition: MapEntity.cs:99
List< ushort > linkedToID
Definition: MapEntity.cs:19
override Vector2 Position
Definition: MapEntity.cs:214
List< ushort > unresolvedLinkedToID
Definition: MapEntity.cs:20
readonly List< MapEntity > linkedTo
Definition: MapEntity.cs:52
override Vector2 SimPosition
Definition: MapEntity.cs:227
Prefab(ContentFile file, Identifier identifier)
Definition: Prefab.cs:40
readonly Identifier Identifier
Definition: Prefab.cs:34
static Screen Selected
Definition: Screen.cs:5
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 Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
RuinGeneration.Ruin Ruin
Definition: WayPoint.cs:74
JobPrefab AssignedJob
Definition: WayPoint.cs:116
void InitializeLinks()
Definition: WayPoint.cs:1072
static bool ShowWayPoints
Definition: WayPoint.cs:20
const float LadderWaypointInterval
Definition: WayPoint.cs:22
Structure Stairs
Definition: WayPoint.cs:28
static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap)
Definition: WayPoint.cs:1086
WayPoint(MapEntityPrefab prefab, Rectangle rectangle)
Definition: WayPoint.cs:125
string[] IdCardTags
Definition: WayPoint.cs:102
static List< WayPoint > WayPointList
Definition: WayPoint.cs:18
IEnumerable< Identifier > Tags
Definition: WayPoint.cs:114
WayPoint(Rectangle newRect, Submarine submarine)
Definition: WayPoint.cs:151
WayPoint(Type type, Rectangle newRect, Submarine submarine, ushort id=Entity.NullEntityID)
Definition: WayPoint.cs:156
Rectangle ExitPointWorldRect
Definition: WayPoint.cs:86
void OnGapStateChanged(bool open, Gap gap)
Only called by a Gap when the state changes. So in practice used like an event callback,...
Definition: WayPoint.cs:46
static WayPoint GetRandom(SpawnType spawnType=SpawnType.Human, JobPrefab assignedJob=null, Submarine sub=null, bool useSyncedRand=false, string spawnPointTag=null, bool ignoreSubmarine=false)
Definition: WayPoint.cs:933
SpawnType spawnType
Definition: WayPoint.cs:24
override string? Name
Definition: WayPoint.cs:93
override void OnMapLoaded()
Definition: WayPoint.cs:1048
WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap=null)
Definition: WayPoint.cs:118
Level.Cave Cave
Definition: WayPoint.cs:76
override void ShallowRemove()
Remove the entity from the entity list without removing links to other entities
Definition: WayPoint.cs:1190
Action< WayPoint > OnLinksChanged
Definition: WayPoint.cs:90
override MapEntity Clone()
Definition: WayPoint.cs:191
static List< WayPoint > GetOutpostSpawnPoints(CharacterTeamType teamID)
Definition: WayPoint.cs:1014
static bool GenerateSubWaypoints(Submarine submarine)
Definition: WayPoint.cs:205
Level.Tunnel Tunnel
Definition: WayPoint.cs:73
override XElement Save(XElement parentElement)
Definition: WayPoint.cs:1144
override void Remove()
Definition: WayPoint.cs:1196
SpawnType SpawnType
Definition: WayPoint.cs:79
static WayPoint[] SelectCrewSpawnPoints(List< CharacterInfo > crew, Submarine submarine)
Definition: WayPoint.cs:946
void ConnectTo(WayPoint wayPoint2)
Definition: WayPoint.cs:918