Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Levels/Level.cs
5 using FarseerPhysics;
6 using FarseerPhysics.Dynamics;
7 using Microsoft.Xna.Framework;
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Globalization;
12 using System.Linq;
13 using System.Xml.Linq;
14 using Voronoi2;
15 
16 namespace Barotrauma
17 {
18  partial class Level : Entity, IServerSerializable
19  {
20  public enum PlacementType
21  {
22  Top, Bottom
23  }
24 
25  public enum EventType
26  {
27  SingleDestructibleWall,
28  GlobalDestructibleWall
29  }
30 
31  //all entities are disabled after they reach this depth
32  public const int MaxEntityDepth = -1000000;
33  public const float ShaftHeight = 1000.0f;
34 
38  public const float OutsideBoundsCurrentMargin = 30000.0f;
39 
43  public const float OutsideBoundsCurrentMarginExponential = 150000.0f;
44 
48  public const float OutsideBoundsCurrentHardLimit = 200000.0f;
49 
53  public const int MaxSubmarineWidth = 16000;
54 
55  private static Level loaded;
56  public static Level Loaded
57  {
58  get { return loaded; }
59  private set
60  {
61  if (loaded == value) { return; }
62  loaded = value;
63  GameAnalyticsManager.SetCurrentLevel(loaded?.LevelData);
64  }
65  }
66 
67  [Flags]
68  public enum PositionType
69  {
70  None = 0,
71  MainPath = 0x1,
72  SidePath = 0x2,
73  Cave = 0x4,
74  Ruin = 0x8,
75  Wreck = 0x10,
76  BeaconStation = 0x20,
77  Abyss = 0x40,
78  AbyssCave = 0x80,
79  Outpost = 0x100,
80  }
81 
82  public struct InterestingPosition
83  {
84  public Point Position;
85  public readonly PositionType PositionType;
86  public bool IsValid;
88  public Ruin Ruin;
89  public Cave Cave;
90 
91  public InterestingPosition(Point position, PositionType positionType, Submarine submarine = null, bool isValid = true)
92  {
93  Position = position;
94  PositionType = positionType;
95  IsValid = isValid;
96  Submarine = submarine;
97  Ruin = null;
98  Cave = null;
99  }
100 
101  public InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid = true)
102  {
103  Position = position;
104  PositionType = positionType;
105  IsValid = isValid;
106  Submarine = null;
107  Ruin = ruin;
108  Cave = null;
109  }
110  public InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid = true)
111  {
112  Position = position;
113  PositionType = positionType;
114  IsValid = isValid;
115  Submarine = null;
116  Ruin = null;
117  Cave = cave;
118  }
119 
123  public bool IsEnclosedArea()
124  {
125  return
126  PositionType == PositionType.Cave ||
127  PositionType == PositionType.Ruin ||
128  PositionType == PositionType.Outpost ||
129  PositionType == PositionType.BeaconStation ||
130  PositionType == PositionType.Wreck ||
131  PositionType == PositionType.AbyssCave;
132  }
133  }
134 
135  public enum TunnelType
136  {
137  MainPath, SidePath, Cave
138  }
139 
140  public class Tunnel
141  {
142  public readonly Tunnel ParentTunnel;
143 
144  public readonly int MinWidth;
145 
146  public readonly TunnelType Type;
147 
148  public List<Point> Nodes
149  {
150  get;
151  private set;
152  }
153 
154  public List<VoronoiCell> Cells
155  {
156  get;
157  private set;
158  }
159 
160  public List<WayPoint> WayPoints
161  {
162  get;
163  private set;
164  }
165 
166  public Tunnel(TunnelType type, List<Point> nodes, int minWidth, Tunnel parentTunnel)
167  {
168  Type = type;
169  MinWidth = minWidth;
170  ParentTunnel = parentTunnel;
171  Nodes = new List<Point>(nodes);
172  Cells = new List<VoronoiCell>();
173  WayPoints = new List<WayPoint>();
174  }
175  }
176 
177  public class Cave
178  {
179  public Rectangle Area;
180 
181  public readonly List<Tunnel> Tunnels = new List<Tunnel>();
182 
183  public Point StartPos, EndPos;
184 
185  public readonly HashSet<Mission> MissionsToDisplayOnSonar = new HashSet<Mission>();
186 
188 
189  public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos)
190  {
191  CaveGenerationParams = caveGenerationParams;
192  Area = area;
193  StartPos = startPos;
194  EndPos = endPos;
195  }
196  }
197 
198  //how close the sub has to be to start/endposition to exit
199  public const float ExitDistance = 6000.0f;
200  public const int GridCellSize = 2000;
201  private List<VoronoiCell>[,] cellGrid;
202  private List<VoronoiCell> cells;
203 
205  {
206  get;
207  private set;
208  }
209 
210  public int AbyssStart
211  {
212  get { return AbyssArea.Y + AbyssArea.Height; }
213  }
214 
215  public int AbyssEnd
216  {
217  get { return AbyssArea.Y; }
218  }
219 
220  public class AbyssIsland
221  {
222  public Rectangle Area;
223  public readonly List<VoronoiCell> Cells;
224 
225  public AbyssIsland(Rectangle area, List<VoronoiCell> cells)
226  {
227  Debug.Assert(cells != null && cells.Any());
228  Area = area;
229  Cells = cells;
230  }
231  }
232  public List<AbyssIsland> AbyssIslands = new List<AbyssIsland>();
233 
234  //TODO: make private
235  public List<double> siteCoordsX, siteCoordsY;
236 
237  //TODO: make private
238  public List<(Point point, double distance)> distanceField;
239 
240  private Point startPosition, endPosition;
241 
242  private readonly Rectangle borders;
243 
244  private List<Body> bodies;
245 
246  private List<Point> bottomPositions;
247 
248  //no need for frequent network updates, as currently the only thing that's synced
249  //are the slowly moving ice chunks that move in a very predictable way
250  const float NetworkUpdateInterval = 5.0f;
251  private float networkUpdateTimer;
252 
253  public Vector2 StartPosition
254  {
255  get { return startPosition.ToVector2(); }
256  }
257 
258  private Point startExitPosition;
259  public Vector2 StartExitPosition
260  {
261  get { return startExitPosition.ToVector2(); }
262  }
263 
264  public Point Size
265  {
266  get { return LevelData.Size; }
267  }
268 
269  public Vector2 EndPosition
270  {
271  get { return endPosition.ToVector2(); }
272  }
273 
274  private Point endExitPosition;
275  public Vector2 EndExitPosition
276  {
277  get { return endExitPosition.ToVector2(); }
278  }
279 
280  public int BottomPos
281  {
282  get;
283  private set;
284  }
285 
286  public int SeaFloorTopPos
287  {
288  get;
289  private set;
290  }
291 
292  public const float DefaultRealWorldCrushDepth = 3500.0f;
293 
297  public float CrushDepth
298  {
299  get
300  {
301  return LevelData.CrushDepth;
302  }
303  }
304 
308  public float RealWorldCrushDepth
309  {
310  get
311  {
313  }
314  }
315 
316  public LevelWall SeaFloor { get; private set; }
317 
318  public List<Ruin> Ruins { get; private set; }
319 
320  public List<Submarine> Wrecks { get; private set; }
321 
322  public Submarine BeaconStation { get; private set; }
323  private Sonar beaconSonar;
324 
325  public List<LevelWall> ExtraWalls { get; private set; }
326 
327  public List<LevelWall> UnsyncedExtraWalls { get; private set; }
328 
329  public List<Tunnel> Tunnels { get; private set; } = new List<Tunnel>();
330 
331  public List<Cave> Caves { get; private set; } = new List<Cave>();
332 
333  public List<InterestingPosition> PositionsOfInterest { get; private set; }
334 
335  public Submarine StartOutpost { get; private set; }
336  public Submarine EndOutpost { get; private set; }
337 
338  private SubmarineInfo preSelectedStartOutpost;
339  private SubmarineInfo preSelectedEndOutpost;
340 
341  public readonly LevelData LevelData;
342 
347  public enum LevelGenStage
348  {
349  LevelGenParams,
350  Size,
351  GenStart,
352  TunnelGen1,
353  TunnelGen2,
354  AbyssGen,
355  CaveGen,
356  VoronoiGen,
357  VoronoiGen2,
358  VoronoiGen3,
359  Ruins,
360  Outposts,
361  FloatingIce,
362  LevelBodies,
363  IceSpires,
364  TopAndBottom,
365  PlaceLevelObjects,
366  GenerateItems,
367  Finish
368  }
369 
370  private readonly Dictionary<LevelGenStage, int> equalityCheckValues = Enum.GetValues(typeof(LevelGenStage))
371  .Cast<LevelGenStage>()
372  .Select(k => (k, 0))
373  .ToDictionary();
374  public IReadOnlyDictionary<LevelGenStage, int> EqualityCheckValues => equalityCheckValues;
375 
376  private void GenerateEqualityCheckValue(LevelGenStage stage)
377  {
378  equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient);
379  }
380 
381  private void SetEqualityCheckValue(LevelGenStage stage, int value)
382  {
383  equalityCheckValues[stage] = value;
384  }
385 
386  private void ClearEqualityCheckValues()
387  {
388  foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage)))
389  {
390  equalityCheckValues[stage] = 0;
391  }
392  }
393 
394  public List<Entity> EntitiesBeforeGenerate { get; private set; } = new List<Entity>();
395  public int EntityCountBeforeGenerate { get; private set; }
396  public int EntityCountAfterGenerate { get; private set; }
397 
398  public Body TopBarrier
399  {
400  get;
401  private set;
402  }
403 
404  public Body BottomBarrier
405  {
406  get;
407  private set;
408  }
409 
410  public LevelObjectManager LevelObjectManager { get; private set; }
411 
412  public bool Generating { get; private set; }
413 
414  public Location StartLocation { get; private set; }
415  public Location EndLocation { get; private set; }
416 
417  public bool Mirrored
418  {
419  get;
420  private set;
421  }
422 
423  public string Seed
424  {
425  get { return LevelData.Seed; }
426  }
427 
428 
429  public static float? ForcedDifficulty;
430  public float Difficulty
431  {
432  get { return ForcedDifficulty ?? LevelData.Difficulty; }
433  }
434 
438  public bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty) => LevelData.IsAllowedDifficulty(minDifficulty, maxDifficulty);
439 
441  {
442  get { return LevelData.Type; }
443  }
444 
445 
446  public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome;
447 
451  public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost;
452 
456  public static bool IsLoadedFriendlyOutpost =>
457  loaded?.Type == LevelData.LevelType.Outpost &&
458  (loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1);
459 
461  {
462  get { return LevelData.GenerationParams; }
463  }
464 
466  {
468  }
469 
470  public Color BackgroundColor
471  {
473  }
474 
475  public Color WallColor
476  {
477  get { return LevelData.GenerationParams.WallColor; }
478  }
479 
480  private Level(LevelData levelData) : base(null, 0)
481  {
482  LevelData = levelData;
483  borders = new Rectangle(Point.Zero, levelData.Size);
484  }
485 
487  {
488  if (StartOutpost != null &&
489  Type == LevelData.LevelType.Outpost &&
491  StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player))
492  {
493  return GameMain.GameSession.Campaign?.CurrentLocation is not { IsFactionHostile: true };
494  }
495  return false;
496  }
497 
498  public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
499  {
500  Debug.Assert(levelData.Biome != null);
501  if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); }
502  if (levelData.Size.X <= 0) { throw new ArgumentException("Level width needs to be larger than zero."); }
503  if (levelData.Size.Y <= 0) { throw new ArgumentException("Level height needs to be larger than zero."); }
504 
505  Level level = new Level(levelData)
506  {
507  preSelectedStartOutpost = startOutpost,
508  preSelectedEndOutpost = endOutpost
509  };
510  level.Generate(mirror, startLocation, endLocation);
511  return level;
512  }
513 
514  private void Generate(bool mirror, Location startLocation, Location endLocation)
515  {
516  Loaded?.Remove();
517  Loaded = this;
518  Generating = true;
519 #if CLIENT
520  Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode);
521 #endif
523  DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier);
524 
525  ClearEqualityCheckValues();
526  EntitiesBeforeGenerate = GetEntities().ToList();
528 
529  StartLocation = startLocation;
530  EndLocation = endLocation;
531 
532  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
533 
534  GenerateEqualityCheckValue(LevelGenStage.GenStart);
535  SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier));
536  SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16);
537 
539 
540  if (Type == LevelData.LevelType.Outpost) { mirror = false; }
541  Mirrored = mirror;
542 
543 #if CLIENT
544  if (backgroundCreatureManager == null)
545  {
546  var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
547  backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
548  }
549 #endif
550  Stopwatch sw = new Stopwatch();
551  sw.Start();
552 
553  PositionsOfInterest = new List<InterestingPosition>();
554  ExtraWalls = new List<LevelWall>();
555  UnsyncedExtraWalls = new List<LevelWall>();
556  bodies = new List<Body>();
557  List<Vector2> sites = new List<Vector2>();
558 
559  Voronoi voronoi = new Voronoi(1.0);
560 
561 #if CLIENT
562  renderer = new LevelRenderer(this);
563 #endif
564 
566 
567  int minMainPathWidth = Math.Min(GenerationParams.MinTunnelRadius, MaxSubmarineWidth);
568  int minWidth = 500;
569  if (Submarine.MainSub != null)
570  {
571  Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders();
572  dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f);
573  minWidth = Math.Max(dockedSubBorders.Width, dockedSubBorders.Height);
574  minMainPathWidth = Math.Max(minMainPathWidth, minWidth);
575  minMainPathWidth = Math.Min(minMainPathWidth, MaxSubmarineWidth);
576  }
577  minMainPathWidth = Math.Min(minMainPathWidth, borders.Width / 5);
578  LevelData.MinMainPathWidth = minMainPathWidth;
579 
580  Rectangle pathBorders = borders;
581  pathBorders.Inflate(
582  -Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5),
583  -Math.Min(minMainPathWidth * 2, borders.Height / 5));
584 
585  if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); }
586  if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); }
587 
588  startPosition = new Point(
589  (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.StartPosition.X),
590  (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.StartPosition.Y));
591  startExitPosition = new Point(startPosition.X, borders.Bottom);
592 
593  endPosition = new Point(
594  (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.EndPosition.X),
595  (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
596  endExitPosition = new Point(endPosition.X, borders.Bottom);
597 
598  GenerateEqualityCheckValue(LevelGenStage.TunnelGen1);
599 
600  //----------------------------------------------------------------------------------
601  //generate the initial nodes for the main path and smaller tunnels
602  //----------------------------------------------------------------------------------
603 
604  Tunnel mainPath = new Tunnel(
605  TunnelType.MainPath,
606  GeneratePathNodes(startPosition, endPosition, pathBorders, null, GenerationParams.MainPathVariance),
607  minMainPathWidth, parentTunnel: null);
608  Tunnels.Add(mainPath);
609 
610  Tunnel startPath = null, endPath = null, endHole = null;
611  if (GenerationParams.StartPosition.Y < 0.5f && (Mirrored ? !HasEndOutpost() : !HasStartOutpost()))
612  {
613  startPath = new Tunnel(
614  TunnelType.SidePath,
615  new List<Point>() { startExitPosition, startPosition },
616  minWidth, parentTunnel: mainPath);
617  Tunnels.Add(startPath);
618  }
619  else
620  {
621  startExitPosition = startPosition;
622  }
623  if (GenerationParams.EndPosition.Y < 0.5f && (Mirrored ? !HasStartOutpost() : !HasEndOutpost()))
624  {
625  endPath = new Tunnel(
626  TunnelType.SidePath,
627  new List<Point>() { endPosition, endExitPosition },
628  minWidth, parentTunnel: mainPath);
629  Tunnels.Add(endPath);
630  }
631  else
632  {
633  endExitPosition = endPosition;
634  }
635 
637  {
638  if (Mirrored)
639  {
640  endHole = new Tunnel(
641  TunnelType.SidePath,
642  new List<Point>() { startPosition, new Point(0, startPosition.Y) },
643  minWidth, parentTunnel: mainPath);
644  }
645  else
646  {
647  endHole = new Tunnel(
648  TunnelType.SidePath,
649  new List<Point>() { endPosition, new Point(Size.X, endPosition.Y) },
650  minWidth, parentTunnel: mainPath);
651  }
652  Tunnels.Add(endHole);
653  }
654 
655  //create a tunnel from the lowest point in the main path to the abyss
656  //to ensure there's a way to the abyss in all levels
657  Tunnel abyssTunnel = null;
659  {
660  Point lowestPoint = mainPath.Nodes.First();
661  foreach (var pathNode in mainPath.Nodes)
662  {
663  if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; }
664  }
665  abyssTunnel = new Tunnel(
666  TunnelType.SidePath,
667  new List<Point>() { lowestPoint, new Point(lowestPoint.X, 0) },
668  minWidth, parentTunnel: mainPath);
669  Tunnels.Add(abyssTunnel);
670  }
671 
672  int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient);
673 
674  for (int j = 0; j < sideTunnelCount; j++)
675  {
676  if (mainPath.Nodes.Count < 4) { break; }
677  var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
678 
679  Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)];
680  if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; }
681 
682  Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)];
683  Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)];
684 
685  var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance);
686  //make sure the path is wide enough to pass through
687  int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient);
688  Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
689  }
690 
691  CalculateTunnelDistanceField(null);
692  GenerateSeaFloorPositions();
693 
694  GenerateEqualityCheckValue(LevelGenStage.TunnelGen2);
695 
696  GenerateAbyssArea();
697 
698  GenerateEqualityCheckValue(LevelGenStage.AbyssGen);
699 
700  GenerateCaves(mainPath);
701 
702  GenerateEqualityCheckValue(LevelGenStage.CaveGen);
703 
704  //----------------------------------------------------------------------------------
705  //generate voronoi sites
706  //----------------------------------------------------------------------------------
707 
708  GenerateVoronoiSites();
709 
710  GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
711 
712  //----------------------------------------------------------------------------------
713  // construct the voronoi graph and cells
714  //----------------------------------------------------------------------------------
715 
716  Stopwatch sw2 = new Stopwatch();
717  sw2.Start();
718 
719  Debug.Assert(siteCoordsX.Count == siteCoordsY.Count);
720 
721  int remainingRetries = 5;
722  bool voronoiGraphInvalid = false;
723  do
724  {
725  remainingRetries--;
726  voronoiGraphInvalid = false;
727  //construct voronoi cells based on the graph edges
728  List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
729  cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid);
730  for (int i = 0; i < cells.Count; i++)
731  {
732  for (int j = i + 1; j < cells.Count; j++)
733  {
734  //sites can never be inside multiple cells in a voronoi graph by definition
735  //if they are, that'll cause severe issues with the rest of the level generation.
736 
737  //There seems to be a very rare issue that sometimes causes the graph to generate incorrectly (see #10944 and #12980),
738  //leading to a crash due. I haven't been able to figure out what's causing that - there don't seem to be any issues in the sites,
739  //so I'm getting the feeling it could be a bug with the voronoi graph generation.
740 
741  //If that happens, let's just retry a couple of times (re-randomizing the sites and regenerating
742  //the map seems to fix the issue in all cases I've seen)
743  if (cells[j].IsPointInside(cells[i].Center))
744  {
745  voronoiGraphInvalid = true;
746  break;
747  }
748  if (voronoiGraphInvalid) { break; }
749  }
750  }
751  if (voronoiGraphInvalid)
752  {
753  string errorMsg = "Unknown error during level generation. Invalid voronoi graph: the same voronoi site was inside multiple cells.";
754  if (remainingRetries > 0)
755  {
756  DebugConsole.AddWarning(errorMsg + " Retrying...");
757  GenerateVoronoiSites();
758  }
759  else
760  {
761  //throw a console error and let the generation finish, hoping for the best
762  DebugConsole.ThrowError(errorMsg);
763  }
764  }
765  } while (remainingRetries > 0 && voronoiGraphInvalid);
766 
767  GenerateAbyssGeometry();
768  GenerateAbyssPositions();
769 
770  Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms");
771  sw2.Restart();
772 
773  //----------------------------------------------------------------------------------
774  // generate a path through the tunnel nodes
775  //----------------------------------------------------------------------------------
776 
777  List<VoronoiCell> pathCells = new List<VoronoiCell>();
778  foreach (Tunnel tunnel in Tunnels)
779  {
780  CaveGenerator.GeneratePath(tunnel, this);
781  if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath)
782  {
783  if (tunnel != startPath && tunnel != endPath && tunnel != endHole)
784  {
785  var distinctCells = tunnel.Cells.Distinct().ToList();
786  for (int i = 2; i < distinctCells.Count; i += 3)
787  {
788  PositionsOfInterest.Add(new InterestingPosition(
789  new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y),
790  tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath,
791  Caves.Find(cave => cave.Tunnels.Contains(tunnel))));
792  }
793  }
794  }
795 
796  bool connectToParentTunnel = tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave;
797  GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel : null);
798 
799  EnlargePath(tunnel.Cells, tunnel.MinWidth);
800  foreach (var pathCell in tunnel.Cells)
801  {
802  MarkEdges(pathCell, tunnel.Type);
803  if (!pathCells.Contains(pathCell))
804  {
805  pathCells.Add(pathCell);
806  }
807  }
808 
809  static void MarkEdges(VoronoiCell cell, TunnelType tunnelType)
810  {
811  foreach (GraphEdge edge in cell.Edges)
812  {
813  switch (tunnelType)
814  {
815  case TunnelType.MainPath:
816  edge.NextToMainPath = true;
817  break;
818  case TunnelType.SidePath:
819  edge.NextToSidePath = true;
820  break;
821  case TunnelType.Cave:
822  edge.NextToCave = true;
823  break;
824  }
825  }
826  }
827  }
828 
829  var potentialIslands = new List<VoronoiCell>();
830  foreach (var cell in pathCells)
831  {
832  if (GetDistToTunnel(cell.Center, mainPath) < minMainPathWidth ||
833  (startPath != null && GetDistToTunnel(cell.Center, startPath) < minMainPathWidth) ||
834  (endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) ||
835  (endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; }
836  if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; }
837  if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; }
838  potentialIslands.Add(cell);
839  }
840  for (int i = 0; i < GenerationParams.IslandCount; i++)
841  {
842  if (potentialIslands.Count == 0) { break; }
843  var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient);
844  island.CellType = CellType.Solid;
845  island.Island = true;
846  pathCells.Remove(island);
847  }
848 
849  foreach (InterestingPosition positionOfInterest in PositionsOfInterest)
850  {
851  WayPoint wayPoint = new WayPoint(
852  positionOfInterest.Position.ToVector2(),
853  SpawnType.Enemy,
854  submarine: null);
855  }
856 
857  startPosition.X = (int)pathCells[0].Site.Coord.X;
858  startExitPosition.X = startPosition.X;
859 
860  GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2);
861 
862  //----------------------------------------------------------------------------------
863  // remove unnecessary cells and create some holes at the bottom of the level
864  //----------------------------------------------------------------------------------
865 
867  {
868  cells.ForEach(c => c.CellType = CellType.Removed);
869  cells.Clear();
870  }
871 
872  cells = cells.Except(pathCells).ToList();
873  //remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad
874  cells.ForEachMod(c =>
875  {
876  if (c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y, Size.Y) && e.AdjacentCell(c) == null))
877  {
878  c.CellType = CellType.Removed;
879  cells.Remove(c);
880  }
881  });
882 
883  int xPadding = borders.Width / 5;
884  pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minMainPathWidth));
885 
886  foreach (VoronoiCell cell in cells)
887  {
888  if (cell.Site.Coord.Y < borders.Height / 2) { continue; }
889  cell.Edges.ForEach(e => e.OutsideLevel = true);
890  }
891 
892  foreach (AbyssIsland abyssIsland in AbyssIslands)
893  {
894  abyssIsland.Cells.RemoveAll(c => c.CellType == CellType.Path);
895  cells.AddRange(abyssIsland.Cells);
896  }
897 
898  List<Point> ruinPositions = new List<Point>();
899  int ruinCount = GenerationParams.UseRandomRuinCount()
900  ? Rand.Range(GenerationParams.MinRuinCount, GenerationParams.MaxRuinCount + 1, Rand.RandSync.ServerAndClient)
902 
903  bool hasRuinMissions = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false;
904  if (hasRuinMissions)
905  {
906  ruinCount = Math.Max(ruinCount, 1);
907  }
908 
909  for (int i = 0; i < ruinCount; i++)
910  {
911  Point ruinSize = new Point(5000);
912  int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
913  int limitRight = Math.Min(endPosition.X, Size.X - ruinSize.X / 2);
914  Rectangle limits = new Rectangle(limitLeft, ruinSize.Y, limitRight - limitLeft, Size.Y - ruinSize.Y);
915  Debug.Assert(limits.Width > 0);
916  Debug.Assert(limits.Height > 0);
917  ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, limits: limits));
918  CalculateTunnelDistanceField(ruinPositions);
919  }
920 
921  //----------------------------------------------------------------------------------
922  // initialize the cells that are still left and insert them into the cell grid
923  //----------------------------------------------------------------------------------
924 
925  foreach (VoronoiCell cell in pathCells)
926  {
927  cell.Edges.ForEach(e => e.OutsideLevel = false);
928  cell.CellType = CellType.Path;
929  cells.Remove(cell);
930  }
931 
932  for (int x = 0; x < cellGrid.GetLength(0); x++)
933  {
934  for (int y = 0; y < cellGrid.GetLength(1); y++)
935  {
936  cellGrid[x, y].Clear();
937  }
938  }
939 
940  //----------------------------------------------------------------------------------
941  // mirror if needed
942  //----------------------------------------------------------------------------------
943 
944  if (mirror)
945  {
946  HashSet<GraphEdge> mirroredEdges = new HashSet<GraphEdge>();
947  HashSet<Site> mirroredSites = new HashSet<Site>();
948  List<VoronoiCell> allCells = new List<VoronoiCell>(cells);
949  allCells.AddRange(pathCells);
950  foreach (VoronoiCell cell in allCells)
951  {
952  foreach (GraphEdge edge in cell.Edges)
953  {
954  if (mirroredEdges.Contains(edge)) { continue; }
955  edge.Point1.X = borders.Width - edge.Point1.X;
956  edge.Point2.X = borders.Width - edge.Point2.X;
957  if (edge.Site1 != null && !mirroredSites.Contains(edge.Site1))
958  {
959  //make sure that sites right at the edge of a grid cell end up in the same cell as in the non-mirrored level
960  if (edge.Site1.Coord.X % GridCellSize < 1.0f &&
961  edge.Site1.Coord.X % GridCellSize >= 0.0f) { edge.Site1.Coord.X += 1.0f; }
962  edge.Site1.Coord.X = borders.Width - edge.Site1.Coord.X;
963  mirroredSites.Add(edge.Site1);
964  }
965  if (edge.Site2 != null && !mirroredSites.Contains(edge.Site2))
966  {
967  if (edge.Site2.Coord.X % GridCellSize < 1.0f &&
968  edge.Site2.Coord.X % GridCellSize >= 0.0f) { edge.Site2.Coord.X += 1.0f; }
969  edge.Site2.Coord.X = borders.Width - edge.Site2.Coord.X;
970  mirroredSites.Add(edge.Site2);
971  }
972  mirroredEdges.Add(edge);
973  }
974  }
975 
976  foreach (AbyssIsland island in AbyssIslands)
977  {
978  island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height);
979  foreach (var cell in island.Cells)
980  {
981  if (!mirroredSites.Contains(cell.Site))
982  {
983  if (cell.Site.Coord.X % GridCellSize < 1.0f &&
984  cell.Site.Coord.X % GridCellSize >= 0.0f) { cell.Site.Coord.X += 1.0f; }
985  cell.Site.Coord.X = borders.Width - cell.Site.Coord.X;
986  mirroredSites.Add(cell.Site);
987  }
988  }
989  }
990 
991  for (int i = 0; i < ruinPositions.Count; i++)
992  {
993  ruinPositions[i] = new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y);
994  }
995 
996  foreach (Cave cave in Caves)
997  {
998  cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height);
999  cave.StartPos = new Point(borders.Width - cave.StartPos.X, cave.StartPos.Y);
1000  cave.EndPos = new Point(borders.Width - cave.EndPos.X, cave.EndPos.Y);
1001  }
1002 
1003  foreach (Tunnel tunnel in Tunnels)
1004  {
1005  for (int i = 0; i < tunnel.Nodes.Count; i++)
1006  {
1007  tunnel.Nodes[i] = new Point(borders.Width - tunnel.Nodes[i].X, tunnel.Nodes[i].Y);
1008  }
1009  }
1010 
1011  for (int i = 0; i < PositionsOfInterest.Count; i++)
1012  {
1013  PositionsOfInterest[i] = new InterestingPosition(
1014  new Point(borders.Width - PositionsOfInterest[i].Position.X, PositionsOfInterest[i].Position.Y),
1015  PositionsOfInterest[i].PositionType)
1016  {
1017  Submarine = PositionsOfInterest[i].Submarine,
1018  Cave = PositionsOfInterest[i].Cave,
1019  Ruin = PositionsOfInterest[i].Ruin,
1020  };
1021  }
1022 
1023  foreach (WayPoint waypoint in WayPoint.WayPointList)
1024  {
1025  if (waypoint.Submarine != null) continue;
1026  waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f));
1027  }
1028 
1029  for (int i = 0; i < bottomPositions.Count; i++)
1030  {
1031  bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y);
1032  }
1033  bottomPositions.Reverse();
1034 
1035  startPosition.X = borders.Width - startPosition.X;
1036  endPosition.X = borders.Width - endPosition.X;
1037 
1038  startExitPosition.X = borders.Width - startExitPosition.X;
1039  endExitPosition.X = borders.Width - endExitPosition.X;
1040 
1041  CalculateTunnelDistanceField(ruinPositions);
1042  }
1043 
1044  foreach (VoronoiCell cell in cells)
1045  {
1046  int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize);
1047  x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1);
1048  int y = (int)Math.Floor(cell.Site.Coord.Y / GridCellSize);
1049  y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1);
1050 
1051  cellGrid[x, y].Add(cell);
1052  }
1053 
1054  float destructibleWallRatio = MathHelper.Lerp(0.2f, 1.0f, LevelData.Difficulty / 100.0f);
1055  foreach (Cave cave in Caves)
1056  {
1057  if (cave.Area.Y > 0)
1058  {
1059  List<VoronoiCell> cavePathCells = CreatePathToClosestTunnel(cave.StartPos);
1060 
1061  var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type != TunnelType.Cave);
1062 
1063  WayPoint prevWp = mainTunnel.WayPoints.First();
1064  if (prevWp != null)
1065  {
1066  for (int i = 0; i < cavePathCells.Count; i++)
1067  {
1068  var connectingEdge = i > 0 ? cavePathCells[i].Edges.Find(e => e.AdjacentCell(cavePathCells[i]) == cavePathCells[i - 1]) : null;
1069  if (connectingEdge != null)
1070  {
1071  var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null);
1072  ConnectWaypoints(prevWp, edgeWayPoint, 500.0f);
1073  prevWp = edgeWayPoint;
1074  }
1075  var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null);
1076  ConnectWaypoints(prevWp, newWaypoint, 500.0f);
1077  prevWp = newWaypoint;
1078  }
1079  var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints);
1080  ConnectWaypoints(prevWp, closestPathPoint, 500.0f);
1081  }
1082  }
1083 
1084  List<VoronoiCell> caveCells = new List<VoronoiCell>();
1085  caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
1086  foreach (var caveCell in caveCells)
1087  {
1088  if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; }
1089  if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
1090  {
1091  var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
1092  if (chunk != null)
1093  {
1094  chunk.Body.BodyType = BodyType.Static;
1095  ExtraWalls.Add(chunk);
1096  }
1097  }
1098  }
1099  }
1100 
1101  GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3);
1102 
1103  //----------------------------------------------------------------------------------
1104  // create some ruins
1105  //----------------------------------------------------------------------------------
1106 
1107  Ruins = new List<Ruin>();
1108  for (int i = 0; i < ruinPositions.Count; i++)
1109  {
1110  Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i);
1111  GenerateRuin(ruinPositions[i], mirror, hasRuinMissions);
1112  }
1113 
1114  GenerateEqualityCheckValue(LevelGenStage.Ruins);
1115 
1116  //----------------------------------------------------------------------------------
1117  // create floating ice chunks
1118  //----------------------------------------------------------------------------------
1119 
1121  {
1122  List<Point> iceChunkPositions = new List<Point>();
1123  foreach (InterestingPosition pos in PositionsOfInterest)
1124  {
1125  if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; }
1126  if (pos.Position.X < pathBorders.X + minMainPathWidth || pos.Position.X > pathBorders.Right - minMainPathWidth) { continue; }
1127  if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; }
1128  if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; }
1129  iceChunkPositions.Add(pos.Position);
1130  }
1131 
1132  for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++)
1133  {
1134  if (iceChunkPositions.Count == 0) { break; }
1135  Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)];
1136  float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient);
1137  var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f);
1138  var chunk = CreateIceChunk(vertices, selectedPos.ToVector2());
1139  chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f);
1140  chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient);
1141  ExtraWalls.Add(chunk);
1142  iceChunkPositions.Remove(selectedPos);
1143  }
1144  }
1145 
1146  GenerateEqualityCheckValue(LevelGenStage.FloatingIce);
1147 
1148  //----------------------------------------------------------------------------------
1149  // generate the bodies and rendered triangles of the cells
1150  //----------------------------------------------------------------------------------
1151 
1152  foreach (VoronoiCell cell in cells)
1153  {
1154  foreach (GraphEdge ge in cell.Edges)
1155  {
1156  VoronoiCell adjacentCell = ge.AdjacentCell(cell);
1157  ge.IsSolid = adjacentCell == null || !cells.Contains(adjacentCell);
1158  }
1159  }
1160 
1161  List<VoronoiCell> cellsWithBody = new List<VoronoiCell>(cells);
1163  {
1164  foreach (VoronoiCell cell in cellsWithBody)
1165  {
1166  CaveGenerator.RoundCell(cell,
1167  minEdgeLength: GenerationParams.CellSubdivisionLength,
1168  roundingAmount: GenerationParams.CellRoundingAmount,
1169  irregularity: GenerationParams.CellIrregularity);
1170  }
1171  }
1172 
1173 #if CLIENT
1174  List<(List<VoronoiCell> cells, Cave parentCave)> cellBatches = new List<(List<VoronoiCell>, Cave)>
1175  {
1176  (cellsWithBody.ToList(), null)
1177  };
1178  foreach (Cave cave in Caves)
1179  {
1180  (List<VoronoiCell> cells, Cave parentCave) newCellBatch = (new List<VoronoiCell>(), cave);
1181  foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells))
1182  {
1183  foreach (var edge in caveCell.Edges)
1184  {
1185  if (!edge.NextToCave) { continue; }
1186  if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell1))
1187  {
1188  Debug.Assert(cellsWithBody.Contains(edge.Cell1));
1189  cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell1));
1190  newCellBatch.cells.Add(edge.Cell1);
1191  }
1192  if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell2))
1193  {
1194  Debug.Assert(cellsWithBody.Contains(edge.Cell2));
1195  cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell2));
1196  newCellBatch.cells.Add(edge.Cell2);
1197  }
1198  }
1199  }
1200  if (newCellBatch.cells.Any())
1201  {
1202  cellBatches.Add(newCellBatch);
1203  }
1204  }
1205  cellBatches.RemoveAll(cb => !cb.cells.Any());
1206 
1207  int totalCellsInBatches = cellBatches.Sum(cb => cb.cells.Count);
1208  Debug.Assert(cellsWithBody.Count == totalCellsInBatches);
1209 
1210  List<List<Vector2[]>> triangleLists = new List<List<Vector2[]>>();
1211  foreach ((List<VoronoiCell> cells, Cave cave) cellBatch in cellBatches)
1212  {
1213  bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.cells, this, out List<Vector2[]> triangles));
1214  triangleLists.Add(triangles);
1215  }
1216 #else
1217  bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List<Vector2[]> triangles));
1218 #endif
1219  foreach (VoronoiCell cell in cells)
1220  {
1221  CompareCCW compare = new CompareCCW(cell.Center);
1222  foreach (GraphEdge edge in cell.Edges)
1223  {
1224  //remove references to cells that we failed to generate a body for
1225  if (edge.Cell1 != null && edge.Cell1.Body == null && edge.Cell1.CellType != CellType.Empty) { edge.Cell1 = null; }
1226  if (edge.Cell2 != null && edge.Cell2.Body == null && edge.Cell2.CellType != CellType.Empty) { edge.Cell2 = null; }
1227 
1228  //make the order of the points CCW
1229  if (compare.Compare(edge.Point1, edge.Point2) == -1)
1230  {
1231  var temp = edge.Point1;
1232  edge.Point1 = edge.Point2;
1233  edge.Point2 = temp;
1234  }
1235  }
1236  }
1237 
1238 #if CLIENT
1239  Debug.Assert(triangleLists.Count == cellBatches.Count);
1240  for (int i = 0; i < triangleLists.Count; i++)
1241  {
1242  renderer.SetVertices(
1243  CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(),
1244  CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(),
1245  cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture,
1246  cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture,
1248  }
1249 #endif
1250 
1251  GenerateEqualityCheckValue(LevelGenStage.LevelBodies);
1252 
1253  //----------------------------------------------------------------------------------
1254  // create ice spires
1255  //----------------------------------------------------------------------------------
1256 
1257  List<GraphEdge> usedSpireEdges = new List<GraphEdge>();
1258  for (int i = 0; i < GenerationParams.IceSpireCount; i++)
1259  {
1260  var spire = CreateIceSpire(usedSpireEdges);
1261  if (spire != null) { ExtraWalls.Add(spire); };
1262  }
1263 
1264  GenerateEqualityCheckValue(LevelGenStage.IceSpires);
1265 
1266  //----------------------------------------------------------------------------------
1267  // connect side paths and cave branches to their parents
1268  //----------------------------------------------------------------------------------
1269 
1270  foreach (Ruin ruin in Ruins)
1271  {
1272  GenerateRuinWayPoints(ruin);
1273  }
1274 
1275  foreach (Tunnel tunnel in Tunnels)
1276  {
1277  if (tunnel.ParentTunnel == null) { continue; }
1278  if (tunnel.Type == TunnelType.Cave && tunnel.ParentTunnel == mainPath) { continue; }
1279  ConnectWaypoints(tunnel, tunnel.ParentTunnel);
1280  }
1281 
1282  //----------------------------------------------------------------------------------
1283  // create outposts at the start and end of the level
1284  //----------------------------------------------------------------------------------
1285 
1286  CreateOutposts();
1287 
1288  GenerateEqualityCheckValue(LevelGenStage.Outposts);
1289 
1290  //----------------------------------------------------------------------------------
1291  // top barrier & sea floor
1292  //----------------------------------------------------------------------------------
1293 
1294  TopBarrier = GameMain.World.CreateEdge(
1295  ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)),
1296  ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0)));
1297  //for debugging purposes
1298  TopBarrier.UserData = "topbarrier";
1299  TopBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, borders.Height)), 0.0f);
1300  TopBarrier.BodyType = BodyType.Static;
1301  TopBarrier.CollisionCategories = Physics.CollisionLevel;
1302 
1303  bodies.Add(TopBarrier);
1304 
1305  GenerateSeaFloor();
1306 
1307  if (mirror)
1308  {
1309  Point tempP = startPosition;
1310  startPosition = endPosition;
1311  endPosition = tempP;
1312 
1313  tempP = startExitPosition;
1314  startExitPosition = endExitPosition;
1315  endExitPosition = tempP;
1316  }
1317  if (StartOutpost != null)
1318  {
1319  startExitPosition = StartOutpost.WorldPosition.ToPoint();
1320  startPosition = startExitPosition;
1321  }
1322  if (EndOutpost != null)
1323  {
1324  endExitPosition = EndOutpost.WorldPosition.ToPoint();
1325  endPosition = endExitPosition;
1326  }
1327 
1328  CreateWrecks();
1329  CreateBeaconStation();
1330 
1331  GenerateEqualityCheckValue(LevelGenStage.TopAndBottom);
1332 
1334 
1335  GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects);
1336 
1337  GenerateItems();
1338 
1339  foreach (Submarine sub in Submarine.Loaded)
1340  {
1341  if (sub.Info.IsOutpost)
1342  {
1343 #if CLIENT
1344  if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
1345 #endif
1346  OutpostGenerator.PowerUpOutpost(sub);
1347  }
1348  }
1349 
1350  GenerateEqualityCheckValue(LevelGenStage.GenerateItems);
1351 
1352 #if CLIENT
1353  backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount);
1354 #endif
1355 
1356  foreach (VoronoiCell cell in cells)
1357  {
1358  foreach (GraphEdge edge in cell.Edges)
1359  {
1360  //edge.Cell1 = null;
1361  //edge.Cell2 = null;
1362  edge.Site1 = null;
1363  edge.Site2 = null;
1364  }
1365  }
1366 
1367  //initialize MapEntities that aren't in any sub (e.g. items inside ruins)
1368  MapEntity.MapLoaded(MapEntity.MapEntityList.FindAll(me => me.Submarine == null), false);
1369 
1370  Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms");
1371  sw2.Restart();
1372 
1373  Debug.WriteLine("**********************************************************************************");
1374  Debug.WriteLine("Generated a map with " + siteCoordsX.Count + " sites in " + sw.ElapsedMilliseconds + " ms");
1375  Debug.WriteLine("Seed: " + Seed);
1376  Debug.WriteLine("**********************************************************************************");
1377 
1378  if (GameSettings.CurrentConfig.VerboseLogging)
1379  {
1380  DebugConsole.NewMessage("Generated level with the seed " + Seed + " (type: " + GenerationParams.Identifier + ")", Color.White);
1381  }
1382 
1384 
1385 #if SERVER
1386  if (GameMain.Server.EntityEventManager.Events.Count() > 0)
1387  {
1388  DebugConsole.NewMessage("WARNING: Entity events have been created during level generation. Events should not be created until the round is fully initialized.");
1389  }
1390  GameMain.Server.EntityEventManager.Clear();
1391 #endif
1392 
1393  GenerateEqualityCheckValue(LevelGenStage.Finish);
1394  //assign an ID to make entity events work
1395  //ID = FindFreeID();
1396  Generating = false;
1397  }
1398 
1399 
1400  private void GenerateVoronoiSites()
1401  {
1402  Point siteInterval = GenerationParams.VoronoiSiteInterval;
1403  int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
1404  Point siteVariance = GenerationParams.VoronoiSiteVariance;
1405  siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
1406  siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
1407  const int caveSiteInterval = 500;
1408  for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
1409  {
1410  for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
1411  {
1412  int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
1413  int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
1414 
1415  bool closeToTunnel = false;
1416  bool closeToCave = false;
1417  foreach (Tunnel tunnel in Tunnels)
1418  {
1419  float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
1420  for (int i = 1; i < tunnel.Nodes.Count; i++)
1421  {
1422  if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; }
1423  if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; }
1424  if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; }
1425  if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; }
1426 
1427  double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
1428  if (Math.Sqrt(tunnelDistSqr) < minDist)
1429  {
1430  closeToTunnel = true;
1431  //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
1432  if (tunnel.Type == TunnelType.Cave)
1433  {
1434  closeToCave = true;
1435  }
1436  break;
1437  }
1438  }
1439  }
1440 
1441  if (!closeToTunnel)
1442  {
1443  //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry
1444  if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
1445  }
1446 
1447  if (!TooCloseToOtherSites(siteX, siteY))
1448  {
1449  siteCoordsX.Add(siteX);
1450  siteCoordsY.Add(siteY);
1451  }
1452 
1453  if (closeToCave)
1454  {
1455  for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
1456  {
1457  for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
1458  {
1459  int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
1460  int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
1461 
1462  if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
1463  {
1464  siteCoordsX.Add(caveSiteX);
1465  siteCoordsY.Add(caveSiteY);
1466  }
1467  }
1468  }
1469  }
1470  }
1471  }
1472 
1473  bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f)
1474  {
1475  float minDistanceSqr = minDistance * minDistance;
1476  for (int i = 0; i < siteCoordsX.Count; i++)
1477  {
1478  if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
1479  {
1480  return true;
1481  }
1482  }
1483  return false;
1484  }
1485 
1486  for (int i = 0; i < siteCoordsX.Count; i++)
1487  {
1488  Debug.Assert(
1489  !double.IsNaN(siteCoordsX[i]) && !double.IsInfinity(siteCoordsX[i]),
1490  $"Potential error in level generation: invalid voronoi site ({siteCoordsX[i]})");
1491  Debug.Assert(
1492  !double.IsNaN(siteCoordsY[i]) && !double.IsInfinity(siteCoordsY[i]),
1493  $"Potential error in level generation: invalid voronoi site ({siteCoordsY[i]})");
1494  Debug.Assert(
1495  siteCoordsX[i] > 0 || siteCoordsY[i] > 0,
1496  $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
1497  Debug.Assert(
1498  siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height,
1499  $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
1500  for (int j = i + 1; j < siteCoordsX.Count; j++)
1501  {
1502  Debug.Assert(
1503  MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 10.0f,
1504  "Potential error in level generation: two voronoi sites are extremely close to each other.");
1505  }
1506  }
1507  }
1508 
1509  private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance)
1510  {
1511  List<Point> pathNodes = new List<Point> { startPosition };
1512 
1513  Point nodeInterval = GenerationParams.MainPathNodeIntervalRange;
1514 
1515  for (int x = startPosition.X + nodeInterval.X;
1516  x < endPosition.X - nodeInterval.X;
1517  x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient))
1518  {
1519  Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient));
1520 
1521  //allow placing the 2nd main path node at any height regardless of variance
1522  //(otherwise low variance will always make the main path go through the upper part of the level)
1523  if (pathNodes.Count > 2 || parentTunnel != null)
1524  {
1525  nodePos.Y = (int)MathHelper.Clamp(
1526  nodePos.Y,
1527  pathNodes.Last().Y - pathBorders.Height * variance * 0.5f,
1528  pathNodes.Last().Y + pathBorders.Height * variance * 0.5f);
1529  }
1530  if (pathNodes.Count == 1)
1531  {
1532  //if the path starts below the center of the level, head up and vice versa
1533  //to utilize as much of the vertical space as possible
1534  nodePos.Y = (int)(startPosition.Y + Math.Abs(nodePos.Y - startPosition.Y) * -Math.Sign(nodePos.Y - pathBorders.Center.Y));
1535  nodePos.Y = MathHelper.Clamp(nodePos.Y, pathBorders.Y, pathBorders.Bottom);
1536  }
1537 
1538  //prevent intersections with other tunnels
1539  foreach (Tunnel tunnel in Tunnels)
1540  {
1541  for (int i = 1; i < tunnel.Nodes.Count; i++)
1542  {
1543  Point node1 = tunnel.Nodes[i - 1];
1544  Point node2 = tunnel.Nodes[i];
1545  if (node1.X >= nodePos.X) { continue; }
1546  if (node2.X <= pathNodes.Last().X) { continue; }
1547  if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) { continue; }
1548  if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth &&
1549  !MathUtils.LineSegmentsIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2()))
1550  {
1551  continue;
1552  }
1553 
1554  if (nodePos.Y < pathNodes.Last().Y)
1555  {
1556  nodePos.Y = Math.Min(Math.Max(node1.Y, node2.Y) + tunnel.MinWidth * 2, pathBorders.Bottom);
1557  }
1558  else
1559  {
1560  nodePos.Y = Math.Max(Math.Min(node1.Y, node2.Y) - tunnel.MinWidth * 2, pathBorders.Y);
1561  }
1562  break;
1563  }
1564  }
1565 
1566  pathNodes.Add(nodePos);
1567  }
1568 
1569  if (pathNodes.Count == 1)
1570  {
1571  pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y));
1572  }
1573 
1574  pathNodes.Add(endPosition);
1575  return pathNodes;
1576  }
1577 
1578  private List<VoronoiCell> CreateHoles(float holeProbability, Rectangle limits, int submarineSize)
1579  {
1580  List<VoronoiCell> toBeRemoved = new List<VoronoiCell>();
1581  foreach (VoronoiCell cell in cells)
1582  {
1583  if (cell.Edges.Any(e => e.NextToCave)) { continue; }
1584  if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) { continue; }
1585  if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; }
1586 
1587  float closestDist = 0.0f;
1588  Point? closestTunnelNode = null;
1589  foreach (Tunnel tunnel in Tunnels)
1590  {
1591  foreach (Point node in tunnel.Nodes)
1592  {
1593  float dist = Math.Abs(cell.Center.X - node.X);
1594  if (closestTunnelNode == null || dist < closestDist)
1595  {
1596  closestDist = dist;
1597  closestTunnelNode = node;
1598  }
1599  }
1600  }
1601 
1602  if (closestTunnelNode != null && closestTunnelNode.Value.Y < cell.Center.Y) { continue; }
1603 
1604  toBeRemoved.Add(cell);
1605  }
1606 
1607  return toBeRemoved;
1608  }
1609 
1610  private void EnlargePath(List<VoronoiCell> pathCells, float minWidth)
1611  {
1612  if (minWidth <= 0.0f) { return; }
1613 
1614  List<VoronoiCell> removedCells = GetTooCloseCells(pathCells, minWidth);
1615  foreach (VoronoiCell removedCell in removedCells)
1616  {
1617  if (removedCell.CellType == CellType.Path) { continue; }
1618 
1619  pathCells.Add(removedCell);
1620  removedCell.CellType = CellType.Path;
1621  }
1622  }
1623 
1624  private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel)
1625  {
1626  if (tunnel.Cells.Count == 0) { return; }
1627 
1628  List<WayPoint> wayPoints = new List<WayPoint>();
1629  WayPoint prevWayPoint = null;
1630  for (int i = 0; i < tunnel.Cells.Count; i++)
1631  {
1632  tunnel.Cells[i].CellType = CellType.Path;
1633  var newWaypoint = new WayPoint(new Rectangle((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Center.Y, 10, 10), null)
1634  {
1635  Tunnel = tunnel
1636  };
1637  wayPoints.Add(newWaypoint);
1638 
1639  if (prevWayPoint != null)
1640  {
1641  bool solidCellBetween = false;
1642  foreach (GraphEdge edge in tunnel.Cells[i].Edges)
1643  {
1644  if (edge.AdjacentCell(tunnel.Cells[i])?.CellType == CellType.Solid &&
1645  MathUtils.LineSegmentsIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2))
1646  {
1647  solidCellBetween = true;
1648  break;
1649  }
1650  }
1651 
1652  if (solidCellBetween)
1653  {
1654  //something between the previous waypoint and this one
1655  // -> find the edge that connects the cells and place a waypoint there, instead of connecting the centers of the cells directly
1656  var edgeBetweenCells = tunnel.Cells[i].Edges.Find(e => e.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[i - 1]);
1657  if (edgeBetweenCells != null)
1658  {
1659  var edgeWaypoint = new WayPoint(new Rectangle((int)edgeBetweenCells.Center.X, (int)edgeBetweenCells.Center.Y, 10, 10), null)
1660  {
1661  Tunnel = tunnel
1662  };
1663  prevWayPoint.ConnectTo(edgeWaypoint);
1664  prevWayPoint = edgeWaypoint;
1665  }
1666  }
1667  prevWayPoint.ConnectTo(newWaypoint);
1668 
1669  //look back at the tunnel cells before the previous one, and see if the current cell shares edges with them
1670  //= if we can "skip" from cell #1 to cell #3, create a waypoint between them.
1671  //Fixes there sometimes not being a path past a destructible ice chunk even if there's space to go past it.
1672  for (int j = i - 2; j > 0 && j > i - 5; j--)
1673  {
1674  foreach (GraphEdge edge in tunnel.Cells[i].Edges)
1675  {
1676  if (Vector2.DistanceSquared(edge.Point1, edge.Point2) < 30.0f * 30.0f) { continue; }
1677  if (!edge.IsSolid && edge.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[j])
1678  {
1679  var edgeWaypoint = new WayPoint(new Rectangle((int)edge.Center.X, (int)edge.Center.Y, 10, 10), null)
1680  {
1681  Tunnel = tunnel
1682  };
1683  wayPoints[j].ConnectTo(edgeWaypoint);
1684  edgeWaypoint.ConnectTo(newWaypoint);
1685  break;
1686  }
1687  }
1688  }
1689  }
1690  prevWayPoint = newWaypoint;
1691  }
1692 
1693  tunnel.WayPoints.AddRange(wayPoints);
1694 
1695  //connect to the tunnel we're branching off from
1696  if (parentTunnel != null)
1697  {
1698  var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel);
1699  if (parentStart != null)
1700  {
1701  wayPoints.First().ConnectTo(parentStart);
1702  }
1703  if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave)
1704  {
1705  var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel);
1706  if (parentEnd != null)
1707  {
1708  wayPoints.Last().ConnectTo(parentEnd);
1709  }
1710  }
1711  }
1712  }
1713 
1714  private void ConnectWaypoints(Tunnel tunnel, Tunnel parentTunnel)
1715  {
1716  foreach (WayPoint wayPoint in tunnel.WayPoints)
1717  {
1718  var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel);
1719  if (closestWaypoint == null) { continue; }
1720  if (Submarine.PickBody(
1721  ConvertUnits.ToSimUnits(wayPoint.WorldPosition),
1722  ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null)
1723  {
1724  float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f;
1725  ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel);
1726  }
1727  }
1728  }
1729 
1730  private List<WayPoint> ConnectWaypoints(WayPoint wp1, WayPoint wp2, float interval)
1731  {
1732  List<WayPoint> newWaypoints = new List<WayPoint>();
1733 
1734  Vector2 diff = wp2.WorldPosition - wp1.WorldPosition;
1735  float dist = diff.Length();
1736 
1737  WayPoint prevWaypoint = wp1;
1738  for (float x = interval; x < dist - interval; x += interval)
1739  {
1740  var newWaypoint = new WayPoint(wp1.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null);
1741  prevWaypoint.ConnectTo(newWaypoint);
1742  prevWaypoint = newWaypoint;
1743  newWaypoints.Add(newWaypoint);
1744  }
1745  prevWaypoint.ConnectTo(wp2);
1746 
1747  return newWaypoints;
1748  }
1749 
1750  private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel)
1751  {
1752  return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints);
1753  }
1754 
1755  private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable<WayPoint> waypoints, Func<WayPoint, bool> filter = null)
1756  {
1757  float closestDist = float.PositiveInfinity;
1758  WayPoint closestWayPoint = null;
1759  foreach (WayPoint otherWayPoint in waypoints)
1760  {
1761  float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition);
1762  if (dist < closestDist)
1763  {
1764  if (filter != null)
1765  {
1766  if (!filter(otherWayPoint)) { continue; }
1767  }
1768  closestDist = dist;
1769  closestWayPoint = otherWayPoint;
1770  }
1771  }
1772  return closestWayPoint;
1773  }
1774 
1775  private List<VoronoiCell> GetTooCloseCells(List<VoronoiCell> emptyCells, float minDistance)
1776  {
1777  List<VoronoiCell> tooCloseCells = new List<VoronoiCell>();
1778  if (minDistance <= 0.0f) { return tooCloseCells; }
1779  foreach (var cell in emptyCells.Distinct())
1780  {
1781  foreach (var tooCloseCell in GetTooCloseCells(cell.Center, minDistance))
1782  {
1783  if (!tooCloseCells.Contains(tooCloseCell))
1784  {
1785  tooCloseCells.Add(tooCloseCell);
1786  }
1787  }
1788  }
1789  return tooCloseCells;
1790  }
1791 
1792  public List<VoronoiCell> GetTooCloseCells(Vector2 position, float minDistance)
1793  {
1794  HashSet<VoronoiCell> tooCloseCells = new HashSet<VoronoiCell>();
1795  var closeCells = GetCells(position, searchDepth: Math.Max((int)Math.Ceiling(minDistance / GridCellSize), 3));
1796  float minDistSqr = minDistance * minDistance;
1797  foreach (VoronoiCell cell in closeCells)
1798  {
1799  bool tooClose = false;
1800 
1801  //if the cell is very large, the position can be far away from the edges while being inside the cell
1802  //so we need to check that here too
1803  if (cell.IsPointInside(position))
1804  {
1805  tooClose = true;
1806  }
1807  else
1808  {
1809  foreach (GraphEdge edge in cell.Edges)
1810  {
1811  if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
1812  Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
1813  MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
1814  {
1815  tooClose = true;
1816  break;
1817  }
1818  }
1819  }
1820  if (tooClose) { tooCloseCells.Add(cell); }
1821  }
1822  return tooCloseCells.ToList();
1823  }
1824 
1825  private void GenerateAbyssPositions()
1826  {
1827  int count = 10;
1828  for (int i = 0; i < count; i++)
1829  {
1830  float xPos = MathHelper.Lerp(borders.X, borders.Right, i / (float)(count - 1));
1831  float seaFloorPos = GetBottomPosition(xPos).Y;
1832 
1833  //above the bottom of the level = can't place a point here
1834  if (seaFloorPos > AbyssStart) { continue; }
1835 
1836  float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient));
1837 
1838  foreach (var abyssIsland in AbyssIslands)
1839  {
1840  if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos)))
1841  {
1842  xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
1843  }
1844  }
1845 
1846  PositionsOfInterest.Add(new InterestingPosition(new Point((int)xPos, (int)yPos), PositionType.Abyss));
1847  }
1848  }
1849 
1850  private void GenerateAbyssArea()
1851  {
1852  int abyssStartY = borders.Y - 5000;
1853  int abyssEndY = Math.Max(abyssStartY - 100000, BottomPos + 1000);
1854  int abyssHeight = abyssStartY - abyssEndY;
1855 
1856  if (abyssHeight < 0)
1857  {
1858  abyssStartY = borders.Y;
1859  abyssEndY = BottomPos;
1860  if (abyssStartY - abyssEndY < 1000)
1861  {
1862 #if DEBUG
1863  DebugConsole.ThrowError("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
1864 #else
1865  DebugConsole.AddWarning("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
1866 #endif
1867  }
1868  }
1869  else if (abyssHeight > 30000)
1870  {
1871  //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth
1872  //but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth)
1873  if (abyssEndY + CrushDepth < 0 && abyssStartY > -CrushDepth)
1874  {
1875  abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2);
1876  }
1877 
1878  if (abyssStartY - abyssEndY < 10000)
1879  {
1880  abyssStartY = borders.Y;
1881  }
1882  }
1883 
1884  AbyssArea = new Rectangle(borders.X, abyssEndY, borders.Width, abyssStartY - abyssEndY);
1885  }
1886 
1887  private void GenerateAbyssGeometry()
1888  {
1889  //TODO: expose island parameters
1890 
1891  Voronoi voronoi = new Voronoi(1.0);
1892  Point siteInterval = new Point(500, 500);
1893  Point siteVariance = new Point(200, 200);
1894 
1895  Point islandSize = Vector2.Lerp(
1896  GenerationParams.AbyssIslandSizeMin.ToVector2(),
1897  GenerationParams.AbyssIslandSizeMax.ToVector2(),
1898  Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint();
1899 
1900  if (AbyssArea.Height < islandSize.Y) { return; }
1901 
1902  int createdCaves = 0;
1903  int islandCount = GenerationParams.AbyssIslandCount;
1904  for (int i = 0; i < islandCount; i++)
1905  {
1906  Point islandPosition = Point.Zero;
1907  Rectangle islandArea = new Rectangle(islandPosition, islandSize);
1908 
1909  //prevent overlaps
1910  int tries = 0;
1911  const int MaxTries = 20;
1912  do
1913  {
1914  islandPosition = new Point(
1915  Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient),
1916  Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient));
1917 
1918  //move the island above the sea floor geometry
1919  islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500);
1920  islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X + islandArea.Width).Y + 500);
1921 
1922  islandArea.Location = islandPosition;
1923 
1924  tries++;
1925  } while ((AbyssIslands.Any(island => island.Area.Intersects(islandArea)) || islandArea.Bottom > AbyssArea.Bottom) && tries < MaxTries);
1926 
1927  if (tries >= MaxTries)
1928  {
1929  break;
1930  }
1931 
1932  bool createCave =
1933  //force at least one abyss cave
1934  (i == islandCount - 1 && createdCaves == 0) ||
1935  Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < GenerationParams.AbyssIslandCaveProbability;
1936  if (!createCave)
1937  {
1938  //just create a chunk with no cave
1939  float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f;
1940  var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance);
1941  Vector2 position = islandArea.Center.ToVector2();
1942  for (int j = 0; j < vertices.Count; j++)
1943  {
1944  vertices[j] += position;
1945  }
1946  var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this, createBody: false);
1947  AbyssIslands.Add(new AbyssIsland(islandArea, newChunk.Cells));
1948  continue;
1949  }
1950 
1951  var siteCoordsX = new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
1952  var siteCoordsY = new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
1953 
1954  for (int x = islandArea.X; x < islandArea.Right; x += siteInterval.X)
1955  {
1956  for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y)
1957  {
1958  siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient));
1959  siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient));
1960  }
1961  }
1962 
1963  var graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), islandArea);
1964  var islandCells = CaveGenerator.GraphEdgesToCells(graphEdges, islandArea, GridCellSize, out var cellGrid);
1965 
1966  //make the island elliptical
1967  for (int j = islandCells.Count - 1; j >= 0; j--)
1968  {
1969  var cell = islandCells[j];
1970  double xDiff = (cell.Site.Coord.X - islandArea.Center.X) / (islandArea.Width * 0.5);
1971  double yDiff = (cell.Site.Coord.Y - islandArea.Center.Y) / (islandArea.Height * 0.5);
1972 
1973  //a conical stalactite-like shape at the bottom
1974  if (yDiff < 0) { xDiff += xDiff * Math.Abs(yDiff); }
1975 
1976  double normalizedDist = Math.Sqrt(xDiff * xDiff + yDiff * yDiff);
1977  if (normalizedDist > 0.95 ||
1978  cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.X)) ||
1979  cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.Right)) ||
1980  cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Y)) ||
1981  cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Bottom)))
1982  {
1983  islandCells[j].CellType = CellType.Removed;
1984  islandCells.RemoveAt(j);
1985  }
1986  }
1987 
1988  var caveParams = CaveGenerationParams.GetRandom(this, abyss: true, rand: Rand.RandSync.ServerAndClient);
1989 
1990  float caveScaleRelativeToIsland = 0.7f;
1991  GenerateCave(
1992  caveParams, Tunnels.First(),
1993  new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2),
1994  new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland)));
1995  AbyssIslands.Add(new AbyssIsland(islandArea, islandCells));
1996  createdCaves++;
1997  }
1998  }
1999 
2000  private void GenerateSeaFloorPositions()
2001  {
2004 
2005  bottomPositions = new List<Point>
2006  {
2007  new Point(0, BottomPos)
2008  };
2009 
2010  int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.ServerAndClient);
2011  for (int i = 0; i < mountainCount; i++)
2012  {
2013  bottomPositions.Add(
2014  new Point(Size.X / (mountainCount + 1) * (i + 1),
2015  BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.ServerAndClient)));
2016  }
2017  bottomPositions.Add(new Point(Size.X, BottomPos));
2018 
2019  int minVertexInterval = 5000;
2020  float currInverval = Size.X / 2;
2021  while (currInverval > minVertexInterval)
2022  {
2023  for (int i = 0; i < bottomPositions.Count - 1; i++)
2024  {
2025  bottomPositions.Insert(i + 1,
2026  new Point(
2027  (bottomPositions[i].X + bottomPositions[i + 1].X) / 2,
2028  (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.ServerAndClient)));
2029  i++;
2030  }
2031 
2032  currInverval /= 2;
2033  }
2034 
2035  SeaFloorTopPos = bottomPositions.Max(p => p.Y);
2036  }
2037 
2038  private void GenerateSeaFloor()
2039  {
2040  SeaFloor = new LevelWall(bottomPositions.Select(p => p.ToVector2()).ToList(), new Vector2(0.0f, -2000.0f), GenerationParams.WallColor, this);
2041  ExtraWalls.Add(SeaFloor);
2042 
2043  BottomBarrier = GameMain.World.CreateEdge(
2044  ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)),
2045  ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0)));
2046  //for debugging purposes
2047  BottomBarrier.UserData = "bottombarrier";
2048  BottomBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, BottomPos)), 0.0f);
2049  BottomBarrier.BodyType = BodyType.Static;
2050  BottomBarrier.CollisionCategories = Physics.CollisionLevel;
2051 
2052  bodies.Add(BottomBarrier);
2053  }
2054 
2055  private void GenerateCaves(Tunnel parentTunnel)
2056  {
2057  for (int i = 0; i < GenerationParams.CaveCount; i++)
2058  {
2059  var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient);
2060  Point caveSize = new Point(
2061  Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient),
2062  Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient));
2063  int padding = (int)(caveSize.X * 1.2f);
2064  Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2);
2065 
2066  int radius = Math.Max(caveSize.X, caveSize.Y) / 2;
2067  var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.25f, asCloseAsPossible: true, allowedArea);
2068 
2069  GenerateCave(caveParams, parentTunnel, cavePos, caveSize);
2070 
2071  CalculateTunnelDistanceField(null);
2072  }
2073  }
2074 
2075  private void GenerateCave(CaveGenerationParams caveParams, Tunnel parentTunnel, Point cavePos, Point caveSize)
2076  {
2077  Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize);
2078  Point closestParentNode = parentTunnel.Nodes.First();
2079  double closestDist = double.PositiveInfinity;
2080  foreach (Point node in parentTunnel.Nodes)
2081  {
2082  if (caveArea.Contains(node)) { continue; }
2083  double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y);
2084  if (dist < closestDist)
2085  {
2086  closestParentNode = node;
2087  closestDist = dist;
2088  }
2089  }
2090 
2091  if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
2092  {
2093  caveStartPosVector = caveArea.Location.ToVector2();
2094  }
2095 
2096  Point caveStartPos = caveStartPosVector.ToPoint();
2097  Point caveEndPos = cavePos - (caveStartPos - cavePos);
2098 
2099  Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos);
2100  Caves.Add(cave);
2101 
2102  var caveSegments = MathUtils.GenerateJaggedLine(
2103  caveStartPos.ToVector2(), caveEndPos.ToVector2(),
2104  iterations: 3,
2105  offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f,
2106  bounds: caveArea,
2107  rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
2108 
2109  if (!caveSegments.Any()) { return; }
2110 
2111  List<Tunnel> caveBranches = new List<Tunnel>();
2112 
2113  var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 150, parentTunnel);
2114  Tunnels.Add(tunnel);
2115  caveBranches.Add(tunnel);
2116 
2117  int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient);
2118  for (int j = 0; j < branches; j++)
2119  {
2120  Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient);
2121  Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2();
2122  Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2();
2123  var branchSegments = MathUtils.GenerateJaggedLine(
2124  branchStartPos, branchEndPos,
2125  iterations: 3,
2126  offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f,
2127  bounds: caveArea,
2128  rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
2129  if (!branchSegments.Any()) { continue; }
2130 
2131  var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch);
2132  Tunnels.Add(branch);
2133  caveBranches.Add(branch);
2134  }
2135 
2136  foreach (Tunnel branch in caveBranches)
2137  {
2138  var node = branch.Nodes.Last();
2139  PositionsOfInterest.Add(new InterestingPosition(node, node.Y < AbyssArea.Bottom ? PositionType.AbyssCave : PositionType.Cave, cave));
2140  cave.Tunnels.Add(branch);
2141  }
2142 
2143  static List<Point> SegmentsToNodes(List<Vector2[]> segments)
2144  {
2145  List<Point> nodes = new List<Point>();
2146  foreach (Vector2[] segment in segments)
2147  {
2148  nodes.Add(segment[0].ToPoint());
2149  }
2150  nodes.Add(segments.Last()[1].ToPoint());
2151  return nodes;
2152  }
2153  }
2154 
2155  private void GenerateRuin(Point ruinPos, bool mirror, bool requireMissionReadyRuin)
2156  {
2157  float GetWeight(RuinGenerationParams p)
2158  {
2159  if (p.PreferredDifficulty == -1) { return 100; }
2160  float diff = Math.Abs(Difficulty - p.PreferredDifficulty) / 100;
2161  float weight = MathUtils.Pow(1 - diff, 10);
2162  return Math.Max(weight, 0);
2163  }
2164  IEnumerable<RuinGenerationParams> possibleRuinGenerationParams = RuinGenerationParams.RuinParams;
2165  if (requireMissionReadyRuin)
2166  {
2167  possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.IsMissionReady);
2168  }
2169  if (possibleRuinGenerationParams.Multiple())
2170  {
2171  // Sort by weight and choose from the closest 25% of the candidates.
2172  // Prevents choosing from the "wrong" end, which would otherwise be possible (yet rare), because we use a weighted random for the pick.
2173  possibleRuinGenerationParams = possibleRuinGenerationParams
2174  /* the prefabs aren't in a consistent order, so we need to sort them first to ensure the clients and server choose the same one */
2175  .OrderByDescending(p => p.UintIdentifier)
2176  .OrderByDescending(GetWeight)
2177  .Take((int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1));
2178  }
2179  var selectedRuinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient);
2180  if (selectedRuinGenerationParams == null)
2181  {
2182  DebugConsole.ThrowError("Failed to generate alien ruins. Could not find any RuinGenerationParameters!");
2183  return;
2184  }
2185  DebugConsole.NewMessage($"Creating alien ruins using {selectedRuinGenerationParams.Identifier} (preferred difficulty: {selectedRuinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow, debugOnly: true);
2186 
2187  LocationType locationType = StartLocation?.Type;
2188  if (locationType == null)
2189  {
2190  locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
2191  if (selectedRuinGenerationParams.AllowedLocationTypes.Any())
2192  {
2193  locationType = LocationType.Prefabs.Where(lt =>
2194  selectedRuinGenerationParams.AllowedLocationTypes.Any(allowedType =>
2195  allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient);
2196  }
2197  }
2198 
2199  var ruin = new Ruin(this, selectedRuinGenerationParams, locationType, ruinPos, mirror);
2200  if (ruin.Submarine != null)
2201  {
2202  SetLinkedSubCrushDepth(ruin.Submarine);
2203  }
2204 
2205  Ruins.Add(ruin);
2206  var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4);
2207 
2208  foreach (VoronoiCell cell in tooClose)
2209  {
2210  if (cell.CellType == CellType.Empty) { continue; }
2211  if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; }
2212  foreach (GraphEdge e in cell.Edges)
2213  {
2214  if (ruin.Area.Contains(e.Point1) || ruin.Area.Contains(e.Point2) ||
2215  MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, ruin.Area, out _))
2216  {
2217  cell.CellType = CellType.Removed;
2218  for (int x = 0; x < cellGrid.GetLength(0); x++)
2219  {
2220  for (int y = 0; y < cellGrid.GetLength(1); y++)
2221  {
2222  cellGrid[x, y].Remove(cell);
2223  }
2224  }
2225  cells.Remove(cell);
2226  break;
2227  }
2228  }
2229  }
2230 
2231  ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center);
2232  }
2233 
2234  private void GenerateRuinWayPoints(Ruin ruin)
2235  {
2236  var tooClose = GetTooCloseCells(ruin.Area.Center.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 6);
2237 
2238  List<WayPoint> wayPoints = new List<WayPoint>();
2239  float outSideWaypointInterval = 500.0f;
2240  WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
2241  Rectangle waypointArea = ruin.Area;
2242  waypointArea.Inflate(100, 100);
2243 
2244  //generate waypoints around the ruin
2245  for (int i = 0; i < 2; i++)
2246  {
2247  for (float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval)
2248  {
2249  var wayPoint = new WayPoint(new Vector2(x, waypointArea.Y + waypointArea.Height * i), SpawnType.Path, null)
2250  {
2251  Ruin = ruin
2252  };
2253  wayPoints.Add(wayPoint);
2254  if (x == waypointArea.X + outSideWaypointInterval)
2255  {
2256  cornerWaypoint[i, 0] = wayPoint;
2257  }
2258  else
2259  {
2260  wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
2261  }
2262  }
2263  cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1];
2264  }
2265 
2266  for (int i = 0; i < 2; i++)
2267  {
2268  WayPoint wayPoint = null;
2269  for (float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval)
2270  {
2271  wayPoint = new WayPoint(new Vector2(waypointArea.X + waypointArea.Width * i, y), SpawnType.Path, null)
2272  {
2273  Ruin = ruin
2274  };
2275  wayPoints.Add(wayPoint);
2276  if (y == waypointArea.Y)
2277  {
2278  wayPoint.ConnectTo(cornerWaypoint[0, i]);
2279  }
2280  else
2281  {
2282  wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
2283  }
2284  }
2285  wayPoint.ConnectTo(cornerWaypoint[1, i]);
2286  }
2287 
2288  //remove waypoints that are inside walls
2289  for (int i = wayPoints.Count - 1; i >= 0; i--)
2290  {
2291  WayPoint wp = wayPoints[i];
2292  var overlappingCell = tooClose.Find(c => c.CellType != CellType.Removed && c.IsPointInside(wp.WorldPosition));
2293  if (overlappingCell == null) { continue; }
2294  if (wp.linkedTo.Count > 1)
2295  {
2296  WayPoint linked1 = wp.linkedTo[0] as WayPoint;
2297  WayPoint linked2 = wp.linkedTo[1] as WayPoint;
2298  linked1.ConnectTo(linked2);
2299  }
2300  wp.Remove();
2301  wayPoints.RemoveAt(i);
2302  }
2303 
2304  Debug.Assert(wayPoints.Any(), "Couldn't generate waypoints around ruins.");
2305 
2306  //connect ruin entrances to the outside waypoints
2307  foreach (Gap g in Gap.GapList)
2308  {
2309  if (g.Submarine != ruin.Submarine || g.IsRoomToRoom || g.linkedTo.Count == 0) { continue; }
2310  var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g);
2311  if (gapWaypoint == null) { continue; }
2312 
2313  //place another waypoint in front of the entrance
2314  Vector2 entranceDir = Vector2.Zero;
2315  if (g.IsHorizontal)
2316  {
2317  entranceDir = Vector2.UnitX * 2 * Math.Sign(g.WorldPosition.X - g.linkedTo[0].WorldPosition.X);
2318  }
2319  else
2320  {
2321  entranceDir = Vector2.UnitY * 2 * Math.Sign(g.WorldPosition.Y - g.linkedTo[0].WorldPosition.Y);
2322  }
2323  var entranceWayPoint = new WayPoint(g.WorldPosition + entranceDir * 64.0f, SpawnType.Path, null)
2324  {
2325  Ruin = ruin
2326  };
2327  entranceWayPoint.ConnectTo(gapWaypoint);
2328  var closestWp = FindClosestWayPoint(entranceWayPoint.WorldPosition, wayPoints, (wp) =>
2329  {
2330  return Submarine.PickBody(
2331  ConvertUnits.ToSimUnits(wp.WorldPosition),
2332  ConvertUnits.ToSimUnits(entranceWayPoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
2333  });
2334  if (closestWp == null) { continue; }
2335  ConnectWaypoints(entranceWayPoint, closestWp, outSideWaypointInterval);
2336  }
2337 
2338  //create a waypoint path from the ruin to the closest tunnel
2339  WayPoint prevWp = FindClosestWayPoint(ruin.PathCells.First().Center, wayPoints, (wp) =>
2340  {
2341  return Submarine.PickBody(
2342  ConvertUnits.ToSimUnits(wp.WorldPosition),
2343  ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
2344  });
2345  if (prevWp != null)
2346  {
2347  for (int i = 0; i < ruin.PathCells.Count; i++)
2348  {
2349  var connectingEdge = i > 0 ? ruin.PathCells[i].Edges.Find(e => e.AdjacentCell(ruin.PathCells[i]) == ruin.PathCells[i - 1]) : null;
2350  if (connectingEdge != null)
2351  {
2352  var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null);
2353  ConnectWaypoints(prevWp, edgeWayPoint, outSideWaypointInterval);
2354  prevWp = edgeWayPoint;
2355  }
2356  var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null);
2357  ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval);
2358  prevWp = newWaypoint;
2359  }
2360  var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, Tunnels.SelectMany(t => t.WayPoints));
2361  ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval);
2362  }
2363  }
2364 
2365  private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null)
2366  {
2367  var pointsAboveBottom = distanceField.FindAll(d => d.point.Y > GetBottomPosition(d.point.X).Y + minDistance);
2368  if (pointsAboveBottom.Count == 0)
2369  {
2370  DebugConsole.ThrowError("Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?");
2371  return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.ServerAndClient)].point;
2372  }
2373 
2374  var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point)));
2375  if (!validPoints.Any())
2376  {
2377  DebugConsole.AddWarning("Failed to find a valid position far enough from the main path. Choosing the furthest possible position.\n" + Environment.StackTrace);
2378  if (limits != null)
2379  {
2380  //try choosing something within the specified limits
2381  validPoints = pointsAboveBottom.FindAll(d => limits.Value.Contains(d.point));
2382  }
2383  if (!validPoints.Any())
2384  {
2385  //couldn't find anything, let's just go with the furthest one
2386  validPoints = pointsAboveBottom;
2387  }
2388  (Point position, double distance) furthestPoint = validPoints.First();
2389  foreach (var point in validPoints)
2390  {
2391  if (point.distance > furthestPoint.distance)
2392  {
2393  furthestPoint = point;
2394  }
2395  }
2396  return furthestPoint.position;
2397  }
2398 
2399  if (asCloseAsPossible)
2400  {
2401  if (!validPoints.Any()) { validPoints = distanceField; }
2402  (Point position, double distance) closestPoint = validPoints.First();
2403  foreach (var point in validPoints)
2404  {
2405  if (point.distance < closestPoint.distance)
2406  {
2407  closestPoint = point;
2408  }
2409  }
2410  return closestPoint.position;
2411  }
2412  else
2413  {
2414  return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point;
2415  }
2416  }
2417 
2418  private void CalculateTunnelDistanceField(List<Point> ruinPositions)
2419  {
2420  int density = 1000;
2421  distanceField = new List<(Point point, double distance)>();
2422 
2423  if (Mirrored)
2424  {
2425  for (int x = Size.X - 1; x >= 0; x -= density)
2426  {
2427  for (int y = 0; y < Size.Y; y += density)
2428  {
2429  addPoint(x, y);
2430  }
2431  }
2432  }
2433  else
2434  {
2435  for (int x = 0; x < Size.X; x += density)
2436  {
2437  for (int y = 0; y < Size.Y; y += density)
2438  {
2439  addPoint(x, y);
2440  }
2441  }
2442  }
2443 
2444  void addPoint(int x, int y)
2445  {
2446  Point point = new Point(x, y);
2447  double shortestDistSqr = double.PositiveInfinity;
2448  foreach (Tunnel tunnel in Tunnels)
2449  {
2450  for (int i = 1; i < tunnel.Nodes.Count; i++)
2451  {
2452  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
2453  }
2454  }
2455  if (ruinPositions != null)
2456  {
2457  int ruinSize = 10000;
2458  foreach (Point ruinPos in ruinPositions)
2459  {
2460  double xDiff = Math.Abs(point.X - ruinPos.X);
2461  double yDiff = Math.Abs(point.Y - ruinPos.Y);
2462  if (xDiff < ruinSize && yDiff < ruinSize)
2463  {
2464  shortestDistSqr = 0.0f;
2465  }
2466  else
2467  {
2468  shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr);
2469  }
2470  }
2471  }
2472  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y));
2473  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom));
2474  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y));
2475  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom));
2476  distanceField.Add((point, Math.Sqrt(shortestDistSqr)));
2477  }
2478  }
2479 
2480  private double GetDistToTunnel(Vector2 position, Tunnel tunnel)
2481  {
2482  Point point = position.ToPoint();
2483  double shortestDistSqr = double.PositiveInfinity;
2484  for (int i = 1; i < tunnel.Nodes.Count; i++)
2485  {
2486  shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
2487  }
2488  return Math.Sqrt(shortestDistSqr);
2489  }
2490 
2491  private DestructibleLevelWall CreateIceChunk(IEnumerable<GraphEdge> edges, Vector2 position, float? health = null)
2492  {
2493  List<Vector2> vertices = new List<Vector2>();
2494  foreach (GraphEdge edge in edges)
2495  {
2496  if (!vertices.Any())
2497  {
2498  vertices.Add(edge.Point1);
2499  }
2500  else if (!vertices.Any(v => v.NearlyEquals(edge.Point1)))
2501  {
2502  vertices.Add(edge.Point1);
2503  }
2504  else if (!vertices.Any(v => v.NearlyEquals(edge.Point2)))
2505  {
2506  vertices.Add(edge.Point2);
2507  }
2508  }
2509  if (vertices.Count < 3) { return null; }
2510  return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health);
2511  }
2512 
2513  private DestructibleLevelWall CreateIceChunk(List<Vector2> vertices, Vector2 position, float? health = null)
2514  {
2515  DestructibleLevelWall newChunk = new DestructibleLevelWall(vertices, Color.White, this, health, true);
2516  newChunk.Body.Position = ConvertUnits.ToSimUnits(position);
2517  newChunk.Cells.ForEach(c => c.Translation = position);
2518  newChunk.Body.BodyType = BodyType.Dynamic;
2519  newChunk.Body.FixedRotation = true;
2520  newChunk.Body.LinearDamping = 0.5f;
2521  newChunk.Body.IgnoreGravity = true;
2522  newChunk.Body.Mass *= 10.0f;
2523  return newChunk;
2524  }
2525 
2526  private DestructibleLevelWall CreateIceSpire(List<GraphEdge> usedSpireEdges)
2527  {
2528  const float maxLength = 15000.0f;
2529  float minEdgeLength = 100.0f;
2530  var mainPathPos = PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient);
2531  double closestDistSqr = double.PositiveInfinity;
2532  GraphEdge closestEdge = null;
2533  VoronoiCell closestCell = null;
2534  foreach (VoronoiCell cell in cells)
2535  {
2536  if (cell.CellType != CellType.Solid) { continue; }
2537  foreach (GraphEdge edge in cell.Edges)
2538  {
2539  if (!edge.IsSolid || usedSpireEdges.Contains(edge) || edge.NextToCave) { continue; }
2540  //don't spawn spires near the start/end of the level
2541  if (edge.Center.Y > Size.Y / 2 && (edge.Center.X < Size.X * 0.3f || edge.Center.X > Size.X * 0.7f)) { continue; }
2542  if (Vector2.DistanceSquared(edge.Center, StartPosition) < maxLength * maxLength) { continue; }
2543  if (Vector2.DistanceSquared(edge.Center, EndPosition) < maxLength * maxLength) { continue; }
2544  //don't spawn on very long or very short edges
2545  float edgeLengthSqr = Vector2.DistanceSquared(edge.Point1, edge.Point2);
2546  if (edgeLengthSqr > 1000.0f * 1000.0f || edgeLengthSqr < minEdgeLength * minEdgeLength) { continue; }
2547  //don't spawn on edges facing away from the main path
2548  if (Vector2.Dot(Vector2.Normalize(mainPathPos.Position.ToVector2()) - edge.Center, edge.GetNormal(cell)) < 0.5f) { continue; }
2549  double distSqr = MathUtils.DistanceSquared(edge.Center.X, edge.Center.Y, mainPathPos.Position.X, mainPathPos.Position.Y);
2550  if (distSqr < closestDistSqr)
2551  {
2552  closestDistSqr = distSqr;
2553  closestEdge = edge;
2554  closestCell = cell;
2555  }
2556  }
2557  }
2558 
2559  if (closestEdge == null) { return null; }
2560 
2561  usedSpireEdges.Add(closestEdge);
2562 
2563  Vector2 edgeNormal = closestEdge.GetNormal(closestCell);
2564  float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength);
2565  spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f);
2566 
2567  Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
2568  Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
2569  List<Vector2> vertices = new List<Vector2>()
2570  {
2571  closestEdge.Point1,
2572  extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
2573  extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
2574  closestEdge.Point2,
2575  };
2576  Vector2 center = Vector2.Zero;
2577  vertices.ForEach(v => center += v);
2578  center /= vertices.Count;
2579  DestructibleLevelWall spire = new DestructibleLevelWall(vertices.Select(v => v - center).ToList(), Color.White, this, health: 100.0f, giftWrap: true);
2580 #if CLIENT
2581  //make the edge at the bottom of the spire non-solid
2582  foreach (GraphEdge edge in spire.Cells[0].Edges)
2583  {
2584  if ((edge.Point1.NearlyEquals(closestEdge.Point1 - center) && edge.Point2.NearlyEquals(closestEdge.Point2 - center)) ||
2585  (edge.Point1.NearlyEquals(closestEdge.Point2 - center) && edge.Point2.NearlyEquals(closestEdge.Point1 - center)))
2586  {
2587  edge.IsSolid = false;
2588  break;
2589  }
2590  }
2591  spire.GenerateVertices();
2592 #endif
2593  spire.Body.Position = ConvertUnits.ToSimUnits(center);
2594  spire.Body.BodyType = BodyType.Static;
2595  spire.Body.FixedRotation = true;
2596  spire.Body.IgnoreGravity = true;
2597  spire.Body.Mass *= 10.0f;
2598  spire.Cells.ForEach(c => c.Translation = center);
2599  spire.WallDamageOnTouch = 50.0f;
2600  return spire;
2601  }
2602 
2603  // TODO: Improve this temporary level editor debug solution (or remove it)
2604  private static int nextPathPointId;
2605  public List<PathPoint> PathPoints { get; } = new List<PathPoint>();
2606  public struct PathPoint
2607  {
2608  public string Id { get; }
2609  public Vector2 Position { get; }
2610  public bool ShouldContainResources { get; set; }
2612  {
2613  get
2614  {
2615  return ClusterLocations.Count switch
2616  {
2617  1 => 5.0f,
2618  2 => 2.5f,
2619  3 => 1.0f,
2620  _ => 0.0f,
2621  };
2622  }
2623  }
2624  public List<Identifier> ResourceTags { get; }
2625  public List<Identifier> ResourceIds { get; }
2626  public List<ClusterLocation> ClusterLocations { get; }
2627  public TunnelType TunnelType { get; }
2628 
2629  private PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType, List<Identifier> resourceTags, List<Identifier> resourceIds, List<ClusterLocation> clusterLocations)
2630  {
2631  Id = id;
2632  Position = position;
2633  ShouldContainResources = shouldContainResources;
2634  ResourceTags = resourceTags;
2635  ResourceIds = resourceIds;
2636  ClusterLocations = clusterLocations;
2637  TunnelType = tunnelType;
2638  }
2639 
2640  public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)
2641  : this(id, position, shouldContainResources, tunnelType, new List<Identifier>(), new List<Identifier>(), new List<ClusterLocation>())
2642  {
2643 
2644  }
2645 
2646  public PathPoint WithResources(bool containsResources)
2647  {
2648  return new PathPoint(Id, Position, containsResources, TunnelType, ResourceTags, ResourceIds, ClusterLocations);
2649  }
2650  }
2651 
2652  public List<ClusterLocation> AbyssResources { get; } = new List<ClusterLocation>();
2653  public struct ClusterLocation
2654  {
2655  public VoronoiCell Cell { get; }
2656  public GraphEdge Edge { get; }
2657  public Vector2 EdgeCenter { get; }
2661  public List<Item> Resources { get; private set; }
2662 
2664  public ClusterLocation(VoronoiCell cell, GraphEdge edge, bool initializeResourceList = false)
2665  {
2666  Cell = cell;
2667  Edge = edge;
2668  EdgeCenter = edge.Center;
2669  Resources = initializeResourceList ? new List<Item>() : null;
2670  }
2671 
2672  public bool Equals(ClusterLocation anotherLocation) =>
2673  Cell == anotherLocation.Cell && Edge == anotherLocation.Edge;
2674 
2675  public bool Equals(VoronoiCell cell, GraphEdge edge) =>
2676  Cell == cell && Edge == edge;
2677 
2678  public void InitializeResources()
2679  {
2680  Resources = new List<Item>();
2681  }
2682  }
2683 
2684  // TODO: Take into account items which aren't ores or plants
2685  // Such as the exploding crystals in The Great Sea
2686  private void GenerateItems()
2687  {
2688  var levelResources = new List<(ItemPrefab itemPrefab, ItemPrefab.CommonnessInfo commonnessInfo)>();
2689  var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>();
2690  Vector2 commonnessRange = new Vector2(float.MaxValue, float.MinValue), caveCommonnessRange = new Vector2(float.MaxValue, float.MinValue);
2691  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier))
2692  {
2693  if (itemPrefab.GetCommonnessInfo(this) is { CanAppear: true } commonnessInfo)
2694  {
2695  if (commonnessInfo.Commonness > 0.0)
2696  {
2697  if (commonnessInfo.Commonness < commonnessRange.X) { commonnessRange.X = commonnessInfo.Commonness; }
2698  if (commonnessInfo.Commonness > commonnessRange.Y) { commonnessRange.Y = commonnessInfo.Commonness; }
2699  }
2700  if (commonnessInfo.CaveCommonness > 0.0)
2701  {
2702  if (commonnessInfo.CaveCommonness < caveCommonnessRange.X) { caveCommonnessRange.X = commonnessInfo.CaveCommonness; }
2703  if (commonnessInfo.CaveCommonness > caveCommonnessRange.Y) { caveCommonnessRange.Y = commonnessInfo.CaveCommonness; }
2704  }
2705  levelResources.Add((itemPrefab, commonnessInfo));
2706  }
2707  else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) ||
2708  itemPrefab.LevelQuantity.TryGetValue(LevelData.Biome.Identifier, out fixedQuantityResourceInfo) ||
2709  itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo))
2710  {
2711  fixedResources.Add((itemPrefab, fixedQuantityResourceInfo));
2712  }
2713  }
2714 
2715  DebugConsole.Log("Generating level resources...");
2716  var allValidLocations = GetAllValidClusterLocations();
2717  var maxResourceOverlap = 0.4f;
2718 
2719  foreach (var (itemPrefab, resourceInfo) in fixedResources)
2720  {
2721  for (int i = 0; i < resourceInfo.ClusterQuantity; i++)
2722  {
2723  var location = allValidLocations.GetRandom(l =>
2724  {
2725  if (l.Cell == null || l.Edge == null) { return false; }
2726  if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
2727  if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
2728  if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
2729  return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
2730 
2731  }, randSync: Rand.RandSync.ServerAndClient);
2732 
2733  if (location.Cell == null || location.Edge == null) { break; }
2734 
2735  PlaceResources(itemPrefab, resourceInfo.ClusterSize, location, out _);
2736  var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
2737  allValidLocations.RemoveAt(locationIndex);
2738  }
2739  }
2740 
2741  // Abyss Resources
2742  AbyssResources.Clear();
2743 
2744  var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f);
2745  if (abyssResourcePrefabs.Any())
2746  {
2748  for (int i = 0; i < abyssClusterCount; i++)
2749  {
2750  var selectedPrefab = ToolBox.SelectWeightedRandom(
2751  abyssResourcePrefabs.Select(r => r.itemPrefab).ToList(),
2752  abyssResourcePrefabs.Select(r => r.commonnessInfo.AbyssCommonness).ToList(),
2753  Rand.RandSync.ServerAndClient);
2754 
2755  var location = allValidLocations.GetRandom(l =>
2756  {
2757  if (l.Cell == null || l.Edge == null) { return false; }
2758  if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; }
2759  l.InitializeResources();
2760  return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _);
2761  }, randSync: Rand.RandSync.ServerAndClient);
2762 
2763  if (location.Cell == null || location.Edge == null) { break; }
2764 
2765  int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient);
2766  PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0);
2767  var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true);
2768  abyssClusterLocation.Resources.AddRange(placedResources);
2769  AbyssResources.Add(abyssClusterLocation);
2770 
2771  var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
2772  allValidLocations.RemoveAt(locationIndex);
2773  }
2774  }
2775 
2776 
2777  PathPoints.Clear();
2778  nextPathPointId = 0;
2779 
2780  foreach (Tunnel tunnel in Tunnels)
2781  {
2782  var tunnelLength = 0.0f;
2783  for (int i = 1; i < tunnel.Nodes.Count; i++)
2784  {
2785  tunnelLength += Vector2.Distance(tunnel.Nodes[i - 1].ToVector2(), tunnel.Nodes[i].ToVector2());
2786  }
2787 
2788  var nextNodeIndex = 1;
2789  var positionOnPath = tunnel.Nodes.First().ToVector2();
2790  var lastNodePos = tunnel.Nodes.Last().ToVector2();
2791  var reachedLastNode = false;
2793  do
2794  {
2795  var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient);
2796  reachedLastNode = !CalculatePositionOnPath();
2797  var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++;
2798  var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ?
2800  var containsResources = true;
2801  if (spawnChance < 1.0f)
2802  {
2803  var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient);
2804  containsResources = spawnPointRoll <= spawnChance;
2805  }
2806  var tunnelType = tunnel.Type;
2807  if (tunnel.ParentTunnel != null && tunnel.ParentTunnel.Type == TunnelType.Cave) { tunnelType = TunnelType.Cave; }
2808  PathPoints.Add(new PathPoint(id, positionOnPath, containsResources, tunnel.Type));
2809 
2810  bool CalculatePositionOnPath(float checkedDist = 0.0f)
2811  {
2812  if (nextNodeIndex >= tunnel.Nodes.Count) { return false; }
2813  var distToNextNode = Vector2.Distance(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2());
2814  var lerpAmount = (distance - checkedDist) / distToNextNode;
2815  if (lerpAmount <= 1.0f)
2816  {
2817  positionOnPath = Vector2.Lerp(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2(), lerpAmount);
2818  return true;
2819  }
2820  else
2821  {
2822  positionOnPath = tunnel.Nodes[nextNodeIndex++].ToVector2();
2823  return CalculatePositionOnPath(checkedDist + distToNextNode);
2824  }
2825  }
2826  } while (!reachedLastNode && Vector2.DistanceSquared(positionOnPath, lastNodePos) > (intervalRange.Y * intervalRange.Y));
2827  }
2828 
2829  int itemCount = 0;
2830  Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() };
2831 
2832  var disabledPathPoints = new List<string>();
2833  // Create first cluster for each spawn point
2834  foreach (var pathPoint in PathPoints)
2835  {
2836  if (itemCount >= GenerationParams.ItemCount) { break; }
2837  if (!pathPoint.ShouldContainResources) { continue; }
2838  GenerateFirstCluster(pathPoint);
2839  if (pathPoint.ClusterLocations.Count > 0) { continue; }
2840  disabledPathPoints.Add(pathPoint.Id);
2841  }
2842  // Don't try to spawn more resource clusters for points for which the initial cluster could not be spawned
2843  foreach (string pathPointId in disabledPathPoints)
2844  {
2845  if (PathPoints.FirstOrNull(p => p.Id == pathPointId) is PathPoint pathPoint)
2846  {
2847  PathPoints.RemoveAll(p => p.Id == pathPointId);
2848  PathPoints.Add(pathPoint.WithResources(false));
2849  }
2850  }
2851 
2852  var excludedPathPointIds = new List<string>();
2853  while (itemCount < GenerationParams.ItemCount)
2854  {
2855  var availablePathPoints = PathPoints.Where(p =>
2856  p.ShouldContainResources && p.NextClusterProbability > 0 &&
2857  !excludedPathPointIds.Contains(p.Id)).ToList();
2858 
2859  if (availablePathPoints.None()) { break; }
2860 
2861  var pathPoint = ToolBox.SelectWeightedRandom(
2862  availablePathPoints,
2863  availablePathPoints.Select(p => p.NextClusterProbability).ToList(),
2864  Rand.RandSync.ServerAndClient);
2865 
2866  GenerateAdditionalCluster(pathPoint);
2867  }
2868 
2869 #if DEBUG
2870  int spawnPointsContainingResources = PathPoints.Where(p => p.ClusterLocations.Any()).Count();
2871  string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count);
2872  DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" +
2873  $" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" +
2874  $" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk");
2875  if (AbyssResources.Count > 0)
2876  {
2877  DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" +
2878  $" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk");
2879  }
2880 #endif
2881 
2882  DebugConsole.Log("Level resources generated");
2883 
2884  bool GenerateFirstCluster(PathPoint pathPoint)
2885  {
2886  var intervalRange = pathPoint.TunnelType != TunnelType.Cave ?
2888  allValidLocations.Sort((x, y) =>
2889  Vector2.DistanceSquared(pathPoint.Position, x.EdgeCenter)
2890  .CompareTo(Vector2.DistanceSquared(pathPoint.Position, y.EdgeCenter)));
2891  var selectedLocationIndex = -1;
2892  var generatedCluster = false;
2893  for (int i = 0; i < allValidLocations.Count; i++)
2894  {
2895  var validLocation = allValidLocations[i];
2896  if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) { continue; }
2897  if (validLocation.EdgeCenter.Y < AbyssArea.Bottom) { continue; }
2898  var distanceSquaredToEdge = Vector2.DistanceSquared(pathPoint.Position, validLocation.EdgeCenter);
2899  // Edge isn't too far from the path point
2900  if (distanceSquaredToEdge > 3.0f * (intervalRange.Y * intervalRange.Y)) { continue; }
2901  // Edge is closer to the path point than the cell center
2902  if (distanceSquaredToEdge > Vector2.DistanceSquared(pathPoint.Position, validLocation.Cell.Center)) { continue; }
2903 
2904  var validComparedToOtherPathPoints = true;
2905  // Make sure this path point is closest to 'validLocation'
2906  foreach (var anotherPathPoint in PathPoints)
2907  {
2908  if (anotherPathPoint.Id == pathPoint.Id) { continue; }
2909  if (Vector2.DistanceSquared(anotherPathPoint.Position, validLocation.EdgeCenter) < distanceSquaredToEdge)
2910  {
2911  validComparedToOtherPathPoints = false;
2912  break;
2913  }
2914  }
2915 
2916  foreach (var anotherPathPoint in PathPoints.Where(p => p.Id != pathPoint.Id && p.ClusterLocations.Any()))
2917  {
2918  if (!validComparedToOtherPathPoints) { break; }
2919  foreach (var c in pathPoint.ClusterLocations)
2920  {
2921  if (IsInvalidComparedToExistingLocation())
2922  {
2923  validComparedToOtherPathPoints = false;
2924  break;
2925  }
2926 
2927  bool IsInvalidComparedToExistingLocation()
2928  {
2929  if (c.Equals(validLocation)) { return true; }
2930  // If there is a previously spawned cluster too near
2931  if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) { return true; }
2932  // If there is a line from a previous path point to one of its existing cluster locations
2933  // which intersects with the line from this path point to the new possible cluster location
2934  if (MathUtils.LineSegmentsIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; }
2935  return false;
2936  }
2937  }
2938  }
2939 
2940  if (!validComparedToOtherPathPoints) { continue; }
2941  generatedCluster = CreateResourceCluster(pathPoint, validLocation);
2942  selectedLocationIndex = i;
2943  break;
2944  }
2945 
2946  if (selectedLocationIndex >= 0)
2947  {
2948  allValidLocations.RemoveAt(selectedLocationIndex);
2949  }
2950 
2951  return generatedCluster;
2952 
2953  static bool IsNextToTunnelType(GraphEdge e, TunnelType t) =>
2954  (e.NextToMainPath && t == TunnelType.MainPath) ||
2955  (e.NextToSidePath && t == TunnelType.SidePath) ||
2956  (e.NextToCave && t == TunnelType.Cave);
2957  }
2958 
2959  bool GenerateAdditionalCluster(PathPoint pathPoint)
2960  {
2961  var validLocations = new List<ClusterLocation>();
2962  // First check only the edges of the same cell
2963  // which are connected to one of the existing edges with clusters
2964  foreach (var clusterLocation in pathPoint.ClusterLocations)
2965  {
2966  foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
2967  {
2968  if (HaveConnectingEdgePoints(anotherEdge, clusterLocation.Edge))
2969  {
2970  AddIfValid(clusterLocation.Cell, anotherEdge);
2971  }
2972  }
2973  }
2974 
2975  // Only check edges of adjacent cells if no valid edges were found
2976  // on any of the cells with existing clusters
2977  if (validLocations.None())
2978  {
2979  foreach (var clusterLocation in pathPoint.ClusterLocations)
2980  {
2981  foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
2982  {
2983  var adjacentCell = anotherEdge.AdjacentCell(clusterLocation.Cell);
2984  if (adjacentCell == null) { continue; }
2985  foreach (var adjacentCellEdge in adjacentCell.Edges.Where(e => e != anotherEdge))
2986  {
2987  if (HaveConnectingEdgePoints(adjacentCellEdge, clusterLocation.Edge))
2988  {
2989  AddIfValid(adjacentCell, adjacentCellEdge);
2990  }
2991  }
2992  }
2993  }
2994  }
2995 
2996  if (validLocations.Any())
2997  {
2998  var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient);
2999  if (CreateResourceCluster(pathPoint, location))
3000  {
3001  var i = allValidLocations.FindIndex(l => l.Equals(location));
3002  if (i >= 0)
3003  {
3004  allValidLocations.RemoveAt(i);
3005  }
3006  return true;
3007  }
3008  else
3009  {
3010  excludedPathPointIds.Add(pathPoint.Id);
3011  return false;
3012  }
3013  }
3014  else
3015  {
3016  excludedPathPointIds.Add(pathPoint.Id);
3017  return false;
3018  }
3019 
3020  static bool HaveConnectingEdgePoints(GraphEdge e1, GraphEdge e2) =>
3021  e1.Point1.NearlyEquals(e2.Point1) || e1.Point1.NearlyEquals(e2.Point2) ||
3022  e1.Point2.NearlyEquals(e2.Point1) || e1.Point2.NearlyEquals(e2.Point2);
3023 
3024  void AddIfValid(VoronoiCell c, GraphEdge e)
3025  {
3026  if (IsAlreadyInList(e)) { return; }
3027  if (allValidLocations.None(l => l.Equals(c, e))) { return; }
3028  if (pathPoint.ClusterLocations.Any(cl => cl.Edge == e)) { return; }
3029  validLocations.Add(new ClusterLocation(c, e));
3030  }
3031 
3032  bool IsAlreadyInList(GraphEdge edge) =>
3033  validLocations.Any(l => l.Edge == edge);
3034  }
3035 
3036  bool CreateResourceCluster(PathPoint pathPoint, ClusterLocation location)
3037  {
3038  if (location.Cell == null || location.Edge == null) { return false; }
3039 
3040  ItemPrefab selectedPrefab;
3041  if (pathPoint.ClusterLocations.Count == 0)
3042  {
3043  selectedPrefab = ToolBox.SelectWeightedRandom(
3044  levelResources.Select(it => it.itemPrefab).ToList(),
3045  levelResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
3046  Rand.RandSync.ServerAndClient);
3047  selectedPrefab.Tags.ForEach(t =>
3048  {
3049  if (exclusiveResourceTags.Contains(t))
3050  {
3051  pathPoint.ResourceTags.Add(t);
3052  }
3053  });
3054  }
3055  else
3056  {
3057  var filteredResources = pathPoint.ResourceTags.None() ? levelResources :
3058  levelResources.Where(it => it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t)));
3059  selectedPrefab = ToolBox.SelectWeightedRandom(
3060  filteredResources.Select(it => it.itemPrefab).ToList(),
3061  filteredResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
3062  Rand.RandSync.ServerAndClient);
3063  }
3064 
3065  if (selectedPrefab == null) { return false; }
3066 
3067  // Create resources for the cluster
3068  float commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonnessInfo.GetCommonness(pathPoint.TunnelType);
3069  float lerpAmount = pathPoint.TunnelType != TunnelType.Cave ?
3070  MathUtils.InverseLerp(commonnessRange.X, commonnessRange.Y, commonness) :
3071  MathUtils.InverseLerp(caveCommonnessRange.X, caveCommonnessRange.Y, commonness);
3072  var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount);
3073  var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength);
3074  maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge);
3075  if (itemCount + maxClusterSize > GenerationParams.ItemCount)
3076  {
3077  maxClusterSize += GenerationParams.ItemCount - (itemCount + maxClusterSize);
3078  }
3079 
3080  if (maxClusterSize < 1) { return false; }
3081 
3082  var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize);
3083  var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient);
3084 
3085  if (resourcesInCluster < 1) { return false; }
3086 
3087  PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLength: edgeLength);
3088  itemCount += resourcesInCluster;
3089  location.InitializeResources();
3090  location.Resources.AddRange(placedResources);
3091  pathPoint.ClusterLocations.Add(location);
3092  pathPoint.ResourceIds.Add(selectedPrefab.Identifier);
3093 
3094  return true;
3095  }
3096 
3097  int GetMaxResourcesOnEdge(ItemPrefab resourcePrefab, ClusterLocation location, out float edgeLength)
3098  {
3099  edgeLength = 0.0f;
3100  if (location.Cell == null || location.Edge == null) { return 0; }
3101  edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
3102  if (resourcePrefab == null) { return 0; }
3103  return (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X));
3104  }
3105  }
3106 
3108  public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable<Cave> targetCaves = null)
3109  {
3110  var allValidLocations = GetAllValidClusterLocations();
3111  var placedResources = new List<Item>();
3112 
3113  // if there are no valid locations, don't place anything
3114  if (allValidLocations.None()) { return placedResources; }
3115 
3116  // Make sure not to pick a spot that already has other level resources
3117  for (int i = allValidLocations.Count - 1; i >= 0; i--)
3118  {
3119  if (HasResources(allValidLocations[i]))
3120  {
3121  allValidLocations.RemoveAt(i);
3122  }
3123 
3124  bool HasResources(ClusterLocation clusterLocation)
3125  {
3126  foreach (var p in PathPoints)
3127  {
3128  foreach (var c in p.ClusterLocations)
3129  {
3130  if (!c.Equals(clusterLocation)) { continue; }
3131  foreach (var r in c.Resources)
3132  {
3133  if (r == null) { continue; }
3134  if (r.Removed) { continue; }
3135  if (!(r.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)) { return true; }
3136  }
3137  }
3138  }
3139  return false;
3140  }
3141  }
3142 
3143  if (PositionsOfInterest.None(p => p.PositionType == positionType))
3144  {
3145  DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources.");
3146  foreach (var validType in MineralMission.ValidPositionTypes)
3147  {
3148  if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType))
3149  {
3150  DebugConsole.AddWarning($"Placing in \"{validType}\" instead.");
3151  positionType = validType;
3152  break;
3153  }
3154  }
3155  }
3156 
3157  try
3158  {
3159  RemoveInvalidLocations(positionType switch
3160  {
3161  PositionType.MainPath => IsOnMainPath,
3162  PositionType.SidePath => IsOnSidePath,
3163  PositionType.Cave => IsInCave,
3164  PositionType.AbyssCave => IsInAbyssCave,
3165  _ => throw new NotImplementedException(),
3166  });
3167  }
3168  catch (NotImplementedException)
3169  {
3170  DebugConsole.ThrowError($"Unexpected PositionType (\"{positionType}\") for mineral mission resources: mineral spawning might not work as expected.");
3171  }
3172 
3173  if (targetCaves != null && targetCaves.Any())
3174  {
3175  // If resources are placed inside a cave, make sure all of them are placed inside the same one
3176  allValidLocations.RemoveAll(l => targetCaves.None(c => c.Area.Contains(l.EdgeCenter)));
3177  }
3178 
3179  var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient);
3180  Vector2 poiPos = poi.Position.ToVector2();
3181  allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter)
3182  .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter)));
3183  float maxResourceOverlap = 0.4f;
3184  var selectedLocation = allValidLocations.FirstOrDefault(l =>
3185  Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength &&
3186  !l.Edge.OutsideLevel &&
3187  ((l.Edge.Cell1?.IsDestructible ?? false) || (l.Edge.Cell2?.IsDestructible ?? false)) &&
3188  requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X)));
3189 
3190 
3191  if (selectedLocation.Edge == null)
3192  {
3193  //couldn't find a long enough edge, find the largest one
3194  float longestEdge = 0.0f;
3195  foreach (var validLocation in allValidLocations)
3196  {
3197  if (Vector2.Distance(validLocation.Edge.Point1, validLocation.Edge.Point2) is float edgeLength && edgeLength > longestEdge)
3198  {
3199  selectedLocation = validLocation;
3200  longestEdge = edgeLength;
3201  }
3202  }
3203  }
3204  if (selectedLocation.Edge == null)
3205  {
3206  throw new Exception("Failed to find a suitable level wall edge to place level resources on.");
3207  }
3208  PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
3209  Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
3210  return placedResources;
3211 
3212  static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath;
3213  static bool IsOnSidePath(ClusterLocation location) => location.Edge.NextToSidePath;
3214  static bool IsInCave(ClusterLocation location) => location.Edge.NextToCave;
3215  bool IsInAbyssCave(ClusterLocation location) => location.EdgeCenter.Y < AbyssStart;
3216  void RemoveInvalidLocations(Predicate<ClusterLocation> match)
3217  {
3218  allValidLocations.RemoveAll(l => !match(l));
3219  }
3220  }
3221 
3222  private List<ClusterLocation> GetAllValidClusterLocations()
3223  {
3224  var subBorders = new List<Rectangle>();
3225  Wrecks.ForEach(AddBordersToList);
3226  AddBordersToList(BeaconStation);
3227 
3228  var locations = new List<ClusterLocation>();
3229  foreach (var c in GetAllCells())
3230  {
3231  if (c.CellType != CellType.Solid) { continue; }
3232  foreach (var e in c.Edges)
3233  {
3234  if (IsValidEdge(e))
3235  {
3236  locations.Add(new ClusterLocation(c, e));
3237  }
3238  }
3239  }
3240  return locations;
3241 
3242  void AddBordersToList(Submarine s)
3243  {
3244  if (s == null) { return; }
3245  var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2());
3246  // range of piezo crystal discharge is 3500, pad the rect to ensure no such kind of hazards spawn near
3247  rect.Inflate(4000, 4000);
3248  subBorders.Add(rect);
3249  }
3250 
3251  bool IsValidEdge(GraphEdge e)
3252  {
3253  if (!e.IsSolid) { return false; }
3254  if (e.OutsideLevel) { return false; }
3255  var eCenter = e.Center;
3256  if (IsBlockedByWreckOrBeacon()) { return false; }
3257  if (IsBlockedByWall()) { return false; }
3258  return true;
3259 
3260  bool IsBlockedByWreckOrBeacon()
3261  {
3262  foreach (var r in subBorders)
3263  {
3264  if (r.Contains(e.Point1)) { return true; }
3265  if (r.Contains(e.Point2)) { return true; }
3266  if (r.Contains(eCenter)) { return true; }
3267 
3268  if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _))
3269  {
3270  return true;
3271  }
3272  }
3273  return false;
3274  }
3275 
3276  bool IsBlockedByWall()
3277  {
3278  foreach (var w in ExtraWalls)
3279  {
3280  foreach (var c in w.Cells)
3281  {
3282  if (c.IsPointInside(eCenter)) { return true; }
3283  if (c.IsPointInside(eCenter - 100 * e.GetNormal(c))) { return true; }
3284  if (c.Edges.Any(extraWallEdge => extraWallEdge == e)) { return true; }
3285  }
3286  }
3287  return false;
3288  }
3289  }
3290  }
3291 
3292  private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List<Item> placedResources,
3293  float? edgeLength = null, float maxResourceOverlap = 0.4f)
3294  {
3295  edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
3296  Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
3297  if (!MathUtils.IsValid(edgeDir))
3298  {
3299  edgeDir = Vector2.Zero;
3300  }
3301  var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
3302  minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
3303  var lerpAmounts = new float[resourceCount];
3304  lerpAmounts[0] = 0.0f;
3305  var lerpAmount = 0.0f;
3306  for (int i = 1; i < resourceCount; i++)
3307  {
3308  var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
3309  lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
3310  lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
3311  }
3312  var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
3313  placedResources = new List<Item>();
3314  for (int i = 0; i < resourceCount; i++)
3315  {
3316  Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
3317  var item = new Item(resourcePrefab, selectedPos, submarine: null);
3318  Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
3319  float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
3320  moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
3321  item.Move(edgeNormal * moveAmount);
3322  if (item.GetComponent<Holdable>() is Holdable h)
3323  {
3324  h.AttachToWall();
3325  item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
3326  }
3327  else if (item.body != null)
3328  {
3329  item.body.SetTransformIgnoreContacts(item.body.SimPosition, MathUtils.VectorToAngle(edgeNormal) - MathHelper.PiOver2);
3330  }
3331  placedResources.Add(item);
3332  }
3333  }
3334 
3335  public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f, Func<InterestingPosition, bool> filter = null)
3336  {
3337  if (!PositionsOfInterest.Any())
3338  {
3339  return new Vector2(Size.X / 2, Size.Y / 2);
3340  }
3341 
3342  Vector2 position = Vector2.Zero;
3343  int tries = 0;
3344  do
3345  {
3346  TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out InterestingPosition potentialPos, filter);
3347 
3348  Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
3349  Vector2 startPos = potentialPos.Position.ToVector2();
3350  if (!IsPositionInsideWall(startPos + offset))
3351  {
3352  startPos += offset;
3353  }
3354 
3355  Vector2 endPos = startPos - Vector2.UnitY * Size.Y;
3356 
3357  //try to find a level wall below the position unless the position is indoors
3358  if (!potentialPos.PositionType.IsEnclosedArea())
3359  {
3360  if (Submarine.PickBody(
3361  ConvertUnits.ToSimUnits(startPos),
3362  ConvertUnits.ToSimUnits(endPos),
3363  ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)),
3364  Physics.CollisionLevel | Physics.CollisionWall)?.UserData is VoronoiCell)
3365  {
3366  position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall;
3367  break;
3368  }
3369  }
3370 
3371  tries++;
3372 
3373  if (tries == 10)
3374  {
3375  position = startPos;
3376  }
3377 
3378  } while (tries < 10);
3379 
3380  return position;
3381  }
3382 
3383  public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint, Func<InterestingPosition, bool> filter = null)
3384  {
3385  position = default;
3386  bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, awayPoint, minDistFromPoint, filter);
3387  return success;
3388  }
3389 
3390  public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Func<InterestingPosition, bool> filter = null, bool suppressWarning = false)
3391  {
3392  position = default;
3393  bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning);
3394  return success;
3395  }
3396 
3397  public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint = 0f, Func<InterestingPosition, bool> filter = null, bool suppressWarning = false)
3398  {
3399  if (!PositionsOfInterest.Any())
3400  {
3401  position = default;
3402  return false;
3403  }
3404 
3405  List<InterestingPosition> suitablePositions = PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
3406  if (filter != null)
3407  {
3408  suitablePositions.RemoveAll(p => !filter(p));
3409  }
3410  if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) ||
3411  positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave))
3412  {
3413 #if DEBUG
3414  for (int i = 0; i < PositionsOfInterest.Count; i++)
3415  {
3416  var pos = PositionsOfInterest[i];
3417  if (!suitablePositions.Contains(pos)) { continue; }
3418  if (IsInvalid(pos))
3419  {
3420  pos.IsValid = false;
3421  PositionsOfInterest[i] = pos;
3422  }
3423  }
3424 #endif
3425  suitablePositions.RemoveAll(p => IsInvalid(p));
3426  bool IsInvalid(InterestingPosition p) => IsPositionInsideWall(p.Position.ToVector2());
3427  }
3428  if (!suitablePositions.Any())
3429  {
3430  if (!suppressWarning)
3431  {
3432  string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
3433  GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3434 #if DEBUG
3435  DebugConsole.ThrowError(errorMsg);
3436 #endif
3437  }
3438  position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))];
3439  return false;
3440  }
3441 
3442  List<InterestingPosition> farEnoughPositions = new List<InterestingPosition>(suitablePositions);
3443  if (minDistFromSubs > 0.0f)
3444  {
3445  foreach (Submarine sub in Submarine.Loaded)
3446  {
3447  if (sub.Info.Type != SubmarineType.Player) { continue; }
3448  farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs);
3449  }
3450  }
3451  if (minDistFromPoint > 0.0f)
3452  {
3453  farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), awayPoint) < minDistFromPoint * minDistFromPoint);
3454  }
3455 
3456  if (!farEnoughPositions.Any())
3457  {
3458  string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
3459  GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3460 #if DEBUG
3461  DebugConsole.ThrowError(errorMsg);
3462 #endif
3463  float maxDist = 0.0f;
3464  position = suitablePositions.First();
3465  foreach (InterestingPosition pos in suitablePositions)
3466  {
3467  float dist = Submarine.Loaded.Sum(s =>
3468  Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f);
3469  if (dist > maxDist)
3470  {
3471  position = pos;
3472  maxDist = dist;
3473  }
3474  }
3475 
3476  return false;
3477  }
3478 
3479  position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)];
3480  return true;
3481  }
3482 
3483  public bool IsPositionInsideWall(Vector2 worldPosition)
3484  {
3485  var closestCell = GetClosestCell(worldPosition);
3486  return closestCell != null && closestCell.IsPointInside(worldPosition);
3487  }
3488 
3489  public void Update(float deltaTime, Camera cam)
3490  {
3491  LevelObjectManager.Update(deltaTime);
3492 
3493  foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); }
3494  for (int i = UnsyncedExtraWalls.Count - 1; i >= 0; i--)
3495  {
3496  UnsyncedExtraWalls[i].Update(deltaTime);
3497  }
3498 
3499 #if SERVER
3500  if (GameMain.NetworkMember is { IsServer: true })
3501  {
3502  foreach (LevelWall wall in ExtraWalls)
3503  {
3504  if (wall is DestructibleLevelWall { NetworkUpdatePending: true } destructibleWall)
3505  {
3506  GameMain.NetworkMember.CreateEntityEvent(this, new SingleLevelWallEventData(destructibleWall));
3507  destructibleWall.NetworkUpdatePending = false;
3508  }
3509  }
3510  networkUpdateTimer += deltaTime;
3511  if (networkUpdateTimer > NetworkUpdateInterval)
3512  {
3513  if (ExtraWalls.Any(w => w.Body.BodyType != BodyType.Static))
3514  {
3515  GameMain.NetworkMember.CreateEntityEvent(this, new GlobalLevelWallEventData());
3516  }
3517  networkUpdateTimer = 0.0f;
3518  }
3519  }
3520 #endif
3521 
3522 #if CLIENT
3523  backgroundCreatureManager.Update(deltaTime, cam);
3524  WaterRenderer.Instance?.ScrollWater(Vector2.UnitY, (float)deltaTime);
3525  renderer.Update(deltaTime, cam);
3526 #endif
3527  }
3528 
3529  public Vector2 GetBottomPosition(float xPosition)
3530  {
3531  float interval = Size.X / (bottomPositions.Count - 1);
3532 
3533  int index = (int)Math.Floor(xPosition / interval);
3534  if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
3535 
3536  float t = (xPosition - bottomPositions[index].X) / interval;
3537  t = MathHelper.Clamp(t, 0.0f, 1.0f);
3538  float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
3539 
3540  return new Vector2(xPosition, yPos);
3541  }
3542 
3543  public List<VoronoiCell> GetAllCells()
3544  {
3545  List<VoronoiCell> cells = new List<VoronoiCell>();
3546  for (int x = 0; x < cellGrid.GetLength(0); x++)
3547  {
3548  for (int y = 0; y < cellGrid.GetLength(1); y++)
3549  {
3550  cells.AddRange(cellGrid[x, y]);
3551  }
3552  }
3553  return cells;
3554  }
3555 
3556  private readonly List<VoronoiCell> tempCells = new List<VoronoiCell>();
3557  public List<VoronoiCell> GetCells(Vector2 worldPos, int searchDepth = 2)
3558  {
3559  tempCells.Clear();
3560  int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize);
3561  int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize);
3562 
3563  int startX = Math.Max(gridPosX - searchDepth, 0);
3564  int endX = Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1);
3565 
3566  int startY = Math.Max(gridPosY - searchDepth, 0);
3567  int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1);
3568 
3569  for (int y = startY; y <= endY; y++)
3570  {
3571  for (int x = startX; x <= endX; x++)
3572  {
3573  tempCells.AddRange(cellGrid[x, y]);
3574  }
3575  }
3576 
3577  foreach (LevelWall wall in ExtraWalls)
3578  {
3579  if (wall == SeaFloor)
3580  {
3581  if (SeaFloorTopPos < worldPos.Y - searchDepth * GridCellSize) { continue; }
3582  }
3583  else
3584  {
3585  if (wall is DestructibleLevelWall destructibleWall && destructibleWall.Destroyed) { continue; }
3586  bool closeEnough = false;
3587  foreach (VoronoiCell cell in wall.Cells)
3588  {
3589  if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2))
3590  {
3591  closeEnough = true;
3592  break;
3593  }
3594  }
3595  if (!closeEnough) { continue; }
3596  }
3597  foreach (VoronoiCell cell in wall.Cells)
3598  {
3599  tempCells.Add(cell);
3600  }
3601  }
3602 
3603  foreach (var abyssIsland in AbyssIslands)
3604  {
3605  if (abyssIsland.Area.X > worldPos.X + searchDepth * GridCellSize) { continue; }
3606  if (abyssIsland.Area.Right < worldPos.X - searchDepth * GridCellSize) { continue; }
3607  if (abyssIsland.Area.Y > worldPos.Y + searchDepth * GridCellSize) { continue; }
3608  if (abyssIsland.Area.Bottom < worldPos.Y - searchDepth * GridCellSize) { continue; }
3609 
3610  tempCells.AddRange(abyssIsland.Cells);
3611  }
3612 
3613  return tempCells;
3614  }
3615 
3616  public VoronoiCell GetClosestCell(Vector2 worldPos)
3617  {
3618  double closestDist = double.MaxValue;
3619  VoronoiCell closestCell = null;
3620  int searchDepth = 2;
3621  while (searchDepth < 5)
3622  {
3623  foreach (var cell in GetCells(worldPos, searchDepth))
3624  {
3625  double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, worldPos.X, worldPos.Y);
3626  if (dist < closestDist)
3627  {
3628  closestDist = dist;
3629  closestCell = cell;
3630  }
3631  }
3632  if (closestCell != null) { break; }
3633  searchDepth++;
3634  }
3635  return closestCell;
3636  }
3637 
3638  private List<VoronoiCell> CreatePathToClosestTunnel(Point pos)
3639  {
3640  VoronoiCell closestPathCell = null;
3641  double closestDist = 0.0f;
3642  foreach (Tunnel tunnel in Tunnels)
3643  {
3644  if (tunnel.Type == TunnelType.Cave) { continue; }
3645  foreach (VoronoiCell cell in tunnel.Cells)
3646  {
3647  double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, pos.X, pos.Y);
3648  if (closestPathCell == null || dist < closestDist)
3649  {
3650  closestPathCell = cell;
3651  closestDist = dist;
3652  }
3653  }
3654  }
3655 
3656  //cast a ray from the closest path cell towards the position and remove the cells it hits
3657  List<VoronoiCell> validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed);
3658  List<VoronoiCell> pathCells = new List<VoronoiCell>() { closestPathCell };
3659  foreach (VoronoiCell cell in validCells)
3660  {
3661  foreach (GraphEdge e in cell.Edges)
3662  {
3663  if (!MathUtils.LineSegmentsIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; }
3664 
3665  cell.CellType = CellType.Removed;
3666  for (int x = 0; x < cellGrid.GetLength(0); x++)
3667  {
3668  for (int y = 0; y < cellGrid.GetLength(1); y++)
3669  {
3670  cellGrid[x, y].Remove(cell);
3671  }
3672  }
3673  pathCells.Add(cell);
3674  cells.Remove(cell);
3675 
3676  //go through the edges of this cell and find the ones that are next to a removed cell
3677  foreach (var otherEdge in cell.Edges)
3678  {
3679  var otherAdjacent = otherEdge.AdjacentCell(cell);
3680  if (otherAdjacent == null || otherAdjacent.CellType == CellType.Solid) { continue; }
3681 
3682  //if the edge is very short, remove adjacent cells to prevent making the passage too narrow
3683  if (Vector2.DistanceSquared(otherEdge.Point1, otherEdge.Point2) < 500.0f * 500.0f)
3684  {
3685  foreach (GraphEdge e2 in cell.Edges)
3686  {
3687  if (e2 == otherEdge || e2 == otherEdge) { continue; }
3688  if (!MathUtils.NearlyEqual(otherEdge.Point1, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point2))
3689  {
3690  continue;
3691  }
3692  var adjacentCell = e2.AdjacentCell(cell);
3693  if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; }
3694  adjacentCell.CellType = CellType.Removed;
3695  for (int x = 0; x < cellGrid.GetLength(0); x++)
3696  {
3697  for (int y = 0; y < cellGrid.GetLength(1); y++)
3698  {
3699  cellGrid[x, y].Remove(adjacentCell);
3700  }
3701  }
3702  cells.Remove(adjacentCell);
3703  }
3704  }
3705  }
3706  break;
3707  }
3708  }
3709 
3710  pathCells.Sort((c1, c2) => { return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); });
3711  return pathCells;
3712  }
3713 
3714  public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist);
3715  public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist);
3716 
3717  public bool IsCloseToStart(Point position, float minDist)
3718  {
3719  return MathUtils.LineSegmentToPointDistanceSquared(startPosition, startExitPosition, position) < minDist * minDist;
3720  }
3721 
3722  public bool IsCloseToEnd(Point position, float minDist)
3723  {
3724  return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist;
3725  }
3726 
3727  private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type, bool forceThalamus = false)
3728  {
3729  var tempSW = new Stopwatch();
3730 
3731  // Min distance between a sub and the start/end/other sub.
3732  const float minDistance = Sonar.DefaultSonarRange;
3733  var waypoints = WayPoint.WayPointList.Where(wp =>
3734  wp.Submarine == null &&
3735  wp.SpawnType == SpawnType.Path &&
3736  wp.WorldPosition.X < EndExitPosition.X &&
3737  !IsCloseToStart(wp.WorldPosition, minDistance) &&
3738  !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
3739 
3740  var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
3741  Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
3742  SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
3743  {
3744  Type = type
3745  };
3746 
3747  //place downwards by default
3748  var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
3749 
3750  // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
3751  int padding = 1500;
3752  Rectangle paddedBorders = new Rectangle(
3753  subBorders.X - padding,
3754  subBorders.Y + padding,
3755  subBorders.Width + padding * 2,
3756  subBorders.Height + padding * 2);
3757 
3758  var positions = new List<Vector2>();
3759  var rects = new List<Rectangle>();
3760  int maxAttempts = 50;
3761  int attemptsLeft = maxAttempts;
3762  bool success = false;
3763  Vector2 spawnPoint = Vector2.Zero;
3764  var allCells = Loaded.GetAllCells();
3765  while (attemptsLeft > 0)
3766  {
3767  if (attemptsLeft < maxAttempts)
3768  {
3769  Debug.WriteLine($"Failed to position the sub {subName}. Trying again.");
3770  }
3771  attemptsLeft--;
3772  if (TryGetSpawnPoint(out spawnPoint))
3773  {
3774  success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
3775  if (success)
3776  {
3777  break;
3778  }
3779  else
3780  {
3781  positions.Clear();
3782  }
3783  }
3784  else
3785  {
3786  DebugConsole.NewMessage($"Failed to find any spawn point for the sub: {subName} (No valid waypoints left).", Color.Red);
3787  break;
3788  }
3789  }
3790  tempSW.Stop();
3791  if (success)
3792  {
3793  Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
3794  tempSW.Restart();
3795  Submarine sub = new Submarine(info);
3796  if (type == SubmarineType.Wreck)
3797  {
3798  sub.MakeWreck();
3799  Wrecks.Add(sub);
3800  PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
3801  foreach (Hull hull in sub.GetHulls(false))
3802  {
3803  if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
3804  {
3805  hull.WaterVolume =
3806  Math.Max(hull.WaterVolume, hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient));
3807  }
3808  }
3809  // Only spawn thalamus when the wreck has some thalamus items defined.
3810  if ((forceThalamus || Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability) && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus")))
3811  {
3812  if (!sub.CreateWreckAI())
3813  {
3814  DebugConsole.NewMessage($"Failed to create wreck AI inside {subName}.", Color.Red);
3815  sub.DisableWreckAI();
3816  }
3817  }
3818  else
3819  {
3820  sub.DisableWreckAI();
3821  }
3822  }
3823  else if (type == SubmarineType.BeaconStation)
3824  {
3825  PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
3826 
3827  sub.ShowSonarMarker = false;
3828  sub.DockedTo.ForEach(s => s.ShowSonarMarker = false);
3829  sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
3830  sub.TeamID = CharacterTeamType.None;
3831  }
3832  tempSW.Stop();
3833  Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
3834  sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false);
3835  wreckPositions.Add(sub, positions);
3836  blockedRects.Add(sub, rects);
3837  return sub;
3838  }
3839  else
3840  {
3841  DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).", Color.Red);
3842  return null;
3843  }
3844 
3845  bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint)
3846  {
3847  positions.Add(spawnPoint);
3848  bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
3849  positions.Add(spawnPoint);
3850 
3851  bool leftSideBlocked = IsSideBlocked(subBorders, false);
3852  bool rightSideBlocked = IsSideBlocked(subBorders, true);
3853  int step = 5;
3854  if (rightSideBlocked && !leftSideBlocked)
3855  {
3856  bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
3857  }
3858  else if (leftSideBlocked && !rightSideBlocked)
3859  {
3860  bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
3861  }
3862  else if (!bottomFound)
3863  {
3864  if (!leftSideBlocked)
3865  {
3866  bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
3867  }
3868  else if (!rightSideBlocked)
3869  {
3870  bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
3871  }
3872  else
3873  {
3874  Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
3875  return false;
3876  }
3877  }
3878  positions.Add(spawnPoint);
3879  //shrink the bounds a bit to allow the sub to go slightly inside the wall
3880  //(just enough that it doesn't look like it's floating)
3881  int shrinkAmount = step + 50;
3882  Rectangle shrunkenBorders = new Rectangle(
3883  subBorders.X + shrinkAmount,
3884  subBorders.Y - shrinkAmount,
3885  subBorders.Width - shrinkAmount * 2,
3886  subBorders.Height - shrinkAmount * 2);
3887  bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders);
3888  if (isBlocked)
3889  {
3890  rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size));
3891  Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls.");
3892  }
3893  else if (!bottomFound)
3894  {
3895  Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
3896  }
3897  else
3898  {
3899  var sp = spawnPoint;
3900  if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
3901  {
3902  Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s).");
3903  return false;
3904  }
3905  }
3906  return !isBlocked && bottomFound;
3907 
3908  bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount)
3909  {
3910  float maxMovement = 5000;
3911  float totalAmount = 0;
3912  bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
3913  //move until the side is no longer blocked
3914  while (IsSideBlocked(subBorders, front: amount < 0))
3915  {
3916  foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
3917  totalAmount += amount;
3918  spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y);
3919  if (Math.Abs(totalAmount) > maxMovement)
3920  {
3921  Debug.WriteLine($"Moving the sub {subName} failed.");
3922  break;
3923  }
3924  }
3925  return foundBottom;
3926  }
3927  }
3928 
3929  bool TryGetSpawnPoint(out Vector2 spawnPoint)
3930  {
3931  spawnPoint = Vector2.Zero;
3932  while (waypoints.Any())
3933  {
3934  var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
3935  waypoints.Remove(wp);
3936  if (!IsBlocked(wp.WorldPosition, paddedBorders))
3937  {
3938  spawnPoint = wp.WorldPosition;
3939  return true;
3940  }
3941  }
3942  return false;
3943  }
3944 
3945  bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint)
3946  {
3947  // Shoot five rays and pick the highest hit point.
3948  int rayCount = 5;
3949  var hitPositions = new Vector2[rayCount];
3950  bool hit = false;
3951  for (int i = 0; i < rayCount; i++)
3952  {
3953  //cast rays starting from the left side of the sub, offset by 20% to 80% of the sub's width
3954  //(ignoring the very back and front of the sub, it's fine if they overlap a bit)
3955  float xOffset =
3956  subBorders.Width * MathHelper.Lerp(0.2f, 0.8f, i / (float)(rayCount - 1));
3957  Vector2 rayStart = new Vector2(
3958  spawnPoint.X + subBorders.Location.X + xOffset,
3959  spawnPoint.Y);
3960  var simPos = ConvertUnits.ToSimUnits(rayStart);
3961  var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1),
3962  customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)),
3963  collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
3964  if (body != null)
3965  {
3966  hitPositions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition);
3967  hit = true;
3968  }
3969  }
3970  int dir = placement == PlacementType.Bottom ? -1 : 1;
3971  float highestPoint = placement == PlacementType.Bottom ? hitPositions.Max(p => p.Y) : hitPositions.Min(p => p.Y);
3972  float halfHeight = subBorders.Height / 2;
3973  float centerOffset = subBorders.Y - halfHeight;
3974  spawnPoint = new Vector2(spawnPoint.X, highestPoint + halfHeight * -dir - centerOffset);
3975  return hit;
3976  }
3977 
3978  bool IsSideBlocked(Rectangle subBorders, bool front)
3979  {
3980  Point centerOffset = new Point(subBorders.Center.X, subBorders.Y - subBorders.Height / 2);
3981 
3982  // Shoot three rays and check whether any of them hits.
3983  int rayCount = 3;
3984  int dir = front ? 1 : -1;
3985  Vector2 halfSize = subBorders.Size.ToVector2() / 2;
3986  Vector2 quarterSize = halfSize / 2;
3987  for (int i = 0; i < rayCount; i++)
3988  {
3989  Vector2 rayStart, to;
3990  switch (i)
3991  {
3992  case 1:
3993  case 2:
3994  float yOffset = quarterSize.Y * (i == 1 ? 1 : -1);
3995  //from a position half-way towards the edge, to the edge.
3996  //we start half-way towards the edge instead of the center, because we want to allow things to poke partially inside the sub
3997  rayStart = new Vector2(spawnPoint.X + quarterSize.X * dir, spawnPoint.Y + yOffset);
3998  to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
3999  break;
4000  case 0:
4001  default:
4002  //center to center-right
4003  rayStart = spawnPoint;
4004  to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
4005  break;
4006  }
4007 
4008  rayStart += centerOffset.ToVector2();
4009  to += centerOffset.ToVector2();
4010 
4011  Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
4012  if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
4013  customPredicate: f => f.Body?.UserData is VoronoiCell cell,
4014  collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
4015  allowInsideFixture: true) != null)
4016  {
4017  return true;
4018  }
4019  }
4020  return false;
4021  }
4022 
4023  bool IsBlocked(Vector2 pos, Rectangle submarineBounds)
4024  {
4025  Rectangle bounds = new Rectangle(
4026  (int)pos.X + submarineBounds.X, (int)pos.Y + submarineBounds.Y,
4027  submarineBounds.Width, submarineBounds.Height);
4028  if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds)))
4029  {
4030  return true;
4031  }
4032  if (Caves.Any(c =>
4033  ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) ||
4034  ToolBox.GetWorldBounds(c.StartPos, new Point(1500)).IntersectsWorld(bounds)))
4035  {
4036  return true;
4037  }
4038  return cells.Any(c =>
4039  c.Body != null &&
4040  c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
4041  }
4042  }
4043 
4044  // For debugging
4045  private readonly Dictionary<Submarine, List<Vector2>> wreckPositions = new Dictionary<Submarine, List<Vector2>>();
4046  private readonly Dictionary<Submarine, List<Rectangle>> blockedRects = new Dictionary<Submarine, List<Rectangle>>();
4047 
4048  private readonly record struct PlaceableWreck(WreckFile WreckFile, WreckInfo WreckInfo)
4049  {
4050  public static Option<PlaceableWreck> TryCreate(WreckFile wreckFile)
4051  {
4052  var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == wreckFile.Path.Value);
4053  if (matchingSub?.WreckInfo is null)
4054  {
4055  DebugConsole.ThrowError($"No matching submarine info found for the wreck file {wreckFile.Path.Value}");
4056  return Option.None;
4057  }
4058 
4059  return Option.Some(new PlaceableWreck(wreckFile, matchingSub.WreckInfo));
4060  }
4061  }
4062 
4063  private void CreateWrecks()
4064  {
4065  var totalSW = new Stopwatch();
4066  totalSW.Start();
4067 
4068  var placeableWrecks = ContentPackageManager.EnabledPackages.All
4069  .SelectMany(p => p.GetFiles<WreckFile>())
4070  .OrderBy(f => f.UintIdentifier)
4071  .Select(PlaceableWreck.TryCreate)
4072  .Where(w => w.IsSome())
4073  .Select(o => o.TryUnwrap(out var w) ? w : throw new InvalidOperationException())
4074  .ToList();
4075 
4076  for (int i = placeableWrecks.Count - 1; i >= 0; i--)
4077  {
4078  var wreckInfo = placeableWrecks[i].WreckInfo;
4079  if (!IsAllowedDifficulty(wreckInfo.MinLevelDifficulty, wreckInfo.MaxLevelDifficulty))
4080  {
4081  placeableWrecks.RemoveAt(i);
4082  }
4083  }
4084  if (placeableWrecks.None())
4085  {
4086  DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!");
4087  Wrecks = new List<Submarine>();
4088  return;
4089  }
4090  placeableWrecks.Shuffle(Rand.RandSync.ServerAndClient);
4091 
4092  int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, placeableWrecks.Count);
4093  int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, placeableWrecks.Count);
4094  int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient);
4095  bool requireThalamus = false;
4096 
4097  if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false)
4098  {
4099  wreckCount = Math.Max(wreckCount, 1);
4100  }
4101 
4102  if (GameMain.GameSession?.GameMode?.Missions.Any(static m => m.Prefab.RequireThalamusWreck) ?? false)
4103  {
4104  requireThalamus = true;
4105  }
4106 
4107  if (LevelData.ForceWreck != null)
4108  {
4109  //force the desired wreck to be chosen first
4110  var matchingFile = placeableWrecks.FirstOrDefault(w => w.WreckFile.Path == LevelData.ForceWreck.FilePath);
4111  if (matchingFile.WreckFile != null)
4112  {
4113  placeableWrecks.Remove(matchingFile);
4114  placeableWrecks.Insert(0, matchingFile);
4115  }
4116  wreckCount = Math.Max(wreckCount, 1);
4117  }
4118 
4119  if (requireThalamus)
4120  {
4121  var thalamusWrecks = placeableWrecks
4122  .Where(static w => w.WreckInfo.WreckContainsThalamus == WreckInfo.HasThalamus.Yes)
4123  .ToList();
4124 
4125  if (thalamusWrecks.Any())
4126  {
4127  thalamusWrecks.Shuffle(Rand.RandSync.ServerAndClient);
4128 
4129  foreach (var wreck in thalamusWrecks)
4130  {
4131  placeableWrecks.Remove(wreck);
4132  placeableWrecks.Insert(0, wreck);
4133  }
4134  }
4135  }
4136 
4137  Wrecks = new List<Submarine>(wreckCount);
4138  for (int i = 0; i < wreckCount; i++)
4139  {
4140  //how many times we'll try placing another sub before giving up
4141  const int MaxSubsToTry = 2;
4142  int attempts = 0;
4143  while (placeableWrecks.Any() && attempts < MaxSubsToTry)
4144  {
4145  var placeableWreck = placeableWrecks.First();
4146  var wreckFile = placeableWreck.WreckFile;
4147  placeableWrecks.RemoveAt(0);
4148  if (wreckFile == null) { continue; }
4149  string wreckName = System.IO.Path.GetFileNameWithoutExtension(wreckFile.Path.Value);
4150  if (SpawnSubOnPath(wreckName, wreckFile, SubmarineType.Wreck, forceThalamus: requireThalamus) is { } wreck)
4151  {
4152  if (wreck.WreckAI is not null)
4153  {
4154  requireThalamus = false;
4155  }
4156  //placed successfully
4157  break;
4158  }
4159  attempts++;
4160  }
4161 
4162  }
4163  totalSW.Stop();
4164  Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)");
4165  }
4166 
4167  private bool HasStartOutpost()
4168  {
4169  if (preSelectedStartOutpost != null) { return true; }
4170  if (LevelData.Type != LevelData.LevelType.Outpost)
4171  {
4172  //only create a starting outpost in campaign and tutorial modes
4173 #if CLIENT
4174  if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible())
4175  {
4176  return false;
4177  }
4178 #else
4179  if (!IsModeStartOutpostCompatible())
4180  {
4181  return false;
4182  }
4183 #endif
4184  }
4185  if (StartLocation != null && !StartLocation.Type.HasOutpost)
4186  {
4187  return false;
4188  }
4189  return true;
4190  }
4191 
4192  private bool HasEndOutpost()
4193  {
4194  if (preSelectedEndOutpost != null) { return true; }
4195  //don't create an end outpost for locations
4196  if (LevelData.Type == LevelData.LevelType.Outpost) { return false; }
4197  if (EndLocation != null && !EndLocation.Type.HasOutpost) { return false; }
4198  return true;
4199  }
4200 
4201  private void CreateOutposts()
4202  {
4203  var outpostFiles = ContentPackageManager.EnabledPackages.All
4204  .SelectMany(p => p.GetFiles<OutpostFile>())
4205  .OrderBy(f => f.UintIdentifier).ToList();
4206  if (!outpostFiles.Any() && !OutpostGenerationParams.OutpostParams.Any() && LevelData.ForceOutpostGenerationParams == null)
4207  {
4208  DebugConsole.ThrowError("No outpost files found in the selected content packages");
4209  return;
4210  }
4211 
4212  for (int i = 0; i < 2; i++)
4213  {
4214  if (GameMain.GameSession?.GameMode is PvPMode) { continue; }
4215 
4216  bool isStart = (i == 0) == !Mirrored;
4217  if (isStart)
4218  {
4219  if (!HasStartOutpost()) { continue; }
4220  }
4221  else
4222  {
4223  if (!HasEndOutpost()) { continue; }
4224  }
4225 
4226  SubmarineInfo outpostInfo;
4227  Submarine outpost = null;
4228 
4229  SubmarineInfo preSelectedOutpost = isStart ? preSelectedStartOutpost : preSelectedEndOutpost;
4230  if (preSelectedOutpost == null)
4231  {
4233  {
4234  Location location = isStart ? StartLocation : EndLocation;
4235  OutpostGenerationParams outpostGenerationParams = null;
4237  {
4238  outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
4239  }
4240  else
4241  {
4242  outpostGenerationParams =
4244  LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
4245  }
4246 
4247  LocationType locationType = location?.Type;
4248  if (locationType == null)
4249  {
4250  locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
4251  if (outpostGenerationParams.AllowedLocationTypes.Any())
4252  {
4253  locationType = LocationType.Prefabs.GetRandom(lt =>
4254  outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
4255  allowedType == "any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient);
4256  }
4257  }
4258 
4259  if (location != null)
4260  {
4261  DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
4262  outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
4263  }
4264  else
4265  {
4266  DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})");
4267  outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
4268  }
4269 
4270  foreach (string categoryToHide in locationType.HideEntitySubcategories)
4271  {
4272  foreach (MapEntity entityToHide in MapEntity.MapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false)))
4273  {
4274  entityToHide.IsLayerHidden = true;
4275  }
4276  }
4277  }
4278  else
4279  {
4280  DebugConsole.NewMessage($"Loading a pre-built outpost for the {(isStart ? "start" : "end")} of the level...");
4281  //backwards compatibility: if there are no generation params available, try to load an outpost file saved as a sub
4282  ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient);
4283  outpostInfo = new SubmarineInfo(outpostFile.Path.Value)
4284  {
4285  Type = SubmarineType.Outpost
4286  };
4287  outpost = new Submarine(outpostInfo);
4288  }
4289  }
4290  else
4291  {
4292  DebugConsole.NewMessage($"Loading a pre-selected outpost for the {(isStart ? "start" : "end")} of the level...");
4293  outpostInfo = preSelectedOutpost;
4294  outpostInfo.Type = SubmarineType.Outpost;
4295  outpost = new Submarine(outpostInfo);
4296  }
4297 
4298  Point? minSize = null;
4299  DockingPort subPort = null;
4300  float closestDistance = float.MaxValue;
4301  if (Submarine.MainSub != null)
4302  {
4303  Point subSize = Submarine.MainSub.GetDockedBorders().Size;
4304  Point outpostSize = outpost.GetDockedBorders().Size;
4305  minSize = new Point(Math.Max(subSize.X, outpostSize.X), subSize.Y + outpostSize.Y);
4306 
4307  foreach (DockingPort port in DockingPort.List)
4308  {
4309  if (port.IsHorizontal || port.Docked) { continue; }
4310  if (port.Item.Submarine != Submarine.MainSub) { continue; }
4311  //the submarine port has to be at the top of the sub
4312  if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; }
4313  float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X);
4314  if (dist < closestDistance || subPort.MainDockingPort)
4315  {
4316  subPort = port;
4317  closestDistance = dist;
4318  }
4319  }
4320  }
4321 
4322  Vector2 spawnPos;
4323  if (GenerationParams.ForceOutpostPosition != Vector2.Zero)
4324  {
4326  }
4327  else
4328  {
4329  DockingPort outpostPort = null;
4330  closestDistance = float.MaxValue;
4331  foreach (DockingPort port in DockingPort.List)
4332  {
4333  if (port.IsHorizontal || port.Docked) { continue; }
4334  if (port.Item.Submarine != outpost) { continue; }
4335  //the outpost port has to be at the bottom of the outpost
4336  if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; }
4337  float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X);
4338  if (dist < closestDistance)
4339  {
4340  outpostPort = port;
4341  closestDistance = dist;
4342  }
4343  }
4344 
4345  float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X;
4346  //don't try to compensate if the port is very far from the sub's center of mass
4347  if (Math.Abs(subDockingPortOffset) > 5000.0f)
4348  {
4349  subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f);
4350  string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
4351  DebugConsole.NewMessage(warningMsg, Color.Orange);
4352  GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
4353  }
4354 
4355  float? outpostDockingPortOffset = null;
4356  if (outpostPort != null)
4357  {
4358  outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X;
4359  //don't try to compensate if the port is very far from the outpost's center of mass
4360  if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f)
4361  {
4362  outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f);
4363  string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible.";
4364  DebugConsole.NewMessage(warningMsg, Color.Orange);
4365  GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
4366  }
4367  }
4368 
4369  Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition;
4370  //if we're placing the outpost at the end of the level, close to the bottom-right,
4371  //and there's a hole leading out the right side of the level, move the spawn position towards that hole.
4372  //Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical
4373  //shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the
4374  //ceiling is at the top of that shaft.
4375  if (i == 1 && GenerationParams.CreateHoleNextToEnd &&
4376  preferredSpawnPos.X > Size.X * 0.75f &&
4377  preferredSpawnPos.Y < Size.Y * 0.25f)
4378  {
4379  preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2;
4380  }
4381 
4382  spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
4383  if (Type == LevelData.LevelType.Outpost)
4384  {
4385  spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
4386  }
4387  }
4388 
4389  outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false);
4390  SetLinkedSubCrushDepth(outpost);
4391 
4392  foreach (WayPoint wp in WayPoint.WayPointList)
4393  {
4394  if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path)
4395  {
4396  PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost));
4397  }
4398  }
4399 
4400  if ((i == 0) == !Mirrored)
4401  {
4402  StartOutpost = outpost;
4403  if (StartLocation != null)
4404  {
4406  outpost.Info.Name = StartLocation.DisplayName.Value;
4407  }
4408  }
4409  else
4410  {
4411  EndOutpost = outpost;
4412  if (EndLocation != null)
4413  {
4414  outpost.TeamID = EndLocation.Type.OutpostTeam;
4415  outpost.Info.Name = EndLocation.DisplayName.Value;
4416  }
4417  }
4418  }
4419  }
4420 
4421  private void CreateBeaconStation()
4422  {
4423  if (!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
4424  var beaconStationFiles = ContentPackageManager.EnabledPackages.All
4425  .SelectMany(p => p.GetFiles<BeaconStationFile>())
4426  .OrderBy(f => f.UintIdentifier).ToList();
4427  if (beaconStationFiles.None())
4428  {
4429  DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!");
4430  return;
4431  }
4432 
4433  var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
4434  ContentFile contentFile = null;
4435  if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation))
4436  {
4437  var contentPath = ContentPath.FromRaw(GenerationParams.ContentPackage, GenerationParams.ForceBeaconStation);
4438  contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == contentPath);
4439  if (contentFile == null)
4440  {
4441  DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead...");
4442  }
4443  }
4444  else if (LevelData.ForceBeaconStation != null)
4445  {
4446  contentFile = beaconStationFiles.FirstOrDefault(b => b.Path == LevelData.ForceBeaconStation.FilePath);
4447  }
4448 
4449  if (contentFile == null)
4450  {
4451  for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
4452  {
4453  var beaconStationFile = beaconStationFiles[i];
4454  var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
4455  Debug.Assert(matchingInfo != null);
4456  if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
4457  {
4458  if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
4459  {
4460  beaconStationFiles.RemoveAt(i);
4461  }
4462  }
4463  }
4464  if (beaconStationFiles.None())
4465  {
4466  DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
4467  return;
4468  }
4469  contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
4470  }
4471 
4472  string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
4473  BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
4474  if (BeaconStation == null)
4475  {
4476  LevelData.HasBeaconStation = false;
4477  return;
4478  }
4479 
4480  Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
4481  if (sonarItem == null)
4482  {
4483  DebugConsole.ThrowError($"No sonar found in the beacon station \"{beaconStationName}\"!");
4484  return;
4485  }
4486  beaconSonar = sonarItem.GetComponent<Sonar>();
4487  }
4488 
4489  public void PrepareBeaconStation()
4490  {
4491  if (!LevelData.HasBeaconStation) { return; }
4492  if (GameMain.NetworkMember?.IsClient ?? false) { return; }
4493 
4494  if (BeaconStation == null)
4495  {
4496  throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level).");
4497  }
4498 
4499  List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
4500 
4501  Item reactorItem = beaconItems.Find(it => it.GetComponent<Reactor>() != null);
4502  Reactor reactorComponent = null;
4503  ItemContainer reactorContainer = null;
4504  if (reactorItem != null)
4505  {
4506  reactorComponent = reactorItem.GetComponent<Reactor>();
4507  reactorComponent.FuelConsumptionRate = 0.0f;
4508  reactorContainer = reactorItem.GetComponent<ItemContainer>();
4509  Repairable repairable = reactorItem.GetComponent<Repairable>();
4510  if (repairable != null)
4511  {
4512  repairable.DeteriorationSpeed = 0.0f;
4513  }
4514  }
4516  {
4517  if (reactorContainer != null && reactorContainer.Inventory.IsEmpty() &&
4518  reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault()))
4519  {
4520  ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()];
4522  fuelPrefab, reactorContainer.Inventory,
4523  onSpawned: (it) => reactorComponent.PowerUpImmediately());
4524  }
4525  if (beaconSonar == null)
4526  {
4527  DebugConsole.AddWarning($"Beacon station \"{BeaconStation.Info.Name}\" has no sonar. Beacon missions might not work correctly with this beacon station.");
4528  }
4529  else
4530  {
4531  beaconSonar.CurrentMode = Sonar.Mode.Active;
4532 #if SERVER
4533  beaconSonar.Item.CreateServerEvent(beaconSonar);
4534 #endif
4535  }
4536  }
4537  else if (GameMain.NetworkMember is not { IsClient: true })
4538  {
4539  bool allowDisconnectedWires = true;
4540  bool allowDamagedDevices = true;
4541  bool allowDamagedWalls = true;
4543  {
4544  allowDisconnectedWires = info.AllowDisconnectedWires;
4545  allowDamagedWalls = info.AllowDamagedWalls;
4546  allowDamagedDevices = info.AllowDamagedDevices;
4547  }
4548 
4549  //remove wires
4550  float disconnectWireMinDifficulty = 20.0f;
4551  float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
4552  if (disconnectWireProbability > 0.0f && allowDisconnectedWires)
4553  {
4554  DisconnectBeaconStationWires(disconnectWireProbability);
4555  }
4556 
4557  if (allowDamagedDevices)
4558  {
4559  DamageBeaconStationDevices(breakDeviceProbability: 0.5f);
4560  }
4561  if (allowDamagedWalls)
4562  {
4563  DamageBeaconStationWalls(damageWallProbability: 0.25f);
4564  }
4565  }
4566  SetLinkedSubCrushDepth(BeaconStation);
4567  }
4568 
4569  public void DisconnectBeaconStationWires(float disconnectWireProbability)
4570  {
4571  if (disconnectWireProbability <= 0.0f) { return; }
4572  List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
4573  foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
4574  {
4575  if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
4576  Wire wire = item.GetComponent<Wire>();
4577  if (wire.Locked) { continue; }
4578  if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent<ConnectionPanel>().Locked))
4579  {
4580  continue;
4581  }
4582  if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent<ConnectionPanel>().Locked))
4583  {
4584  continue;
4585  }
4586  if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < disconnectWireProbability)
4587  {
4588  foreach (Connection connection in wire.Connections)
4589  {
4590  if (connection != null)
4591  {
4592  connection.ConnectionPanel.DisconnectedWires.Add(wire);
4593  wire.RemoveConnection(connection.Item);
4594 #if SERVER
4595  connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
4596  wire.CreateNetworkEvent();
4597 #endif
4598  }
4599  }
4600  }
4601  }
4602  }
4603 
4604  public void DamageBeaconStationDevices(float breakDeviceProbability)
4605  {
4606  if (breakDeviceProbability <= 0.0f) { return; }
4607  //break powered items
4608  List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
4609  foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
4610  {
4611  if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
4612  if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < breakDeviceProbability)
4613  {
4614  item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
4615  }
4616  }
4617  }
4618 
4619  public void DamageBeaconStationWalls(float damageWallProbability)
4620  {
4621  if (damageWallProbability <= 0.0f) { return; }
4622  //poke holes in the walls
4623  foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
4624  {
4625  if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < damageWallProbability)
4626  {
4627  int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
4628  structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
4629  }
4630  }
4631  }
4632 
4633  public bool CheckBeaconActive()
4634  {
4635  if (beaconSonar == null) { return false; }
4636  return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active;
4637  }
4638 
4639  private void SetLinkedSubCrushDepth(Submarine parentSub)
4640  {
4641  foreach (var connectedSub in parentSub.GetConnectedSubs())
4642  {
4643  connectedSub.SetCrushDepth(Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000));
4644  }
4645  }
4646 
4647  private static bool IsModeStartOutpostCompatible()
4648  {
4649 #if CLIENT
4650  return GameMain.GameSession?.GameMode is CampaignMode || GameMain.GameSession?.GameMode is TutorialMode || GameMain.GameSession?.GameMode is TestGameMode;
4651 #else
4652  return GameMain.GameSession?.GameMode is CampaignMode;
4653 #endif
4654  }
4655 
4656  public void SpawnCorpses()
4657  {
4658  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
4659 
4660  foreach (Submarine wreck in Wrecks)
4661  {
4662  int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
4663  var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
4664  var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
4665  var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
4666  if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
4667  pathPoints.Shuffle(Rand.RandSync.Unsynced);
4668  // Sort by job so that we first spawn those with a predefined job (might have special id cards)
4669  corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
4670  var usedJobs = new HashSet<JobPrefab>();
4671  int spawnCounter = 0;
4672  for (int j = 0; j < corpseCount; j++)
4673  {
4674  WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault();
4675  JobPrefab job = sp?.AssignedJob;
4676  CorpsePrefab selectedPrefab;
4677  if (job == null)
4678  {
4679  selectedPrefab = GetCorpsePrefab(usedJobs);
4680  }
4681  else
4682  {
4683  selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier);
4684  if (selectedPrefab == null)
4685  {
4686  corpsePoints.Remove(sp);
4687  pathPoints.Remove(sp);
4688  sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
4689  // Deduce the job from the selected prefab
4690  selectedPrefab = GetCorpsePrefab(usedJobs);
4691  if (selectedPrefab != null)
4692  {
4693  job = selectedPrefab.GetJobPrefab();
4694  }
4695  }
4696  }
4697  if (selectedPrefab == null) { continue; }
4698  Vector2 worldPos;
4699  if (sp == null)
4700  {
4701  if (!TryGetExtraSpawnPoint(out worldPos))
4702  {
4703  break;
4704  }
4705  }
4706  else
4707  {
4708  worldPos = sp.WorldPosition;
4709  corpsePoints.Remove(sp);
4710  pathPoints.Remove(sp);
4711  }
4712 
4713  job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
4714  if (job == null) { continue; }
4715  if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer")
4716  {
4717  // Only spawn one of these jobs per wreck
4718  usedJobs.Add(job);
4719  }
4720  var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
4721  var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
4722  corpse.AnimController.FindHull(worldPos, setSubmarine: true);
4723  corpse.TeamID = CharacterTeamType.None;
4724  corpse.EnableDespawn = false;
4725  selectedPrefab.GiveItems(corpse, wreck, sp);
4726  corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
4727  corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
4728  bool applyBurns = Rand.Value() < 0.1f;
4729  bool applyDamage = Rand.Value() < 0.3f;
4730  foreach (var limb in corpse.AnimController.Limbs)
4731  {
4732  if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
4733  {
4734  var prefab = AfflictionPrefab.BiteWounds;
4735  float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
4736  corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
4737  }
4738  if (applyBurns)
4739  {
4740  var prefab = AfflictionPrefab.Burn;
4741  float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
4742  corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
4743  }
4744 
4745  static float GetStrength(Limb limb, float max)
4746  {
4747  float strength = Rand.Range(0, max);
4748  if (limb.type != LimbType.Head)
4749  {
4750  strength = Math.Min(strength, Rand.Range(0, max));
4751  }
4752  return strength;
4753  }
4754  }
4755  corpse.CharacterHealth.ForceUpdateVisuals();
4756 
4757  bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
4758  if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
4759  {
4760  corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
4761  }
4762 
4763  spawnCounter++;
4764 
4765  static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate = null)
4766  {
4767  IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(p =>
4768  usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) &&
4769  p.SpawnPosition == PositionType.Wreck &&
4770  (predicate == null || predicate(p)));
4771 
4772  return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced);
4773  }
4774  }
4775 #if DEBUG
4776  DebugConsole.NewMessage($"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow);
4777 #endif
4778  bool TryGetExtraSpawnPoint(out Vector2 point)
4779  {
4780  point = Vector2.Zero;
4781  var hull = Hull.HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced();
4782  if (hull != null)
4783  {
4784  point = hull.WorldPosition;
4785  }
4786  return hull != null;
4787  }
4788  }
4789  }
4790 
4791  public void SpawnNPCs()
4792  {
4793  if (Type != LevelData.LevelType.Outpost) { return; }
4794  foreach (Submarine sub in Submarine.Loaded)
4795  {
4796  if (sub?.Info?.OutpostGenerationParams != null)
4797  {
4798  OutpostGenerator.SpawnNPCs(StartLocation, sub);
4799  }
4800  }
4801  }
4802 
4806  public float GetRealWorldDepth(float worldPositionY)
4807  {
4808  if (GameMain.GameSession?.Campaign == null)
4809  {
4810  //ensure the levels aren't too deep to traverse in non-campaign modes where you don't have the option to upgrade/switch the sub
4811  return (-(worldPositionY - GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio;
4812  }
4813  else
4814  {
4815  return (-(worldPositionY - GenerationParams.Height) + LevelData.InitialDepth) * Physics.DisplayToRealWorldRatio;
4816  }
4817  }
4818 
4819  public void DebugSetStartLocation(Location newStartLocation)
4820  {
4821  StartLocation = newStartLocation;
4822  }
4823 
4824  public void DebugSetEndLocation(Location newEndLocation)
4825  {
4826  EndLocation = newEndLocation;
4827  }
4828 
4829  public override void Remove()
4830  {
4831  base.Remove();
4832 #if CLIENT
4833  if (renderer != null)
4834  {
4835  renderer.Dispose();
4836  renderer = null;
4837  }
4838 #endif
4839 
4840  if (LevelObjectManager != null)
4841  {
4843  LevelObjectManager = null;
4844  }
4845 
4846  AbyssIslands?.Clear();
4847  AbyssResources?.Clear();
4848  Caves?.Clear();
4849  Tunnels?.Clear();
4850  PathPoints?.Clear();
4851  PositionsOfInterest?.Clear();
4852 
4853  wreckPositions?.Clear();
4854  Wrecks?.Clear();
4855 
4856  BeaconStation = null;
4857  beaconSonar = null;
4858  StartOutpost = null;
4859  EndOutpost = null;
4860 
4861  blockedRects?.Clear();
4862 
4863  EntitiesBeforeGenerate?.Clear();
4864  ClearEqualityCheckValues();
4865 
4866  if (Ruins != null)
4867  {
4868  Ruins.Clear();
4869  Ruins = null;
4870  }
4871 
4872  bottomPositions?.Clear();
4873  BottomBarrier = null;
4874  TopBarrier = null;
4875  SeaFloor = null;
4876 
4877  distanceField = null;
4878 
4879  if (ExtraWalls != null)
4880  {
4881  foreach (LevelWall w in ExtraWalls) { w.Dispose(); }
4882  ExtraWalls = null;
4883  }
4884  if (UnsyncedExtraWalls != null)
4885  {
4886  foreach (LevelWall w in UnsyncedExtraWalls) { w.Dispose(); }
4887  UnsyncedExtraWalls = null;
4888  }
4889 
4890  tempCells?.Clear();
4891  cells = null;
4892  cellGrid = null;
4893 
4894  if (bodies != null)
4895  {
4896  bodies.Clear();
4897  bodies = null;
4898  }
4899 
4900  StartLocation = null;
4901  EndLocation = null;
4902 
4903  Loaded = null;
4904  }
4905  }
4906 
4907  static class PositionTypeExtensions
4908  {
4912  public static bool IsEnclosedArea(this Level.PositionType positionType)
4913  {
4914  return
4915  positionType == Level.PositionType.Cave ||
4916  positionType == Level.PositionType.AbyssCave ||
4917  positionType.IsIndoorsArea();
4918  }
4919 
4923  public static bool IsIndoorsArea(this Level.PositionType positionType)
4924  {
4925  return
4926  positionType == Level.PositionType.Outpost ||
4927  positionType == Level.PositionType.BeaconStation ||
4928  positionType == Level.PositionType.Ruin ||
4929  positionType == Level.PositionType.Wreck;
4930  }
4931  }
4932 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
Affliction Instantiate(float strength, Character source=null)
static AfflictionPrefab OxygenLow
static AfflictionPrefab Burn
readonly float MaxStrength
The maximum strength this affliction can have.
static AfflictionPrefab BiteWounds
void SpawnCreatures(Level level, int count, Vector2? position=null)
void Update(float deltaTime, Camera cam)
readonly bool IsEndBiome
Definition: Biome.cs:16
readonly float MinDifficulty
Definition: Biome.cs:19
float AdjustedMaxDifficulty
Definition: Biome.cs:22
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
Stores information about the Character that is needed between rounds in the menu etc....
static readonly Identifier HumanSpeciesName
Base class for content file types, which are loaded from filelist.xml via reflection....
Definition: ContentFile.cs:23
readonly ContentPath Path
Definition: ContentFile.cs:137
string???????????? Value
Definition: ContentPath.cs:27
static readonly PrefabCollection< CorpsePrefab > Prefabs
Definition: CorpsePrefab.cs:12
static EntitySpawner Spawner
Definition: Entity.cs:31
virtual Vector2 WorldPosition
Definition: Entity.cs:49
static IReadOnlyCollection< Entity > GetEntities()
Definition: Entity.cs:24
Submarine Submarine
Definition: Entity.cs:53
void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition=null, int? quality=null, Action< Item > onSpawned=null)
static GameSession?? GameSession
Definition: GameMain.cs:88
static bool IsSingleplayer
Definition: GameMain.cs:34
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static readonly List< Hull > HullList
JobPrefab GetJobPrefab(Rand.RandSync randSync=Rand.RandSync.Unsynced, Func< JobPrefab, bool > predicate=null)
Definition: HumanPrefab.cs:159
bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync=Rand.RandSync.Unsynced, bool createNetworkEvents=true)
Definition: HumanPrefab.cs:205
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
readonly HashSet< Wire > DisconnectedWires
Wires that have been disconnected from the panel, but not removed completely (visible at the bottom o...
AbyssIsland(Rectangle area, List< VoronoiCell > cells)
Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos)
Tunnel(TunnelType type, List< Point > nodes, int minWidth, Tunnel parentTunnel)
int? MinMainPathWidth
Determined during level generation based on the size of the submarine. Null if the level hasn't been ...
Definition: LevelData.cs:62
SubmarineInfo ForceBeaconStation
Definition: LevelData.cs:46
float CrushDepth
The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the t...
Definition: LevelData.cs:85
OutpostGenerationParams ForceOutpostGenerationParams
Definition: LevelData.cs:44
LevelGenerationParams GenerationParams
Definition: LevelData.cs:27
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
Definition: LevelData.cs:96
readonly Point Size
Definition: LevelData.cs:52
readonly float Difficulty
Definition: LevelData.cs:23
bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty)
Inclusive (matching the min an max values is accepted).
readonly LevelType Type
Definition: LevelData.cs:19
readonly int InitialDepth
The depth at which the level starts at, in in-game coordinates. E.g. if this was set to 100 000 (= 10...
Definition: LevelData.cs:57
SubmarineInfo ForceWreck
Definition: LevelData.cs:48
readonly string Seed
Definition: LevelData.cs:21
static IEnumerable< OutpostGenerationParams > GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
Definition: LevelData.cs:315
readonly Biome Biome
Definition: LevelData.cs:25
bool OutpostGenerationParamsExist
Definition: LevelData.cs:313
readonly ImmutableHashSet< Identifier > AllowedBiomeIdentifiers
const float OutsideBoundsCurrentMargin
How far outside the boundaries of the level the water current that pushes subs towards the level star...
bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Func< InterestingPosition, bool > filter=null, bool suppressWarning=false)
void DamageBeaconStationWalls(float damageWallProbability)
const float OutsideBoundsCurrentHardLimit
How far outside the boundaries of the level the current stops submarines entirely
LevelGenStage
Random integers generated during the level generation. If these values differ between clients/server,...
Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall=10.0f, Func< InterestingPosition, bool > filter=null)
bool IsCloseToEnd(Vector2 position, float minDist)
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
void DebugSetEndLocation(Location newEndLocation)
void DamageBeaconStationDevices(float breakDeviceProbability)
bool IsCloseToEnd(Point position, float minDist)
BackgroundCreatureManager BackgroundCreatureManager
bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint, Func< InterestingPosition, bool > filter=null)
const int MaxSubmarineWidth
The level generator won't try to adjust the width of the main path above this limit.
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
bool IsCloseToStart(Vector2 position, float minDist)
bool IsCloseToStart(Point position, float minDist)
List<(Point point, double distance)> distanceField
IReadOnlyDictionary< LevelGenStage, int > EqualityCheckValues
float CrushDepth
The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the t...
bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint=0f, Func< InterestingPosition, bool > filter=null, bool suppressWarning=false)
void DebugSetStartLocation(Location newStartLocation)
void Update(float deltaTime, Camera cam)
static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost=null, SubmarineInfo endOutpost=null)
List< Item > GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable< Cave > targetCaves=null)
Used by clients to set the rotation for the resources
const float OutsideBoundsCurrentMarginExponential
How far outside the boundaries of the level the strength of the current starts to increase exponentia...
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
List< VoronoiCell > GetTooCloseCells(Vector2 position, float minDistance)
void DisconnectBeaconStationWires(float disconnectWireProbability)
bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty)
Inclusive (matching the min an max values is accepted).
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
void SetVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color)
void Update(float deltaTime, Camera cam)
LocationType Type
Definition: Location.cs:91
LocalizedString DisplayName
Definition: Location.cs:58
readonly CharacterTeamType OutpostTeam
Definition: LocationType.cs:34
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
static readonly ImmutableArray< PositionType > ValidPositionTypes
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
void FindHull(Vector2? worldPosition=null, bool setSubmarine=true)
static readonly PrefabCollection< RuinGenerationParams > RuinParams
void AddDamage(int sectionIndex, float damage, Character attacker=null, bool emitParticles=true, bool createWallDamageProjectiles=false)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static Rectangle GetBorders(XElement submarineElement)
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
static Rectangle AbsRect(Vector2 pos, Vector2 size)
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 ScrollWater(Vector2 vel, float deltaTime)
static WaterRenderer Instance
VoronoiCell AdjacentCell(VoronoiCell cell)
Vector2 GetNormal(VoronoiCell cell)
Returns the normal of the edge that points outwards from the specified cell
DoubleVector2 Coord
List< GraphEdge > Edges
bool IsPointInsideAABB(Vector2 point2, float margin)
bool IsPointInside(Vector2 point)
Description of Voronoi.
Definition: Voronoi.cs:76
List< GraphEdge > MakeVoronoiGraph(List< Vector2 > sites, int width, int height)
Definition: Voronoi.cs:973
Interface for entities that the server can send events to the clients
bool Equals(ClusterLocation anotherLocation)
List< Item > Resources
Can be null unless initialized in constructor
ClusterLocation(VoronoiCell cell, GraphEdge edge, bool initializeResourceList=false)
List is initialized only when specified, otherwise will be null
bool Equals(VoronoiCell cell, GraphEdge edge)
bool IsEnclosedArea()
Caves, ruins, outposts and similar enclosed areas
InterestingPosition(Point position, PositionType positionType, Submarine submarine=null, bool isValid=true)
InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid=true)
InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid=true)
PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)