using Barotrauma.Items.Components; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Xml.Linq; namespace Barotrauma { public enum SpawnType { Path, Human, Enemy, Cargo }; partial class WayPoint : MapEntity { public static List WayPointList = new List(); public static bool ShowWayPoints = true, ShowSpawnPoints = true; protected SpawnType spawnType; //characters spawning at the waypoint will be given an ID card with these tags private string idCardDesc; private string[] idCardTags; //only characters with this job will be spawned at the waypoint private JobPrefab assignedJob; private Hull currentHull; private ushort ladderId; public Ladder Ladders; public Structure Stairs; private ushort gapId; public Gap ConnectedGap { get; private set; } public Door ConnectedDoor { get { return ConnectedGap?.ConnectedDoor; } } public Hull CurrentHull { get { return currentHull; } } public SpawnType SpawnType { get { return spawnType; } set { spawnType = value; } } public override string Name { get { return spawnType == SpawnType.Path ? "WayPoint" : "SpawnPoint"; } } public string IdCardDesc { get { return idCardDesc; } private set { idCardDesc = value; } } public string[] IdCardTags { get { return idCardTags; } private set { idCardTags = value; for (int i = 0; i < idCardTags.Length; i++) { idCardTags[i] = idCardTags[i].Trim().ToLowerInvariant(); } } } public WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap = null) : this(new Rectangle((int)position.X - 3, (int)position.Y + 3, 6, 6), submarine) { this.spawnType = spawnType; ConnectedGap = gap; } public WayPoint(MapEntityPrefab prefab, Rectangle rectangle) : this (rectangle, Submarine.MainSub) { if (prefab.Name.Contains("Spawn")) { spawnType = SpawnType.Human; } else { SpawnType = SpawnType.Path; } } public WayPoint(Rectangle newRect, Submarine submarine) : this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine) { } public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine) : base (prefab, submarine) { rect = newRect; linkedTo = new ObservableCollection(); idCardTags = new string[0]; #if CLIENT if (iconTexture == null) { iconTexture = Sprite.LoadTexture("Content/Map/waypointIcons.png"); } #endif InsertToList(); WayPointList.Add(this); currentHull = Hull.FindHull(WorldPosition); } public override MapEntity Clone() { var clone = new WayPoint(rect, Submarine) { idCardDesc = idCardDesc, idCardTags = idCardTags, spawnType = spawnType, assignedJob = assignedJob }; return clone; } public override bool IsMouseOn(Vector2 position) { #if CLIENT if (IsHidden()) return false; #endif return base.IsMouseOn(position); } public static void GenerateSubWaypoints(Submarine submarine) { if (!Hull.hullList.Any()) { DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found."); return; } List 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 openDoors = new List(); foreach (Item item in Item.ItemList) { var door = item.GetComponent(); if (door != null && !door.Body.Enabled) { openDoors.Add(door); door.Body.Enabled = true; } } float minDist = 150.0f; float heightFromFloor = 110.0f; foreach (Hull hull in Hull.hullList) { if (hull.Rect.Height < 150) continue; WayPoint prevWaypoint = null; if (hull.Rect.Width < minDist * 3.0f) { new WayPoint( new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine); continue; } for (float x = hull.Rect.X + minDist; x <= hull.Rect.Right - minDist; x += minDist) { var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine); if (prevWaypoint != null) wayPoint.ConnectTo(prevWaypoint); prevWaypoint = wayPoint; } } float outSideWaypointInterval = 200.0f; int outsideWaypointDist = 100; Rectangle borders = Hull.GetBorders(); borders.X -= outsideWaypointDist; borders.Y += outsideWaypointDist; borders.Width += outsideWaypointDist * 2; borders.Height += outsideWaypointDist * 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); 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); if (y == borders.Y - borders.Height) { wayPoint.ConnectTo(cornerWaypoint[1, i]); } else { wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]); } } wayPoint.ConnectTo(cornerWaypoint[0, i]); } List stairList = new List(); foreach (MapEntity me in mapEntityList) { Structure stairs = me as Structure; if (stairs == null) 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, true, new Vector2(-30.0f,30f)); 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(); if (ladders == null) continue; List ladderPoints = new List(); ladderPoints.Add(new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine)); WayPoint prevPoint = ladderPoints[0]; Vector2 prevPos = prevPoint.SimPosition; List ignoredBodies = new List(); for (float y = ladderPoints[0].Position.Y + 100.0f; y < item.Rect.Y - 1.0f; y += 100.0f) { //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(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, Physics.CollisionWall, false, (Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent() != null); Door pickedDoor = null; if (pickedBody != null) { pickedDoor = (pickedBody?.UserData as Item).GetComponent(); } else { //no door, check for walls pickedBody = Submarine.PickBody( ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false); } 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); } } if (prevPoint.rect.Y < item.Rect.Y - 10.0f) { WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine); ladderPoints.Add(newPoint); newPoint.ConnectTo(prevPoint); } //connect ladder waypoints to hull points at the right and left side foreach (WayPoint ladderPoint in ladderPoints) { ladderPoint.Ladders = ladders; //don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch) if (ladderPoint.ConnectedGap != null) continue; for (int dir = -1; dir <= 1; dir += 2) { WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 10f)); if (closest == null) continue; ladderPoint.ConnectTo(closest); } } } foreach (Gap gap in Gap.GapList) { if (!gap.IsHorizontal) continue; //too small to walk through if (gap.Rect.Height < 150.0f) continue; var wayPoint = new WayPoint( new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap); for (int dir = -1; dir <= 1; dir += 2) { float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f; WayPoint closest = wayPoint.FindClosest( dir, true, new Vector2(-tolerance, tolerance), gap.ConnectedDoor == null ? null : gap.ConnectedDoor.Body.FarseerBody); if (closest != null) { wayPoint.ConnectTo(closest); } } } foreach (Gap gap in Gap.GapList) { if (gap.IsHorizontal || gap.IsRoomToRoom) continue; //too small to walk through if (gap.Rect.Width < 100.0f) continue; var wayPoint = new WayPoint( new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap); } var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && !w.linkedTo.Any()); foreach (WayPoint wp in orphans) { wp.Remove(); } //re-disable the bodies of the doors that are supposed to be open foreach (Door door in openDoors) { door.Body.Enabled = false; } } private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null) { if (dir != -1 && dir != 1) return null; float closestDist = 0.0f; WayPoint closest = null; foreach (WayPoint wp in WayPointList) { if (wp.SpawnType != SpawnType.Path || wp == this) continue; float diff = 0.0f; if (horizontalSearch) { if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) continue; diff = wp.Position.X - Position.X; } else { if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) continue; diff = wp.Position.Y - Position.Y; } if (Math.Sign(diff) != dir) continue; float dist = Vector2.Distance(wp.Position, Position); if (closest == null || dist < closestDist) { var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false); if (body != null && body != ignoredBody && !(body.UserData is Submarine)) { if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue; } closestDist = dist; closest = wp; } } return closest; } private void ConnectTo(WayPoint wayPoint2) { System.Diagnostics.Debug.Assert(this != wayPoint2); if (!linkedTo.Contains(wayPoint2)) linkedTo.Add(wayPoint2); if (!wayPoint2.linkedTo.Contains(this)) wayPoint2.linkedTo.Add(this); } public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, bool useSyncedRand = false) { List wayPoints = new List(); foreach (WayPoint wp in WayPointList) { if (sub != null && wp.Submarine != sub) continue; if (wp.spawnType != spawnType) continue; if (assignedJob != null && wp.assignedJob != assignedJob.Prefab) continue; wayPoints.Add(wp); } if (!wayPoints.Any()) return null; return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))]; } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) { List subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine); List unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human); WayPoint[] assignedWayPoints = new WayPoint[crew.Count]; for (int i = 0; i < crew.Count; i++ ) { //try to give the crew member a spawnpoint that hasn't been assigned to anyone and matches their job for (int n = 0; n < unassignedWayPoints.Count; n++) { if (crew[i].Job.Prefab != unassignedWayPoints[n].assignedJob) continue; assignedWayPoints[i] = unassignedWayPoints[n]; unassignedWayPoints.RemoveAt(n); break; } } //go through the crewmembers that don't have a spawnpoint yet (if any) for (int i = 0; i < crew.Count; i++) { if (assignedWayPoints[i] != null) continue; //try to assign a spawnpoint that matches the job, even if the spawnpoint is already assigned to someone else foreach (WayPoint wp in subWayPoints) { if (wp.spawnType != SpawnType.Human || wp.assignedJob != crew[i].Job.Prefab) continue; assignedWayPoints[i] = wp; break; } if (assignedWayPoints[i] != null) continue; //try to assign a spawnpoint that isn't meant for any specific job var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.assignedJob == null); if (nonJobSpecificPoints.Any()) { assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)]; } if (assignedWayPoints[i] != null) continue; //everything else failed -> just give a random spawnpoint inside the sub assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true); } for (int i = 0; i < assignedWayPoints.Length; i++) { if (assignedWayPoints[i] == null) { DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!"); assignedWayPoints[i] = WayPointList[0]; } } return assignedWayPoints; } public override void OnMapLoaded() { currentHull = Hull.FindHull(WorldPosition, currentHull); if (gapId > 0) ConnectedGap = FindEntityByID(gapId) as Gap; if (ladderId > 0) { var ladderItem = FindEntityByID(ladderId) as Item; if (ladderItem != null) Ladders = ladderItem.GetComponent(); } Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionWall | Physics.CollisionStairs); if (pickedBody != null && pickedBody.UserData is Structure) { Structure structure = (Structure)pickedBody.UserData; if (structure != null && structure.StairDirection != Direction.None) { Stairs = structure; } } } public static WayPoint Load(XElement element, Submarine submarine) { Rectangle rect = new Rectangle( int.Parse(element.Attribute("x").Value), int.Parse(element.Attribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine) { ID = (ushort)int.Parse(element.Attribute("ID").Value) }; w.spawnType = spawnType; string idCardDescString = element.GetAttributeString("idcarddesc", ""); if (!string.IsNullOrWhiteSpace(idCardDescString)) { w.IdCardDesc = idCardDescString; } string idCardTagString = element.GetAttributeString("idcardtags", ""); if (!string.IsNullOrWhiteSpace(idCardTagString)) { w.IdCardTags = idCardTagString.Split(','); } string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(jobIdentifier)) { w.assignedJob = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == jobIdentifier) ?? JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == jobIdentifier); } w.ladderId = (ushort)element.GetAttributeInt("ladders", 0); w.gapId = (ushort)element.GetAttributeInt("gap", 0); w.linkedToID = new List(); int i = 0; while (element.Attribute("linkedto" + i) != null) { w.linkedToID.Add((ushort)int.Parse(element.Attribute("linkedto" + i).Value)); i += 1; } return w; } public override XElement Save(XElement parentElement) { if (!ShouldBeSaved) return null; XElement element = new XElement("WayPoint"); element.Add(new XAttribute("ID", ID), new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), new XAttribute("spawn", spawnType)); if (!string.IsNullOrWhiteSpace(idCardDesc)) element.Add(new XAttribute("idcarddesc", idCardDesc)); if (idCardTags.Length > 0) { element.Add(new XAttribute("idcardtags", string.Join(",", idCardTags))); } if (assignedJob != null) element.Add(new XAttribute("job", assignedJob.Identifier)); if (ConnectedGap != null) element.Add(new XAttribute("gap", ConnectedGap.ID)); if (Ladders != null) element.Add(new XAttribute("ladders", Ladders.Item.ID)); parentElement.Add(element); if (linkedTo != null) { int i = 0; foreach (MapEntity e in linkedTo) { if (!e.ShouldBeSaved) continue; element.Add(new XAttribute("linkedto" + i, e.ID)); i += 1; } } return element; } public override void ShallowRemove() { base.ShallowRemove(); WayPointList.Remove(this); } public override void Remove() { base.Remove(); WayPointList.Remove(this); } } }