using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.RuinGeneration; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Factories; using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Voronoi2; namespace Barotrauma { partial class Level : Entity, IServerSerializable { //all entities are disabled after they reach this depth public const int MaxEntityDepth = -300000; public const float ShaftHeight = 1000.0f; public static Level Loaded { get { return loaded; } } [Flags] public enum PositionType { MainPath = 1, Cave = 2, Ruin = 4 } public struct InterestingPosition { public Point Position; public readonly PositionType PositionType; public InterestingPosition(Point position, PositionType positionType) { Position = position; PositionType = positionType; } } static Level loaded; //how close the sub has to be to start/endposition to exit public const float ExitDistance = 6000.0f; private string seed; public const int GridCellSize = 2000; private List[,] cellGrid; private List extraWalls; private LevelWall seaFloor; private List cells; private Point startPosition, endPosition; private Rectangle borders; private List bodies; private List positionsOfInterest; private List ruins; private LevelGenerationParams generationParams; private List> smallTunnels = new List>(); private LevelObjectManager levelObjectManager; private List bottomPositions; //no need for frequent network updates, as currently the only thing that's synced //are the slowly moving ice chunks that move in a very predictable way const float NetworkUpdateInterval = 5.0f; private float networkUpdateTimer; public Vector2 StartPosition { get { return startPosition.ToVector2(); } } public Point Size { get { return new Point(borders.Width, borders.Height); } } public Vector2 EndPosition { get { return endPosition.ToVector2(); } } public int BottomPos { get; private set; } public int SeaFloorTopPos { get; private set; } public LevelWall SeaFloor { get { return seaFloor; } } public List Ruins { get { return ruins; } } public List ExtraWalls { get { return extraWalls; } } public List> SmallTunnels { get { return smallTunnels; } } public List PositionsOfInterest { get { return positionsOfInterest; } } public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } public string Seed { get { return seed; } } public Biome Biome; /// /// A random integer assigned at the end of level generation. If these values differ between clients/server, /// it means the levels aren't identical for some reason and there will most likely be major ID mismatches. /// public int EqualityCheckVal { get; private set; } public float Difficulty { get; private set; } public Body TopBarrier { get; private set; } public Body BottomBarrier { get; private set; } public LevelObjectManager LevelObjectManager { get { return levelObjectManager; } } public bool Mirrored { get; private set; } public LevelGenerationParams GenerationParams { get { return generationParams; } } public Color BackgroundTextureColor { get { return generationParams.BackgroundTextureColor; } } public Color BackgroundColor { get { return generationParams.BackgroundColor; } } public Color WallColor { get { return generationParams.WallColor; } } /// /// Instantiates a level (the Generate-function still needs to be called before the level is playable) /// /// A scalar between 0-100 /// A scalar between 0-1 (0 = the minimum width defined in the generation params is used, 1 = the max width is used) public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome) : base(null) { this.seed = seed; this.Biome = biome; this.Difficulty = difficulty; this.generationParams = generationParams; sizeFactor = MathHelper.Clamp(sizeFactor, 0.0f, 1.0f); int width = (int)MathHelper.Lerp(generationParams.MinWidth, generationParams.MaxWidth, sizeFactor); borders = new Rectangle(0, 0, (width / GridCellSize) * GridCellSize, (generationParams.Height / GridCellSize) * GridCellSize); //remove from entity dictionary base.Remove(); } public static Level CreateRandom(LocationConnection locationConnection) { string seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; float sizeFactor = MathUtils.InverseLerp( MapGenerationParams.Instance.SmallLevelConnectionLength, MapGenerationParams.Instance.LargeLevelConnectionLength, locationConnection.Length); return new Level(seed, locationConnection.Difficulty, sizeFactor, LevelGenerationParams.GetRandom(seed, locationConnection.Biome), locationConnection.Biome); } public static Level CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null) { if (seed == "") { seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString(); } Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); if (generationParams == null) generationParams = LevelGenerationParams.GetRandom(seed); var biome = LevelGenerationParams.GetBiomes().Find(b => generationParams.AllowedBiomes.Contains(b)); return new Level( seed, difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server), Rand.Range(0.0f, 1.0f, Rand.RandSync.Server), generationParams, biome); } public void Generate(bool mirror) { if (loaded != null) loaded.Remove(); loaded = this; levelObjectManager = new LevelObjectManager(); Mirrored = mirror; #if CLIENT if (backgroundCreatureManager == null) { var files = GameMain.Instance.GetFilesOfType(ContentType.BackgroundCreaturePrefabs); if (files.Count() > 0) backgroundCreatureManager = new BackgroundCreatureManager(files); else backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml"); } #endif Stopwatch sw = new Stopwatch(); sw.Start(); positionsOfInterest = new List(); extraWalls = new List(); bodies = new List(); List sites = new List(); Voronoi voronoi = new Voronoi(1.0); Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); #if CLIENT renderer = new LevelRenderer(this); #endif SeaFloorTopPos = generationParams.SeaFloorDepth + generationParams.MountainHeightMax + generationParams.SeaFloorVariance; int minWidth = 6500; int maxWidth = 50000; if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); minWidth = Math.Min(minWidth, maxWidth); } Rectangle pathBorders = borders; pathBorders.Inflate(-minWidth * 2, -minWidth); Debug.Assert(pathBorders.Width > 0 && pathBorders.Height > 0, "The size of the level's path area was negative."); startPosition = new Point( Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); endPosition = new Point( borders.Width - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels //---------------------------------------------------------------------------------- List pathNodes = new List(); pathNodes.Add(new Point(startPosition.X, borders.Height)); Point nodeInterval = generationParams.MainPathNodeIntervalRange; for (int x = startPosition.X + nodeInterval.X; x < endPosition.X - nodeInterval.X; x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) { pathNodes.Add(new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server))); } pathNodes.Add(new Point(endPosition.X, borders.Height)); if (pathNodes.Count <= 2) { pathNodes.Insert(1, borders.Center); } GenerateTunnels(pathNodes, minWidth); //---------------------------------------------------------------------------------- //generate voronoi sites //---------------------------------------------------------------------------------- Point siteInterval = generationParams.VoronoiSiteInterval; int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); Point siteVariance = generationParams.VoronoiSiteVariance; List siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); List siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X) { for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y) { int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server); int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server); if (smallTunnels.Any(t => t.Any(node => MathUtils.DistanceSquared(node.X, node.Y, siteX, siteY) < siteIntervalSqr))) { //add some more sites around the small tunnels to generate more small voronoi cells if (x < borders.Width - siteInterval.X) { siteCoordsX.Add(x + siteInterval.X / 2); siteCoordsY.Add(y); } if (y < borders.Height - siteInterval.Y) { siteCoordsX.Add(x); siteCoordsY.Add(y + siteInterval.Y / 2); } if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y) { siteCoordsX.Add(x + siteInterval.X / 2); siteCoordsY.Add(y + siteInterval.Y / 2); } } siteCoordsX.Add(siteX); siteCoordsY.Add(siteY); } } //---------------------------------------------------------------------------------- // construct the voronoi graph and cells //---------------------------------------------------------------------------------- Stopwatch sw2 = new Stopwatch(); sw2.Start(); Debug.Assert(siteCoordsX.Count == siteCoordsY.Count); List graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height); Debug.WriteLine("MakeVoronoiGraph: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); //construct voronoi cells based on the graph edges cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); //---------------------------------------------------------------------------------- // generate a path through the initial path nodes //---------------------------------------------------------------------------------- List mainPath = CaveGenerator.GeneratePath(pathNodes, cells, cellGrid, GridCellSize, new Rectangle(pathBorders.X, pathBorders.Y, pathBorders.Width, borders.Height), 0.5f, false); for (int i = 2; i < mainPath.Count; i += 3) { positionsOfInterest.Add(new InterestingPosition( new Point((int)mainPath[i].Site.Coord.X, (int)mainPath[i].Site.Coord.Y), PositionType.MainPath)); } List pathCells = new List(mainPath); //make sure the path is wide enough to pass through EnlargeMainPath(pathCells, minWidth); foreach (InterestingPosition positionOfInterest in positionsOfInterest) { WayPoint wayPoint = new WayPoint( positionOfInterest.Position.ToVector2(), SpawnType.Enemy, submarine: null); } startPosition.X = (int)pathCells[0].Site.Coord.X; //---------------------------------------------------------------------------------- // tunnels through the tunnel nodes //---------------------------------------------------------------------------------- List> validTunnels = new List>(); foreach (List tunnel in smallTunnels) { if (tunnel.Count < 2) continue; //find the cell which the path starts from int startCellIndex = CaveGenerator.FindCellIndex(tunnel[0], cells, cellGrid, GridCellSize, 1); if (startCellIndex < 0) continue; //if it wasn't one of the cells in the main path, don't create a tunnel if (cells[startCellIndex].CellType != CellType.Path) continue; int mainPathCellCount = 0; for (int j = 0; j < tunnel.Count; j++) { int tunnelCellIndex = CaveGenerator.FindCellIndex(tunnel[j], cells, cellGrid, GridCellSize, 1); if (tunnelCellIndex > -1 && cells[tunnelCellIndex].CellType == CellType.Path) mainPathCellCount++; } if (mainPathCellCount > tunnel.Count / 2) continue; var newPathCells = CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); positionsOfInterest.Add(new InterestingPosition(tunnel.Last(), PositionType.Cave)); if (tunnel.Count > 4) positionsOfInterest.Add(new InterestingPosition(tunnel[tunnel.Count / 2], PositionType.Cave)); validTunnels.Add(tunnel); pathCells.AddRange(newPathCells); } smallTunnels = validTunnels; sw2.Restart(); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level //---------------------------------------------------------------------------------- cells = CleanCells(pathCells); int xPadding = borders.Width / 5; int yPadding = borders.Height / 5; pathCells.AddRange(CreateBottomHoles(generationParams.BottomHoleProbability, new Rectangle( xPadding, 0, borders.Width - xPadding * 2, borders.Height - yPadding))); foreach (VoronoiCell cell in cells) { if (cell.Site.Coord.Y < borders.Height / 2) continue; cell.Edges.ForEach(e => e.OutsideLevel = true); } //---------------------------------------------------------------------------------- // initialize the cells that are still left and insert them into the cell grid //---------------------------------------------------------------------------------- foreach (VoronoiCell cell in pathCells) { cell.Edges.ForEach(e => e.OutsideLevel = false); cell.CellType = CellType.Path; cells.Remove(cell); } for (int x = 0; x < cellGrid.GetLength(0); x++) { for (int y = 0; y < cellGrid.GetLength(1); y++) { cellGrid[x, y].Clear(); } } //---------------------------------------------------------------------------------- // mirror if needed //---------------------------------------------------------------------------------- if (mirror) { HashSet mirroredEdges = new HashSet(); HashSet mirroredSites = new HashSet(); List allCells = new List(cells); allCells.AddRange(pathCells); foreach (VoronoiCell cell in allCells) { foreach (GraphEdge edge in cell.Edges) { if (mirroredEdges.Contains(edge)) continue; edge.Point1.X = borders.Width - edge.Point1.X; edge.Point2.X = borders.Width - edge.Point2.X; if (!mirroredSites.Contains(edge.Site1)) { //make sure that sites right at the edge of a grid cell end up in the same cell as in the non-mirrored level if (edge.Site1.Coord.X % GridCellSize < 1.0f && edge.Site1.Coord.X % GridCellSize >= 0.0f) edge.Site1.Coord.X += 1.0f; edge.Site1.Coord.X = borders.Width - edge.Site1.Coord.X; mirroredSites.Add(edge.Site1); } if (!mirroredSites.Contains(edge.Site2)) { if (edge.Site2.Coord.X % GridCellSize < 1.0f && edge.Site2.Coord.X % GridCellSize >= 0.0f) edge.Site2.Coord.X += 1.0f; edge.Site2.Coord.X = borders.Width - edge.Site2.Coord.X; mirroredSites.Add(edge.Site2); } mirroredEdges.Add(edge); } } foreach (List smallTunnel in smallTunnels) { for (int i = 0; i < smallTunnel.Count; i++) { smallTunnel[i] = new Point(borders.Width - smallTunnel[i].X, smallTunnel[i].Y); } } for (int i = 0; i < positionsOfInterest.Count; i++) { positionsOfInterest[i] = new InterestingPosition( new Point(borders.Width - positionsOfInterest[i].Position.X, positionsOfInterest[i].Position.Y), positionsOfInterest[i].PositionType); } foreach (WayPoint waypoint in WayPoint.WayPointList) { if (waypoint.Submarine != null) continue; waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f)); } startPosition.X = borders.Width - startPosition.X; endPosition.X = borders.Width - endPosition.X; } foreach (VoronoiCell cell in cells) { int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize); int y = (int)Math.Floor(cell.Site.Coord.Y / GridCellSize); if (x < 0 || y < 0 || x >= cellGrid.GetLength(0) || y >= cellGrid.GetLength(1)) continue; cellGrid[x, y].Add(cell); } //---------------------------------------------------------------------------------- // create some ruins //---------------------------------------------------------------------------------- ruins = new List(); for (int i = 0; i < generationParams.RuinCount; i++) { GenerateRuin(mainPath, this, mirror); } //---------------------------------------------------------------------------------- // create floating ice chunks //---------------------------------------------------------------------------------- if (generationParams.FloatingIceChunkCount > 0) { List iceChunkPositions = new List(); foreach (InterestingPosition pos in positionsOfInterest) { if (pos.PositionType != PositionType.MainPath || pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) continue; if (Math.Abs(pos.Position.X - StartPosition.X) < 10000) continue; if (Math.Abs(pos.Position.Y - StartPosition.Y) < 10000) continue; if (GetTooCloseCells(pos.Position.ToVector2(), minWidth * 0.7f).Count > 0) continue; iceChunkPositions.Add(pos.Position); } for (int i = 0; i < generationParams.FloatingIceChunkCount; i++) { if (iceChunkPositions.Count == 0) break; Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.Server)]; float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server); var newChunk = new LevelWall(CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f), Color.White, this, true) { MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server), MoveAmount = new Vector2(0.0f, minWidth * 0.7f) }; newChunk.Body.Position = ConvertUnits.ToSimUnits(selectedPos.ToVector2()); newChunk.Body.BodyType = BodyType.Dynamic; newChunk.Body.FixedRotation = true; newChunk.Body.LinearDamping = 0.5f; newChunk.Body.GravityScale = 0.0f; newChunk.Body.Mass *= 10.0f; extraWalls.Add(newChunk); iceChunkPositions.Remove(selectedPos); } } //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells //---------------------------------------------------------------------------------- startPosition.Y = borders.Height; endPosition.Y = borders.Height; foreach (VoronoiCell cell in cells) { foreach (GraphEdge ge in cell.Edges) { VoronoiCell adjacentCell = ge.AdjacentCell(cell); ge.IsSolid = (adjacentCell == null || !cells.Contains(adjacentCell)); } } List cellsWithBody = new List(cells); if (generationParams.CellRoundingAmount > 0.01f || generationParams.CellIrregularity > 0.01f) { foreach (VoronoiCell cell in cellsWithBody) { CaveGenerator.RoundCell(cell, minEdgeLength: generationParams.CellSubdivisionLength, roundingAmount: generationParams.CellRoundingAmount, irregularity: generationParams.CellIrregularity); } } bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List triangles)); #if CLIENT renderer.SetBodyVertices(CaveGenerator.GenerateRenderVerticeList(triangles).ToArray(), generationParams.WallColor); renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cellsWithBody, this), generationParams.WallColor); #endif //---------------------------------------------------------------------------------- // create (placeholder) outposts at the start and end of the level //---------------------------------------------------------------------------------- CreateOutposts(); //---------------------------------------------------------------------------------- // top barrier & sea floor //---------------------------------------------------------------------------------- TopBarrier = BodyFactory.CreateEdge(GameMain.World, ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); TopBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, borders.Height)), 0.0f); TopBarrier.BodyType = BodyType.Static; TopBarrier.CollisionCategories = Physics.CollisionLevel; bodies.Add(TopBarrier); GenerateSeaFloor(mirror); levelObjectManager.PlaceObjects(this, generationParams.LevelObjectAmount); GenerateItems(); EqualityCheckVal = Rand.Int(int.MaxValue, Rand.RandSync.Server); #if CLIENT backgroundCreatureManager.SpawnSprites(80); #endif foreach (VoronoiCell cell in cells) { foreach (GraphEdge edge in cell.Edges) { edge.Cell1 = null; edge.Cell2 = null; edge.Site1 = null; edge.Site2 = null; } } //initialize MapEntities that aren't in any sub (e.g. items inside ruins) MapEntity.MapLoaded(MapEntity.mapEntityList.FindAll(me => me.Submarine == null), false); Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); if (mirror) { Point temp = startPosition; startPosition = endPosition; endPosition = temp; } if (StartOutpost != null) { startPosition = new Point((int)StartOutpost.WorldPosition.X, (int)StartOutpost.WorldPosition.Y); } if (EndOutpost != null) { endPosition = new Point((int)EndOutpost.WorldPosition.X, (int)EndOutpost.WorldPosition.Y); } Debug.WriteLine("**********************************************************************************"); Debug.WriteLine("Generated a map with " + siteCoordsX.Count + " sites in " + sw.ElapsedMilliseconds + " ms"); Debug.WriteLine("Seed: " + seed); Debug.WriteLine("**********************************************************************************"); if (GameSettings.VerboseLogging) { DebugConsole.NewMessage("Generated level with the seed " + seed + " (type: " + generationParams.Name + ")", Color.White); } //assign an ID to make entity events work ID = FindFreeID(); } private List CreateBottomHoles(float holeProbability, Rectangle limits) { List toBeRemoved = new List(); foreach (VoronoiCell cell in cells) { if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) continue; if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) continue; float closestDist = 0.0f; WayPoint closestWayPoint = null; foreach (WayPoint wp in WayPoint.WayPointList) { if (wp.SpawnType != SpawnType.Path) continue; float dist = Math.Abs(cell.Center.X - wp.WorldPosition.X); if (closestWayPoint == null || dist < closestDist) { closestDist = dist; closestWayPoint = wp; } } if (closestWayPoint.WorldPosition.Y < cell.Center.Y) continue; toBeRemoved.Add(cell); } return toBeRemoved; } private void EnlargeMainPath(List pathCells, float minWidth) { List wayPoints = new List(); var newWaypoint = new WayPoint(new Rectangle((int)pathCells[0].Site.Coord.X, borders.Height, 10, 10), null); wayPoints.Add(newWaypoint); for (int i = 0; i < pathCells.Count; i++) { pathCells[i].CellType = CellType.Path; newWaypoint = new WayPoint(new Rectangle((int)pathCells[i].Site.Coord.X, (int)pathCells[i].Center.Y, 10, 10), null); wayPoints.Add(newWaypoint); wayPoints[wayPoints.Count-2].linkedTo.Add(newWaypoint); newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); for (int n = 0; n < wayPoints.Count; n++) { if (wayPoints[n].Position != newWaypoint.Position) continue; wayPoints[n].linkedTo.Add(newWaypoint); newWaypoint.linkedTo.Add(wayPoints[n]); break; } } newWaypoint = new WayPoint(new Rectangle((int)pathCells[pathCells.Count - 1].Site.Coord.X, borders.Height, 10, 10), null); wayPoints.Add(newWaypoint); wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint); newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); if (minWidth > 0.0f) { List removedCells = GetTooCloseCells(pathCells, minWidth); foreach (VoronoiCell removedCell in removedCells) { if (removedCell.CellType == CellType.Path) continue; pathCells.Add(removedCell); removedCell.CellType = CellType.Path; } } } private List GetTooCloseCells(List emptyCells, float minDistance) { List tooCloseCells = new List(); Vector2 position = emptyCells[0].Center; if (minDistance <= 0.0f) return tooCloseCells; float step = 100.0f; int targetCellIndex = 1; minDistance *= 0.5f; do { tooCloseCells.AddRange(GetTooCloseCells(position, minDistance)); position += Vector2.Normalize(emptyCells[targetCellIndex].Center - position) * step; if (Vector2.Distance(emptyCells[targetCellIndex].Center, position) < step * 2.0f) targetCellIndex++; } while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f); return tooCloseCells; } private List GetTooCloseCells(Vector2 position, float minDistance) { List tooCloseCells = new List(); var closeCells = GetCells(position, 3); float minDistSqr = minDistance * minDistance; foreach (VoronoiCell cell in closeCells) { bool tooClose = false; foreach (GraphEdge edge in cell.Edges) { if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || Vector2.DistanceSquared(edge.Point2, position) < minDistSqr) { tooClose = true; break; } } if (tooClose && !tooCloseCells.Contains(cell)) tooCloseCells.Add(cell); } return tooCloseCells; } /// /// remove all cells except those that are adjacent to the empty cells /// private List CleanCells(List emptyCells) { List newCells = new List(); foreach (VoronoiCell cell in emptyCells) { foreach (GraphEdge edge in cell.Edges) { VoronoiCell adjacent = edge.AdjacentCell(cell); if (adjacent != null && !newCells.Contains(adjacent)) { newCells.Add(adjacent); } } } return newCells; } private void GenerateSeaFloor(bool mirror) { BottomPos = generationParams.SeaFloorDepth; SeaFloorTopPos = BottomPos; bottomPositions = new List { new Point(0, BottomPos) }; int mountainCount = Rand.Range(generationParams.MountainCountMin, generationParams.MountainCountMax, Rand.RandSync.Server); for (int i = 0; i < mountainCount; i++) { bottomPositions.Add( new Point(Size.X / (mountainCount + 1) * (i + 1), BottomPos + Rand.Range(generationParams.MountainHeightMin, generationParams.MountainHeightMax, Rand.RandSync.Server))); } bottomPositions.Add(new Point(Size.X, BottomPos)); int minVertexInterval = 5000; float currInverval = Size.X / 2; while (currInverval > minVertexInterval) { for (int i = 0; i < bottomPositions.Count - 1; i++) { bottomPositions.Insert(i + 1, new Point( (bottomPositions[i].X + bottomPositions[i + 1].X) / 2, (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, generationParams.SeaFloorVariance, Rand.RandSync.Server))); i++; } currInverval /= 2; } if (mirror) { for (int i = 0; i < bottomPositions.Count; i++) { bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y); } } SeaFloorTopPos = bottomPositions.Max(p => p.Y); seaFloor = new LevelWall(bottomPositions.Select(p => p.ToVector2()).ToList(), new Vector2(0.0f, -2000.0f), generationParams.WallColor, this); extraWalls.Add(seaFloor); BottomBarrier = BodyFactory.CreateEdge(GameMain.World, ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)), ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0))); BottomBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, BottomPos)), 0.0f); BottomBarrier.BodyType = BodyType.Static; BottomBarrier.CollisionCategories = Physics.CollisionLevel; bodies.Add(BottomBarrier); } private void GenerateTunnels(List pathNodes, int pathWidth) { smallTunnels = new List>(); for (int i = 0; i < generationParams.SmallTunnelCount; i++) { var tunnelStartPos = pathNodes[Rand.Range(1, pathNodes.Count - 2, Rand.RandSync.Server)]; int tunnelLength = Rand.Range( generationParams.SmallTunnelLengthRange.X, generationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server); List tunnelNodes = new List() { tunnelStartPos, tunnelStartPos + new Point(0, Math.Sign(tunnelStartPos.Y - Size.Y / 2) * pathWidth * 2) }; List tunnel = GenerateTunnel( tunnelNodes, Rand.Range(generationParams.SmallTunnelLengthRange.X, generationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server), pathNodes); if (tunnel.Any()) smallTunnels.Add(tunnel); int branches = Rand.Range(0, 3, Rand.RandSync.Server); for (int j = 0; j < branches; j++) { List branch = GenerateTunnel( new List() { tunnel[Rand.Int(tunnel.Count, Rand.RandSync.Server)] }, Rand.Range(generationParams.SmallTunnelLengthRange.X, generationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server) * 0.5f, pathNodes); if (branch.Any()) smallTunnels.Add(branch); } } } private List GenerateTunnel(List tunnelNodes, float tunnelLength, List avoidNodes) { int sectionLength = 1000; float currLength = 0.0f; DoubleVector2 dir = null; while (currLength < tunnelLength) { var prevDir = dir; dir = Rand.Vector(1.0, Rand.RandSync.Server); dir.Y += Math.Sign(tunnelNodes[tunnelNodes.Count - 1].Y - Size.Y / 2) * 0.5f; if (prevDir != null) { dir.X = (dir.X + prevDir.X) / 2.0; dir.Y = (dir.Y + prevDir.Y) / 2.0; } double avoidDist = 20000; double avoidDistSqr = avoidDist * avoidDist; foreach (Point pathNode in avoidNodes) { double diffX = tunnelNodes[tunnelNodes.Count - 1].X - pathNode.X; double diffY = tunnelNodes[tunnelNodes.Count - 1].Y - pathNode.Y; if (Math.Abs(diffX) < 1.0f || Math.Abs(diffY) < 1.0f) continue; double distSqr = (diffX * diffX + diffY * diffY); Debug.Assert(distSqr > 0); if (distSqr < avoidDistSqr) { double dist = Math.Sqrt(distSqr); dir.X += (diffX / dist) * (1.0f - dist / avoidDist); dir.Y += (diffY / dist) * (1.0f - dist / avoidDist); } } dir.Normalize(); if (tunnelNodes.Last().Y + dir.Y > Size.Y) { //head back down if the tunnel has reached the top of the level dir.Y = -dir.Y; } else if (tunnelNodes.Last().Y + dir.Y * 500 < 500) { //head back up if reached the bottom of the level dir.Y = -dir.Y; } else if (tunnelNodes.Last().Y + dir.Y + dir.Y < 0.0f || tunnelNodes.Last().Y + dir.Y + dir.Y < SeaFloorTopPos) { //head back up if reached the sea floor dir.Y = -dir.Y; } Point nextNode = tunnelNodes.Last() + new Point((int)(dir.X * sectionLength), (int)(dir.Y * sectionLength)); nextNode.X = MathHelper.Clamp(nextNode.X, 500, Size.X - 500); nextNode.Y = MathHelper.Clamp(nextNode.Y, SeaFloorTopPos, Size.Y - 500); tunnelNodes.Add(nextNode); currLength += sectionLength; } return tunnelNodes; } private void GenerateRuin(List mainPath, Level level, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); Point ruinSize = new Point( Rand.Range(ruinGenerationParams.SizeMin.X, ruinGenerationParams.SizeMax.X, Rand.RandSync.Server), Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server)); int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2; int cellIndex = Rand.Int(cells.Count, Rand.RandSync.Server); Point ruinPos = new Point((int)cells[cellIndex].Site.Coord.X, (int)cells[cellIndex].Site.Coord.X); //50% chance of placing the ruins at a cave if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) { TryGetInterestingPosition(true, PositionType.Cave, 0.0f, out ruinPos); } ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2); ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2); double minDist = ruinRadius * 2; double minDistSqr = minDist * minDist; int iter = 0; while (mainPath.Any(p => MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, p.Site.Coord.X, p.Site.Coord.Y) < minDistSqr) || ruins.Any(r => r.Area.Intersects(new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize)))) { double weighedPathPosX = ruinPos.X; double weighedPathPosY = ruinPos.Y; iter++; foreach (VoronoiCell pathCell in mainPath) { double diffX = ruinPos.X - pathCell.Site.Coord.X; double diffY = ruinPos.Y - pathCell.Site.Coord.Y; double distSqr = diffX * diffX + diffY * diffY; if (distSqr < 1.0) { diffX = 0; diffY = 1; distSqr = 1.0; } if (distSqr > 10000.0 * 10000.0) continue; double dist = Math.Sqrt(distSqr); double moveAmountX = 100.0 * diffX / dist; double moveAmountY = 100.0 * diffY / dist; weighedPathPosX += moveAmountX; weighedPathPosY += moveAmountY; weighedPathPosY = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPosY); } Rectangle ruinArea = new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize); foreach (Ruin otherRuin in ruins) { if (!otherRuin.Area.Intersects(ruinArea)) continue; double diffX = ruinArea.Center.X - otherRuin.Area.Center.X; double diffY = ruinArea.Center.Y - otherRuin.Area.Center.Y; double distSqr = diffX * diffX + diffY * diffY; if (distSqr < 0.01f) { diffX = 0; diffY = -1; distSqr = 1; } double dist = Math.Sqrt(distSqr); double moveAmountX = diffX / dist; double moveAmountY = diffY / dist; int move = (Math.Max(ruinArea.Width, ruinArea.Height) + Math.Max(otherRuin.Area.Width, otherRuin.Area.Height)) / 2; moveAmountX *= move; moveAmountY *= move; weighedPathPosX += moveAmountX; weighedPathPosY += moveAmountY; } ruinPos = new Point((int)weighedPathPosX, (int)weighedPathPosY); //if we can't find a suitable position after 10 000 iterations, give up if (iter > 10000) { if (ruins.Count > 0) { //we already have some ruins, don't add this one at all return; } string errorMsg = "Failed to find a suitable position for ruins. Level seed: " + seed + ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Name); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Level.GenerateRuins:PosNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); break; } //if we haven't found a position after 500 iterations, try another starting point else if (iter > 500 && iter % 500 == 0) { int newCellIndex = Rand.Int(cells.Count, Rand.RandSync.Server); ruinPos = new Point((int)cells[newCellIndex].Site.Coord.X, (int)cells[newCellIndex].Site.Coord.X); } ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2); ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2); } if (Math.Abs(ruinPos.X) > int.MaxValue / 2 || Math.Abs(ruinPos.Y) > int.MaxValue / 2) { DebugConsole.ThrowError("Something went wrong during ruin generation. Ruin position: " + ruinPos); return; } VoronoiCell closestPathCell = null; double closestDist = 0.0f; foreach (VoronoiCell pathCell in mainPath) { double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y); if (closestPathCell == null || dist < closestDist) { closestPathCell = pathCell; closestDist = dist; } } var ruin = new Ruin(closestPathCell, cells, ruinGenerationParams, new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize), mirror); ruins.Add(ruin); ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance)); int waypointCount = 0; foreach (WayPoint wp in WayPoint.WayPointList) { if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; } if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition))) { positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin)); waypointCount++; } } //not enough waypoints inside ruins -> create some spawn positions manually for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++) { positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin)); } foreach (RuinShape ruinShape in ruin.RuinShapes) { var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height)); foreach (VoronoiCell cell in tooClose) { if (cell.CellType == CellType.Empty) continue; foreach (GraphEdge e in cell.Edges) { Rectangle rect = ruinShape.Rect; rect.Y += rect.Height; if (ruinShape.Rect.Contains(e.Point1) || ruinShape.Rect.Contains(e.Point2) || MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, rect, out _)) { cell.CellType = CellType.Removed; int x = (int)Math.Floor(cell.Center.X / GridCellSize); int y = (int)Math.Floor(cell.Center.Y / GridCellSize); cellGrid[x, y].Remove(cell); cells.Remove(cell); break; } } } } //cast a ray from the closest path cell towards the ruin and remove the cell it hits //to ensure that there's always at least one way from the main tunnel to the ruin List validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed); foreach (VoronoiCell cell in validCells) { foreach (GraphEdge e in cell.Edges) { if (MathUtils.LinesIntersect(closestPathCell.Center, ruinPos.ToVector2(), e.Point1, e.Point2)) { cell.CellType = CellType.Removed; int x = (int)Math.Floor(cell.Center.X / GridCellSize); int y = (int)Math.Floor(cell.Center.Y / GridCellSize); cellGrid[x, y].Remove(cell); cells.Remove(cell); break; } } if (cell.CellType == CellType.Removed) { break; } } } private void GenerateItems() { string levelName = generationParams.Name.ToLowerInvariant(); List> levelItems = new List>(); foreach (MapEntityPrefab mapEntityPrefab in MapEntityPrefab.List) { if (!(mapEntityPrefab is ItemPrefab itemPrefab)) { continue; } if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || itemPrefab.LevelCommonness.TryGetValue("", out commonness)) { levelItems.Add(new Pair(itemPrefab, commonness)); } } for (int i = 0; i < generationParams.ItemCount; i++) { var selectedPrefab = ToolBox.SelectWeightedRandom( levelItems.Select(it => it.First).ToList(), levelItems.Select(it => it.Second).ToList(), Rand.RandSync.Server); if (selectedPrefab == null) { break; } var selectedCell = cells[Rand.Int(cells.Count, Rand.RandSync.Server)]; var selectedEdge = selectedCell.Edges.GetRandom(e => e.IsSolid && !e.OutsideLevel, Rand.RandSync.Server); if (selectedEdge == null) continue; float edgePos = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); Vector2 selectedPos = Vector2.Lerp(selectedEdge.Point1, selectedEdge.Point2, edgePos); Vector2 edgeNormal = selectedEdge.GetNormal(selectedCell); var item = new Item(selectedPrefab, selectedPos, submarine: null); item.Move(edgeNormal * item.Rect.Height / 2); var holdable = item.GetComponent(); if (holdable == null) { DebugConsole.ThrowError("Error while placing items in the level - item \"" + item.Name + "\" is not holdable and cannot be attached to the level walls."); } else { holdable.AttachToWall(); #if CLIENT item.SpriteRotation = -MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2; #endif } } } public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f) { if (!positionsOfInterest.Any()) { return new Vector2(Size.X / 2, Size.Y / 2); } Vector2 position = Vector2.Zero; offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall); int tries = 0; do { Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos); startPos += Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); Vector2 endPos = startPos - Vector2.UnitY * Size.Y; if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), null, Physics.CollisionLevel | Physics.CollisionWall) != null) { position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; break; } tries++; if (tries == 10) { position = EndPosition - Vector2.UnitY * 300.0f; } } while (tries < 10); return position; } public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position) { bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos); position = pos.ToVector2(); return success; } public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position) { if (!positionsOfInterest.Any()) { position = new Point(Size.X / 2, Size.Y / 2); return false; } List suitablePositions = positionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType)); if (!suitablePositions.Any()) { string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace; GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif position = positionsOfInterest[Rand.Int(positionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; return false; } List farEnoughPositions = new List(suitablePositions); if (minDistFromSubs > 0.0f) { foreach (Submarine sub in Submarine.Loaded) { if (sub.IsOutpost) { continue; } farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs); } } if (!farEnoughPositions.Any()) { string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace; GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif float maxDist = 0.0f; position = suitablePositions.First().Position; foreach (InterestingPosition pos in suitablePositions) { float dist = Submarine.Loaded.Sum(s => Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f); if (dist > maxDist) { position = pos.Position; maxDist = dist; } } return false; } position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; return true; } public void Update(float deltaTime, Camera cam) { levelObjectManager.Update(deltaTime); foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { networkUpdateTimer += deltaTime; if (networkUpdateTimer > NetworkUpdateInterval) { if (extraWalls.Any(w => w.Body.BodyType != BodyType.Static)) { GameMain.NetworkMember.CreateEntityEvent(this); } networkUpdateTimer = 0.0f; } } #if CLIENT backgroundCreatureManager.Update(deltaTime, cam); WaterRenderer.Instance?.ScrollWater(Vector2.UnitY, (float)deltaTime); renderer.Update(deltaTime, cam); #endif } public Vector2 GetBottomPosition(float xPosition) { int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1)); if (index < 0 || index >= bottomPositions.Count - 1) return new Vector2(xPosition, BottomPos); float yPos = MathHelper.Lerp( bottomPositions[index].Y, bottomPositions[index + 1].Y, (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X)); return new Vector2(xPosition, yPos); } public List GetAllCells() { List cells = new List(); for (int x = 0; x < cellGrid.GetLength(0); x++) { for (int y = 0; y < cellGrid.GetLength(1); y++) { cells.AddRange(cellGrid[x, y]); } } return cells; } public List GetCells(Vector2 worldPos, int searchDepth = 2) { int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize); int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize); int startX = Math.Max(gridPosX - searchDepth, 0); int endX = Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); int startY = Math.Max(gridPosY - searchDepth, 0); int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); List cells = new List(); for (int y = startY; y <= endY; y++) { for (int x = startX; x <= endX; x++) { cells.AddRange(cellGrid[x, y]); } } foreach (LevelWall wall in extraWalls) { foreach (VoronoiCell cell in wall.Cells) { cells.Add(cell); } } return cells; } private void CreateOutposts() { var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost); if (outpostFiles.Count() == 0) { DebugConsole.ThrowError("No outpost files found in the selected content packages"); return; } for (int i = 0; i < 2; i++) { //no outposts at either side of the level when there's more than one main sub (combat missions) if (Submarine.MainSubs.Length > 1 && Submarine.MainSubs[0] != null && Submarine.MainSubs[1] != null) { continue; } //only create a starting outpost in campaign mode if (GameMain.GameSession?.GameMode as CampaignMode == null && ((i == 0) == !Mirrored)) { continue; } string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server); var outpost = new Submarine(outpostFile, tryLoad: false); outpost.Load(unloadPrevious: false); outpost.MakeOutpost(); Point? minSize = null; DockingPort subPort = null; if (Submarine.MainSub != null) { Point subSize = Submarine.MainSub.GetDockedBorders().Size; Point outpostSize = outpost.GetDockedBorders().Size; minSize = new Point(Math.Max(subSize.X, outpostSize.X), subSize.Y + outpostSize.Y); float closestDistance = float.MaxValue; foreach (DockingPort port in DockingPort.List) { if (port.IsHorizontal || port.Docked) { continue; } if (port.Item.Submarine != Submarine.MainSub) { continue; } //the submarine port has to be at the top of the sub if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; } float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X); if (dist < closestDistance) { subPort = port; closestDistance = dist; } } } float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; //don't try to compensate if the port is very far from the sub's center of mass if (Math.Abs(subDockingPortOffset) > 2000.0f) { subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -2000.0f, 2000.0f); string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); } outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset)); if ((i == 0) == !Mirrored) { StartOutpost = outpost; if (GameMain.GameSession?.StartLocation != null) { outpost.Name = GameMain.GameSession.StartLocation.Name; } } else { EndOutpost = outpost; if (GameMain.GameSession?.EndLocation != null) { outpost.Name = GameMain.GameSession.EndLocation.Name; } } } } public override void Remove() { base.Remove(); #if CLIENT if (renderer != null) { renderer.Dispose(); renderer = null; } #endif if (levelObjectManager != null) { levelObjectManager.Remove(); levelObjectManager = null; } if (ruins != null) { ruins.Clear(); ruins = null; } if (extraWalls != null) { foreach (LevelWall w in extraWalls) { w.Dispose(); } extraWalls = null; } cells = null; if (bodies != null) { bodies.Clear(); bodies = null; } loaded = null; } public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) { foreach (LevelWall levelWall in extraWalls) { if (levelWall.Body.BodyType == BodyType.Static) continue; msg.Write(levelWall.Body.Position.X); msg.Write(levelWall.Body.Position.Y); msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); } } } }