Beispiel #1
0
        public static bool GenerateSubWaypoints(Submarine submarine)
        {
            if (!Hull.hullList.Any())
            {
                DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found.");
                return(false);
            }

            List <WayPoint> existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path);

            foreach (WayPoint wayPoint in existingWaypoints)
            {
                wayPoint.Remove();
            }

            //find all open doors and temporarily activate their bodies to prevent visibility checks
            //from ignoring the doors and generating waypoint connections that go straight through the door
            List <Door> openDoors = new List <Door>();

            foreach (Item item in Item.ItemList)
            {
                var door = item.GetComponent <Door>();
                if (door != null && !door.Body.Enabled)
                {
                    openDoors.Add(door);
                    door.Body.Enabled = true;
                }
            }

            float diffFromHullEdge = 50;
            float minDist          = 100.0f;
            float heightFromFloor  = 110.0f;
            float hullMinHeight    = 100;

            foreach (Hull hull in Hull.hullList)
            {
                // Ignore hulls that a human couldn't fit in.
                // Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually.
                if (hull.Rect.Height < hullMinHeight)
                {
                    continue;
                }
                // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor.
                Body floor = null;
                for (int i = 0; i < 5; i++)
                {
                    float horizontalOffset = 0;
                    switch (i)
                    {
                    case 1:
                        horizontalOffset = hull.RectWidth * 0.2f;
                        break;

                    case 2:
                        horizontalOffset = hull.RectWidth * 0.4f;
                        break;

                    case 3:
                        horizontalOffset = -hull.RectWidth * 0.2f;
                        break;

                    case 4:
                        horizontalOffset = -hull.RectWidth * 0.4f;
                        break;
                    }
                    horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset);
                    Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
                    floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
                    if (floor != null)
                    {
                        break;
                    }
                }
                if (floor == null)
                {
                    continue;
                }
                float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
                if (hull.Rect.Width < diffFromHullEdge * 3.0f)
                {
                    new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
                }
                else
                {
                    WayPoint prevWaypoint = null;
                    for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
                    {
                        var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
                        if (prevWaypoint != null)
                        {
                            wayPoint.ConnectTo(prevWaypoint);
                        }
                        prevWaypoint = wayPoint;
                    }
                    if (prevWaypoint == null)
                    {
                        // Ensure that we always create at least one waypoint per hull.
                        new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
                    }
                }
            }

            // Platforms
            foreach (Structure platform in Structure.WallList)
            {
                if (!platform.IsPlatform)
                {
                    continue;
                }
                float    waypointHeight = heightFromFloor;
                WayPoint prevWaypoint   = null;
                for (float x = platform.Rect.X + diffFromHullEdge; x <= platform.Rect.Right - diffFromHullEdge; x += minDist)
                {
                    WayPoint wayPoint = new WayPoint(new Vector2(x, platform.Rect.Y + waypointHeight), SpawnType.Path, submarine);
                    if (prevWaypoint != null)
                    {
                        wayPoint.ConnectTo(prevWaypoint);
                    }
                    // If the waypoint is close to hull waypoints, remove it.
                    if (wayPoint != null)
                    {
                        for (int dir = -1; dir <= 1; dir += 2)
                        {
                            if (wayPoint.FindClosest(dir, horizontalSearch: true, tolerance: new Vector2(minDist, heightFromFloor), ignored: prevWaypoint.ToEnumerable()) != null)
                            {
                                wayPoint.Remove();
                                wayPoint = null;
                                break;
                            }
                        }
                    }
                    prevWaypoint = wayPoint;
                }
            }

            float outSideWaypointInterval = 100.0f;

            if (submarine.Info.Type != SubmarineType.OutpostModule)
            {
                List <(WayPoint, int)> outsideWaypoints = new List <(WayPoint, int)>();

                Rectangle borders        = Hull.GetBorders();
                int       originalWidth  = borders.Width;
                int       originalHeight = borders.Height;
                borders.X        -= Math.Min(500, originalWidth / 4);
                borders.Y        += Math.Min(500, originalHeight / 4);
                borders.Width    += Math.Min(1500, originalWidth / 2);
                borders.Height   += Math.Min(1000, originalHeight / 2);
                borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);

                if (borders.Width <= outSideWaypointInterval * 2)
                {
                    borders.Inflate(outSideWaypointInterval * 2 - borders.Width, 0);
                }

                if (borders.Height <= outSideWaypointInterval * 2)
                {
                    int inflateAmount = (int)(outSideWaypointInterval * 2) - borders.Height;
                    borders.Y      += inflateAmount / 2;
                    borders.Height += inflateAmount;
                }

                WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
                for (int i = 0; i < 2; i++)
                {
                    for (float x = borders.X + outSideWaypointInterval; x < borders.Right - outSideWaypointInterval; x += outSideWaypointInterval)
                    {
                        var wayPoint = new WayPoint(
                            new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
                            SpawnType.Path, submarine);

                        outsideWaypoints.Add((wayPoint, i));

                        if (x == borders.X + outSideWaypointInterval)
                        {
                            cornerWaypoint[i, 0] = wayPoint;
                        }
                        else
                        {
                            wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
                        }
                    }

                    cornerWaypoint[i, 1] = WayPointList[WayPointList.Count - 1];
                }

                for (int i = 0; i < 2; i++)
                {
                    WayPoint wayPoint = null;
                    for (float y = borders.Y - borders.Height; y < borders.Y; y += outSideWaypointInterval)
                    {
                        wayPoint = new WayPoint(
                            new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
                            SpawnType.Path, submarine);

                        outsideWaypoints.Add((wayPoint, i));

                        if (y == borders.Y - borders.Height)
                        {
                            wayPoint.ConnectTo(cornerWaypoint[1, i]);
                        }
                        else
                        {
                            wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
                        }
                    }

                    wayPoint.ConnectTo(cornerWaypoint[0, i]);
                }

                Vector2 center     = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition);
                float   halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2);
                // Try to move the waypoints so that they are near the walls, roughly following the shape of the sub.
                foreach (var wayPoint in outsideWaypoints)
                {
                    WayPoint wp        = wayPoint.Item1;
                    float    xDiff     = center.X - wp.SimPosition.X;
                    Vector2  targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y);
                    Body     wall      = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
                    if (wall == null)
                    {
                        // Try again, and shoot to the center now. It happens with some subs that the first, offset raycast don't hit the walls.
                        targetPos = new Vector2(center.X - xDiff, center.Y);
                        wall      = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
                    }
                    if (wall != null)
                    {
                        float distanceFromWall = 1;
                        if (xDiff > 0 && !submarine.Info.HasTag(SubmarineTag.Shuttle))
                        {
                            // We don't want to move the waypoints near the tail too close to the engine.
                            float yDist = Math.Abs(center.Y - wp.SimPosition.Y);
                            distanceFromWall = MathHelper.Lerp(1, 3, MathUtils.InverseLerp(halfHeight, 0, yDist));
                        }
                        Vector2 newPos = Submarine.LastPickedPosition + Submarine.LastPickedNormal * distanceFromWall;
                        wp.rect = new Rectangle(ConvertUnits.ToDisplayUnits(newPos).ToPoint(), wp.rect.Size);
                        wp.FindHull();
                    }
                }
                // Remove unwanted points
                var      removals = new List <WayPoint>();
                WayPoint previous = null;
                float    tooClose = outSideWaypointInterval / 2;
                foreach (var wayPoint in outsideWaypoints)
                {
                    WayPoint wp = wayPoint.Item1;
                    if (wp.CurrentHull != null ||
                        Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null)
                    {
                        // Remove waypoints that got inside/too near the sub.
                        removals.Add(wp);
                        previous = wp;
                        continue;
                    }
                    foreach (var otherWayPoint in outsideWaypoints)
                    {
                        WayPoint otherWp = otherWayPoint.Item1;
                        if (otherWp == wp)
                        {
                            continue;
                        }
                        if (removals.Contains(otherWp))
                        {
                            continue;
                        }
                        float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position);
                        // Remove waypoints that are too close to each other.
                        if (!removals.Contains(previous) && sqrDist < tooClose * tooClose)
                        {
                            removals.Add(wp);
                        }
                    }
                    previous = wp;
                }
                foreach (WayPoint wp in removals)
                {
                    outsideWaypoints.RemoveAll(w => w.Item1 == wp);
                    wp.Remove();
                }
                for (int i = 0; i < outsideWaypoints.Count; i++)
                {
                    WayPoint current = outsideWaypoints[i].Item1;
                    if (current.linkedTo.Count > 1)
                    {
                        continue;
                    }
                    WayPoint next           = null;
                    int      maxConnections = 2;
                    float    tooFar         = outSideWaypointInterval * 5;
                    for (int j = 0; j < maxConnections; j++)
                    {
                        if (current.linkedTo.Count >= maxConnections)
                        {
                            break;
                        }
                        tooFar /= current.linkedTo.Count;
                        next    = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp.Item1 != next && wp.Item1.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.Item1.linkedTo.Count < 2 && wp.Item2 < i);
                        if (next != null)
                        {
                            current.ConnectTo(next);
                        }
                    }
                }
            }

            List <Structure> stairList = new List <Structure>();

            foreach (MapEntity me in mapEntityList)
            {
                if (!(me is Structure stairs))
                {
                    continue;
                }

                if (stairs.StairDirection != Direction.None)
                {
                    stairList.Add(stairs);
                }
            }

            foreach (Structure stairs in stairList)
            {
                WayPoint[] stairPoints = new WayPoint[3];

                stairPoints[0] = new WayPoint(
                    new Vector2(stairs.Rect.X - 32.0f,
                                stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);

                stairPoints[1] = new WayPoint(
                    new Vector2(stairs.Rect.Right + 32.0f,
                                stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);

                for (int i = 0; i < 2; i++)
                {
                    for (int dir = -1; dir <= 1; dir += 2)
                    {
                        WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70));
                        if (closest == null)
                        {
                            continue;
                        }
                        stairPoints[i].ConnectTo(closest);
                    }
                }

                stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
                stairPoints[0].ConnectTo(stairPoints[2]);
                stairPoints[2].ConnectTo(stairPoints[1]);
            }

            foreach (Item item in Item.ItemList)
            {
                var ladders = item.GetComponent <Ladder>();
                if (ladders == null)
                {
                    continue;
                }

                Vector2         bottomPoint  = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10);
                List <WayPoint> ladderPoints = new List <WayPoint>
                {
                    new WayPoint(bottomPoint, SpawnType.Path, submarine),
                };

                List <Body> ignoredBodies = new List <Body>();
                // Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either.
                // Start point is where the bots normally grasp the ladder when they stand on ground.
                WayPoint lowestPoint = ladderPoints[0];
                WayPoint prevPoint   = lowestPoint;
                Vector2  prevPos     = prevPoint.SimPosition;
                Body     ground      = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies,
                                                          collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform | Physics.CollisionStairs,
                                                          customPredicate: f => !(f.Body.UserData is Submarine));
                float startHeight = ground != null?ConvertUnits.ToDisplayUnits(ground.Position.Y) : bottomPoint.Y;

                startHeight += heightFromFloor;
                WayPoint startPoint = lowestPoint;
                Vector2  nextPos    = new Vector2(item.Rect.Center.X, startHeight);
                // Don't create the start point if it's too close to the lowest point or if it's outside of the sub.
                // If we skip creating the start point, the lowest point is used instead.
                if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null)
                {
                    startPoint = new WayPoint(nextPos, SpawnType.Path, submarine);
                    ladderPoints.Add(startPoint);
                    if (lowestPoint != null)
                    {
                        startPoint.ConnectTo(lowestPoint);
                    }
                    prevPoint = startPoint;
                    prevPos   = prevPoint.SimPosition;
                }
                for (float y = startPoint.Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
                {
                    //first check if there's a door in the way
                    //(we need to create a waypoint linked to the door for NPCs to open it)
                    Body pickedBody = Submarine.PickBody(
                        ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)),
                        prevPos, ignoredBodies, Physics.CollisionWall, false,
                        (Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent <Door>() != null);

                    Door pickedDoor = null;
                    if (pickedBody != null)
                    {
                        pickedDoor = (pickedBody?.UserData as Item).GetComponent <Door>();
                    }
                    else
                    {
                        //no door, check for walls
                        pickedBody = Submarine.PickBody(
                            ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false,
                            (Fixture f) => f.Body.UserData is Structure);
                    }

                    if (pickedBody == null)
                    {
                        prevPos = Submarine.LastPickedPosition;
                        continue;
                    }
                    else
                    {
                        ignoredBodies.Add(pickedBody);
                    }

                    if (pickedDoor != null)
                    {
                        WayPoint newPoint = new WayPoint(pickedDoor.Item.Position, SpawnType.Path, submarine);
                        ladderPoints.Add(newPoint);
                        newPoint.ConnectedGap = pickedDoor.LinkedGap;
                        newPoint.ConnectTo(prevPoint);
                        prevPoint = newPoint;
                        prevPos   = new Vector2(prevPos.X, ConvertUnits.ToSimUnits(pickedDoor.Item.Position.Y - pickedDoor.Item.Rect.Height));
                    }
                    else
                    {
                        WayPoint newPoint = new WayPoint(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * heightFromFloor, SpawnType.Path, submarine);
                        ladderPoints.Add(newPoint);
                        newPoint.ConnectTo(prevPoint);
                        prevPoint = newPoint;
                        prevPos   = ConvertUnits.ToSimUnits(newPoint.Position);
                    }
                }

                // Cap
                if (prevPoint.rect.Y < item.Rect.Y - 40)
                {
                    WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
                    ladderPoints.Add(wayPoint);
                    wayPoint.ConnectTo(prevPoint);
                }

                // Connect ladder waypoints to hull points at the right and left side
                foreach (WayPoint ladderPoint in ladderPoints)
                {
                    ladderPoint.Ladders = ladders;
                    bool isHatch = ladderPoint.ConnectedGap != null && !ladderPoint.ConnectedGap.IsRoomToRoom;
                    for (int dir = -1; dir <= 1; dir += 2)
                    {
                        WayPoint closest = null;
                        if (isHatch)
                        {
                            closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderPoints);
                        }
                        else
                        {
                            closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 100), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints);
                        }
                        if (closest == null)
                        {
                            continue;
                        }
                        ladderPoint.ConnectTo(closest);
                    }
                }
            }

            // Another pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders)
            foreach (Item item in Item.ItemList)
            {
                var ladders = item.GetComponent <Ladder>();
                if (ladders == null)
                {
                    continue;
                }
                var      wps   = WayPointList.Where(wp => wp.Ladders == ladders).OrderByDescending(wp => wp.Rect.Y);
                WayPoint cap   = wps.First();
                WayPoint above = cap.FindClosest(1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
                above?.ConnectTo(cap);
                WayPoint bottom = wps.Last();
                WayPoint below  = bottom.FindClosest(-1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
                below?.ConnectTo(bottom);
            }

            foreach (Gap gap in Gap.GapList)
            {
                if (gap.IsHorizontal)
                {
                    // Too small to walk through
                    if (gap.Rect.Height < hullMinHeight)
                    {
                        continue;
                    }
                    Vector2 pos      = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
                    var     wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
                    // The closest waypoint can be quite far if the gap is at an exterior door.
                    Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000);
                    for (int dir = -1; dir <= 1; dir += 2)
                    {
                        WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
                        if (closest != null)
                        {
                            wayPoint.ConnectTo(closest);
                        }
                    }
                }
                else
                {
                    // Create waypoints on vertical gaps on the outer walls, also hatches.
                    if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))
                    {
                        continue;
                    }
                    // Too small to swim through
                    if (gap.Rect.Width < 50.0f)
                    {
                        continue;
                    }
                    Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
                    // Some hatches are created in the block above where we handle the ladder waypoints. So we need to check for duplicates.
                    if (WayPointList.Any(wp => wp.ConnectedGap == gap))
                    {
                        continue;
                    }
                    var      wayPoint      = new WayPoint(pos, SpawnType.Path, submarine, gap);
                    Hull     connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
                    int      dir           = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
                    WayPoint closest       = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100));
                    if (closest != null)
                    {
                        wayPoint.ConnectTo(closest);
                    }
                    for (dir = -1; dir <= 1; dir += 2)
                    {
                        closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null);
                        if (closest != null)
                        {
                            wayPoint.ConnectTo(closest);
                        }
                    }
                }
            }

            var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && w.linkedTo.None());

            foreach (WayPoint wp in orphans)
            {
                wp.Remove();
            }

            foreach (WayPoint wp in WayPointList)
            {
                if (wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2)
                {
                    DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color.");
                }
            }

            //re-disable the bodies of the doors that are supposed to be open
            foreach (Door door in openDoors)
            {
                door.Body.Enabled = false;
            }

            return(true);
        }