Repository: endless-sky/endless-sky
Pull request #8621: Feat(mechanics): Reviving NPC Travel Instructions (Part 1)
Author: login: mOctave
Labels: name: enhancement
description: Issues asking for or PRs making code changes that add new capabilities to the engine
Created: 2023-04-01T23:50:54Z | Merged: 2025-05-17T22:48:54Z

**Feature:** This PR is a redo of about half of https://github.com/endless-sky/endless-sky/pull/2990

## Feature Details
Almost all the code is tehhowch's, I just fixed merge conflicts removed everything survey and patrol related, and fixed bugs until it worked with modern ES.

This has the destination, stopover and waypoint code from #2990.

I have made a couple of changes to the original, including allowing both stopovers and a destination, limiting `ShipEvent::LAND` to only the final destination, and adding an `outrun` criterion that causes mission failure if the ship lands at its final destination.

## UI Screenshots
N/A

## Usage Examples
### Waypoint
The `waypoint` keyword assigns a system or set of systems to the ships in the NPC block. These systems will be travelled to, if they are accessible, in the order they are specified (though LocationFilter systems are created after all non-LocationFilter waypoints). If a system is inaccessible, it is removed in-flight. If no value is specified, the mission destination system is used. This keywords can utilize LocationFilters to randomly select the waypoints. If the distance argument for LocationFilters is used, it is sequential, e.g. the distance is measured relative to the previous waypoint. All methods can be used together to specify the NPC's waypoints.  
Upon reaching the final system in the list, the NPCs will behave as normal escorts would. The staying personality takes effect when the NPC no longer has waypoints to visit.

<details><summary>Usage</summary>
<pre>
waypoint (no value given)

waypoint <system 1> ... <system N>

waypoint <system 1> <system 2>
waypoint ...
waypoint <system N>

waypoint
    LocationFilter 1, line 1
    ...
    LocationFilter 1, line N
waypoint
    LocationFilter 2, line 1
    ...
    LocationFilter 2, line N
...
</pre>
</details>

### Destination/Stopover
The `destination` and `stopover` keywords assign a planet to the ships in the NPC block. `stopover` can also be used to assign a set of planets. These planets will be landed on, if they are accessible, if the NPC enters the system that contains the planet. If the keyword was `destination`, the ship will not take off again from the planet - it has landed permanently. If the keyword was `stopover`, the ship will not force itself to land on that planet again.  
A ship which has landed at its destination will (if this destination is also the mission destination) permanently satisfy the `accompany` success criterion, and will satisfy the `land` criterion even if the destination is not the mission destination. 
As with the `waypoint` keyword, passing no value results in the mission's destination being used as the target. LocationFilters can similarly be used to randomly select the landing targets.

<details><summary>Usage</summary>
<pre>
destination/stopover (no value given)

stopover <planet 1> ... <planet N>

stopover <planet 1> <planet 2>
stopover ...
stopover <planet N>

destination <planet>

destination/stopover
    LocationFilter 1, line 1
    ...
    LocationFilter 1, line N
stopover
    LocationFilter 2, line 1
    ...
    LocationFilter 2, line N
...
</pre>
</details> 

Note: `destination` and `stopover` will _not_ cause the NPC to fly to the systems its destination or stopover is in. Doing this requires the use of the `waypoint` keyword.

### Land/Outrun
The use of the `land` keyword as a success criterion puts the NPC in a success state when it lands on its final destination planet. This can be paired with other NPC success criteria. If there is no destination specified, a warning is thrown.

The `outrun` keyword acts as the opposite of `land`, putting the NPC in a failure state when it lands on its final destination.

<details><summary>Usage</summary>
<pre>
npc land/outrun
    (npc specification)
    destination (as above)
</pre>
</details> 

## Testing Done
I used the missions in the [test file](https://github.com/endless-sky/endless-sky/files/1271292/npc_travel_to.txt) to test it. I'm still not sure it's working.

### Automated Tests Added
With @Hurleveur's help, I've added a test that runs a mission using the `land`, `destination`, and `waypoint` features and checks to see if it works.

## Performance Impact
Possibly some minor impact, nothing noticeable.



Diff:
diff --git a/source/AI.cpp b/source/AI.cpp
index c2a372edfba5..7185e4ff53fb 100644
--- a/source/AI.cpp
+++ b/source/AI.cpp
@@ -944,6 +944,12 @@ void AI::Step(Command &activeCommands)
 				continue;
 			}
 		}
+		// Update any orders NPCs may have been given by their associated mission.
+		else if(it->IsSpecial() && !it->IsYours() && it->HasTravelDirective())
+		{
+			const Planet *destination = it->AllStopoversVisited() ? it->GetDestinationPlanet() : nullptr;
+			IssueNPCOrders(*it, destination);
+		}
 
 		// This ship may have updated its target ship.
 		double targetDistance = numeric_limits<double>::infinity();
@@ -1707,14 +1713,16 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 
 	int type = it->second.type;
+	const bool hasTravelOrder = type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON;
+
 
 	// Ships without an (alive) parent don't follow orders.
 	shared_ptr<Ship> parent = ship.GetParent();
 	if(!parent)
 		return false;
 	// If your parent is jumping or absent, that overrides your orders unless
-	// your orders are to hold position.
-	if(parent && type != Orders::HOLD_POSITION && type != Orders::HOLD_ACTIVE && type != Orders::MOVE_TO)
+	// your orders are to hold position, or a travel directive.
+	if(parent && type != Orders::HOLD_POSITION && type != Orders::HOLD_ACTIVE && !hasTravelOrder)
 	{
 		if(parent->GetSystem() != ship.GetSystem())
 			return false;
@@ -1728,8 +1736,16 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 	}
 
+
 	shared_ptr<Ship> target = it->second.target.lock();
 	shared_ptr<Minable> targetAsteroid = it->second.targetAsteroid.lock();
+	if(type == Orders::LAND_ON && it->second.targetPlanet)
+	{
+		// LAND_ON would not be issued unless the planet was in this system.
+		ship.SetTargetStellar(ship.GetSystem()->FindStellar(it->second.targetPlanet));
+		it->second.type = Orders::LAND_ON;
+		MoveIndependent(ship, command);
+	}
 	if(type == Orders::MOVE_TO && it->second.targetSystem && ship.GetSystem() != it->second.targetSystem)
 	{
 		// The desired position is in a different system. Find the best
@@ -2037,7 +2053,9 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 	else if(ship.GetTargetStellar())
 	{
 		MoveToPlanet(ship, command);
-		if(!shouldStay && ship.Attributes().Get("fuel capacity") && ship.GetTargetStellar()->HasSprite()
+		// Ships should land on their destination planet if they are free to
+		// move about, or have a travel directive indicating they should land.
+		if(!(shouldStay && ship.Attributes().Get("fuel capacity")) && ship.GetTargetStellar()->HasSprite()
 				&& ship.GetTargetStellar()->GetPlanet() && ship.GetTargetStellar()->GetPlanet()->CanLand(ship))
 			command |= Command::LAND;
 		else if(ship.Position().Distance(ship.GetTargetStellar()->Position()) < 100.)
@@ -2112,9 +2130,10 @@ void AI::MoveEscort(Ship &ship, Command &command)
 		// If the ship has no destination or the destination is unreachable, route to the parent's system.
 		if(!ship.GetTargetStellar() && (!ship.GetTargetSystem() || !ship.JumpNavigation().JumpFuel(ship.GetTargetSystem())))
 		{
-			// Route to the parent ship's system and check whether
-			// the ship should land (refuel or wormhole) or jump.
-			SelectRoute(ship, parent.GetSystem());
+			// Route to the destination (either the parent ship's system or a system
+			// marked by the NPC's mission definition) by landing or jumping.
+			const System *destinationSystem = ship.GetDestinationSystem();
+			SelectRoute(ship, destinationSystem ? destinationSystem : parent.GetSystem());
 		}
 
 		// Perform the action that this ship previously decided on.
@@ -5079,3 +5098,58 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const Planet *destination)
+{
+	Orders newOrders;
+	const System *targetSystem = ship.GetDestinationSystem();
+	const map<const Planet *, bool> &stopovers = ship.GetStopovers();
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	for(const auto &it : stopovers)
+		if(!it.second && it.first->IsInSystem(from))
+		{
+			newOrders.type = Orders::LAND_ON;
+			newOrders.targetPlanet = it.first;
+			break;
+		}
+
+	// Update the NPC's orders.
+	Orders &existing = orders[&ship];
+	if(!newOrders.targetSystem && newOrders.type != Orders::LAND_ON)
+		orders.erase(&ship);
+	else
+		existing = newOrders;
+}
diff --git a/source/AI.h b/source/AI.h
index f187cba5a000..45b952cc1a67 100644
--- a/source/AI.h
+++ b/source/AI.h
@@ -34,6 +34,7 @@ class Body;
 class Flotsam;
 class Government;
 class Minable;
+class Planet;
 class PlayerInfo;
 class Ship;
 class ShipEvent;
@@ -188,10 +189,13 @@ template <class Type>
 		// Hold active is the same command as hold position, but it is given when a ship
 		// actively needs to move back to the position it was holding.
 		static const int HOLD_ACTIVE = 0x001;
+		// Travel orders
 		static const int MOVE_TO = 0x002;
+		static const int TRAVEL_TO = 0x003;
+		static const int LAND_ON = 0x004;
 		// HARVEST is related to MINE and is for picking up flotsam after
 		// ATTACK.
-		static const int HARVEST = 0x003;
+		static const int HARVEST = 0x005;
 		static const int KEEP_STATION = 0x100;
 		static const int GATHER = 0x101;
 		static const int ATTACK = 0x102;
@@ -208,6 +212,7 @@ template <class Type>
 		std::weak_ptr<Minable> targetAsteroid;
 		Point point;
 		const System *targetSystem = nullptr;
+		const Planet *targetPlanet = nullptr;
 	};
 
 
@@ -215,6 +220,8 @@ template <class Type>
 	void IssueOrders(const Orders &newOrders, const std::string &description);
 	// Convert order types based on fulfillment status.
 	void UpdateOrders(const Ship &ship);
+	// Mission NPC blocks may define specific travel plans.
+	void IssueNPCOrders(Ship &ship, const Planet *destination);
 
 
 private:
@@ -246,8 +253,8 @@ template <class Type>
 	// The minimum speed before landing will consider non-landable objects.
 	const float MIN_LANDING_VELOCITY = 80.;
 
-	// Current orders for the player's ships. Because this map only applies to
-	// player ships, which are never deleted except when landed, it can use
+	// Current orders for the player's ships or NPCs. Because this map only applies
+	// to special ships, which are never deleted except when landed, it can use
 	// ordinary pointers instead of weak pointers.
 	std::map<const Ship *, Orders> orders;
 
diff --git a/source/Engine.cpp b/source/Engine.cpp
index 338e8c7208b8..8e2d000d2a4d 100644
--- a/source/Engine.cpp
+++ b/source/Engine.cpp
@@ -441,8 +441,8 @@ void Engine::Place(const list<NPC> &npcs, const shared_ptr<Ship> &flagship)
 		map<string, map<Ship *, int>> carriers;
 		for(const shared_ptr<Ship> &ship : npc.Ships())
 		{
-			// Skip ships that have been destroyed.
-			if(ship->IsDestroyed() || ship->IsDisabled())
+			// Skip ships that have been destroyed, have landed, or are disabled.
+			if(ship->LandedOrDestroyed() || ship->IsDisabled())
 				continue;
 
 			// Redo the loading up of fighters.
@@ -462,8 +462,8 @@ void Engine::Place(const list<NPC> &npcs, const shared_ptr<Ship> &flagship)
 		shared_ptr<Ship> npcFlagship;
 		for(const shared_ptr<Ship> &ship : npc.Ships())
 		{
-			// Skip ships that have been destroyed.
-			if(ship->IsDestroyed())
+			// Skip ships that have been destroyed or permanently landed.
+			if(ship->LandedOrDestroyed())
 				continue;
 
 			// Avoid the exploit where the player can wear down an NPC's
@@ -1913,11 +1913,11 @@ void Engine::MoveShip(const shared_ptr<Ship> &ship)
 	ship->Move(newVisuals, newFlotsam);
 	if(ship->IsDisabled() && !wasDisabled)
 		eventQueue.emplace_back(nullptr, ship, ShipEvent::DISABLE);
-	// Bail out if the ship just died.
+	// Ships which are dead or landed should report that event.
 	if(ship->ShouldBeRemoved())
 	{
-		// Make sure this ship's destruction was recorded, even if it died from
-		// self-destruct.
+		// Make sure this ship's destruction or landing was recorded, even if
+		// it died from self-destruct.
 		if(ship->IsDestroyed())
 		{
 			eventQueue.emplace_back(nullptr, ship, ShipEvent::DESTROY);
@@ -1929,6 +1929,8 @@ void Engine::MoveShip(const shared_ptr<Ship> &ship)
 			if(ship->IsYours())
 				player.DeselectShip(ship.get());
 		}
+		else if(ship->HasLanded() && ship->IsSpecial())
+			eventQueue.emplace_back(nullptr, ship, ShipEvent::LAND);
 		return;
 	}
 
diff --git a/source/Mission.cpp b/source/Mission.cpp
index 2d3f40fc9c7b..c14b08ca24f9 100644
--- a/source/Mission.cpp
+++ b/source/Mission.cpp
@@ -1606,7 +1606,7 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr<Ship> &b
 		return result;
 	}
 	for(const NPC &npc : npcs)
-		result.npcs.push_back(npc.Instantiate(player, subs, sourceSystem, result.destination->GetSystem(), jumps, payload));
+		result.npcs.push_back(npc.Instantiate(player, subs, sourceSystem, result.destination, jumps, payload));
 
 	// Instantiate the actions. The "complete" action is always first so that
 	// the "<payment>" substitution can be filled in.
diff --git a/source/NPC.cpp b/source/NPC.cpp
index a2f78f99d2df..8fcab400c6e5 100644
--- a/source/NPC.cpp
+++ b/source/NPC.cpp
@@ -97,6 +97,10 @@ void NPC::Load(const DataNode &node, const ConditionsStore *playerConditions)
 			succeedIf |= ShipEvent::SCAN_CARGO;
 		else if(node.Token(i) == "scan outfits")
 			succeedIf |= ShipEvent::SCAN_OUTFITS;
+		else if(node.Token(i) == "outrun")
+			failIf |= ShipEvent::LAND;
+		else if(node.Token(i) == "land")
+			succeedIf |= ShipEvent::LAND;
 		else if(node.Token(i) == "capture")
 			succeedIf |= ShipEvent::CAPTURE;
 		else if(node.Token(i) == "provoke")
@@ -112,6 +116,8 @@ void NPC::Load(const DataNode &node, const ConditionsStore *playerConditions)
 	// Check for incorrect objective combinations.
 	if(failIf & ShipEvent::DESTROY && (succeedIf & ShipEvent::DESTROY || succeedIf & ShipEvent::CAPTURE))
 		node.PrintTrace("Error: conflicting NPC mission objective to save and destroy or capture.");
+	if(succeedIf & ShipEvent::LAND && failIf & ShipEvent::LAND)
+		node.PrintTrace("Error: conflicting NPC mission objective to land and outrun.");
 	if(mustEvade && mustAccompany)
 		node.PrintTrace("Warning: NPC mission objective to accompany and evade is synonymous with kill.");
 	if(mustEvade && (succeedIf & ShipEvent::DESTROY || succeedIf & ShipEvent::CAPTURE))
@@ -134,6 +140,61 @@ void NPC::Load(const DataNode &node, const ConditionsStore *playerConditions)
 			else
 				location.Load(child);
 		}
+		else if(key == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				waypointFilters.emplace_back(child);
+		}
+		else if(key == "destination")
+		{
+			// Only the last defined "destination" node is used to determine the final
+			// destination of the NPC. This destination is visited only after all stopovers
+			// have been, and landing on it completes the mission. If no planet
+			// is passed to the node, the mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "destination" or "destination <planet>"
+				if(child.Size() == 1)
+					missingDestination = true;
+				else
+					finalDestination = GameData::Planets().Get(child.Token(1));
+			}
+			// Given "destination" and a location filter. These get processed during NPC instantiation.
+			else
+				destinationFilter.Load(child);
+		}
+		else if(key == "stopover")
+		{
+			// Stopovers can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "stopover" nodes. All stopovers are visited (no permanent
+			// landing on the stopovers). If no planet is passed to the node, the
+			// mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "stopover" or "destination <planet 1> ... <planet N>"
+				if(child.Size() == 1)
+					missingStopover = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						stopovers.push_back(GameData::Planets().Get(child.Token(i)));
+			}
+			// Given "stopover" and child nodes. These get processed during NPC instantiation.
+			else
+				stopoverFilters.emplace_back(child);
+		}
 		else if(key == "uuid" && hasValue)
 			uuid = EsUuid::FromString(child.Token(1));
 		else if(key == "planet" && hasValue)
@@ -276,6 +337,22 @@ void NPC::Load(const DataNode &node, const ConditionsStore *playerConditions)
 		ship->SetPersonality(personality);
 		ship->SetIsSpecial();
 		ship->FinishLoading(false);
+		if(!waypoints.empty())
+			ship->SetWaypoints(waypoints);
+		if(!stopovers.empty())
+			ship->SetStopovers(stopovers);
+		if(finalDestination)
+			ship->SetDestination(finalDestination);
+	}
+
+
+	// NPCs given the "land" or "outrun" completion conditions should also have a destination.
+	if(!finalDestination && !missingDestination && destinationFilter.IsEmpty())
+	{
+		if(succeedIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to land is impossible without a destination.");
+		else if(failIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to outrun is impossible without a destination.");
 	}
 }
 
@@ -332,6 +409,25 @@ void NPC::Save(DataWriter &out) const
 			out.Write("government", government->GetTrueName());
 		personality.Save(out);
 
+		if(!waypoints.empty())
+		{
+			out.WriteToken("waypoint");
+			for(const auto &waypoint : waypoints)
+				out.WriteToken(waypoint->TrueName());
+			out.Write();
+		}
+
+		if(finalDestination)
+			out.Write("destination", finalDestination->TrueName());
+
+		if(!stopovers.empty())
+		{
+			out.WriteToken("stopover");
+			for(const auto &stopover : stopovers)
+				out.WriteToken(stopover->TrueName());
+			out.Write();
+		}
+
 		if(!dialogText.empty())
 		{
 			out.Write("dialog");
@@ -621,9 +717,17 @@ bool NPC::HasFailed() const
 		if(it.second & failIf)
 			return true;
 
-		// If we still need to perform an action on this NPC, then that ship
-		// being destroyed should cause the mission to fail.
-		if((~it.second & succeedIf) && (it.second & ShipEvent::DESTROY))
+		// If we still need to perform an action that requires the NPC ship be
+		// alive, then that ship being destroyed or landed causes the mission to fail.
+		if((~it.second & succeedIf) && (it.second & (ShipEvent::DESTROY | ShipEvent::LAND)))
+			return true;
+
+		// If this ship has landed permanently, the NPC has failed if
+		// 1) it must accompany and is not in the destination system, or
+		// 2) it must evade, and is in the destination system.
+		if((it.second & ShipEvent::LAND) && it.first->GetPlanet() == finalDestination && it.first->GetSystem()
+				&& ((mustAccompany && it.first->GetSystem() != destination)
+				|| (mustEvade && it.first->GetSystem() == destination)))
 			return true;
 	}
 
@@ -632,12 +736,21 @@ bool NPC::HasFailed() const
 
 
 
+// Check if the NPC has the "land" success criterion.
+bool NPC::SucceedsOnLanding() const
+{
+	return succeedIf & ShipEvent::LAND;
+}
+
+
+
 // Create a copy of this NPC but with the fleets replaced by the actual
 // ships they represent, wildcards in the conversation text replaced, etc.
 NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const System *origin,
-		const System *destination, int jumps, int64_t payload) const
+		const Planet *destinationPlanet, int jumps, int64_t payload) const
 {
 	NPC result;
+	result.destination = destinationPlanet->GetSystem();
 	result.government = government;
 	if(!result.government)
 		result.government = GameData::PlayerGovernment();
@@ -646,6 +759,9 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 	result.failIf = failIf;
 	result.mustEvade = mustEvade;
 	result.mustAccompany = mustAccompany;
+	result.waypoints = waypoints;
+	result.finalDestination = finalDestination;
+	result.stopovers = stopovers;
 
 	result.passedSpawnConditions = passedSpawnConditions;
 	result.toSpawn = toSpawn;
@@ -675,6 +791,39 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
+
+	if(missingWaypoint)
+		result.waypoints.push_back(result.destination);
+	for(const LocationFilter &filter : waypointFilters)
+	{
+		const System *choice = filter.PickSystem(result.waypoints.empty() ?
+				origin : result.waypoints.back());
+		if(choice)
+			result.waypoints.push_back(choice);
+	}
+
+	if(missingStopover)
+		result.stopovers.push_back(destinationPlanet);
+	for(const LocationFilter &filter : stopoverFilters)
+	{
+		// Each new stopover's "distance X Y" is calculated from the previous one's system.
+		const Planet *choice = filter.PickPlanet(result.stopovers.empty() ?
+				origin : result.stopovers.back()->GetSystem(), true);
+		if(choice)
+			result.stopovers.push_back(choice);
+	}
+
+	if(missingDestination)
+		result.finalDestination = destinationPlanet;
+	// The destination's "distance X Y" is calculated from the last stopover.
+	if(!destinationFilter.IsEmpty())
+	{
+		const Planet *choice = destinationFilter.PickPlanet(result.stopovers.empty() ?
+				origin : result.stopovers.back()->GetSystem(), true);
+		if(choice)
+			result.finalDestination = choice;
+	}
+
 	// If a planet was specified in the template, it must be in this system.
 	if(planet && result.system->FindStellar(planet))
 		result.planet = planet;
@@ -707,6 +856,13 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 		if(result.personality.IsDerelict())
 			ship->Disable();
 
+		if(!result.waypoints.empty())
+			ship->SetWaypoints(result.waypoints);
+		if(!result.stopovers.empty())
+			ship->SetStopovers(result.stopovers);
+		if(result.finalDestination)
+			ship->SetDestination(result.finalDestination);
+
 		if(personality.IsEntering())
 			Fleet::Enter(*result.system, *ship);
 		else if(result.planet)
diff --git a/source/NPC.h b/source/NPC.h
index ffe0cedacf85..4564b20ffd6e 100644
--- a/source/NPC.h
+++ b/source/NPC.h
@@ -29,6 +29,7 @@ this program. If not, see <https://www.gnu.org/licenses/>.
 #include <map>
 #include <memory>
 #include <string>
+#include <vector>
 
 class ConditionsStore;
 class DataNode;
@@ -107,11 +108,13 @@ class NPC {
 	// Determine if the NPC is in a failed state. A failed state is irrecoverable, except for
 	// NPCs which would despawn upon the player's next landing.
 	bool HasFailed() const;
+	// Determine if the NPC needs to land in order for successful mission completion.
+	bool SucceedsOnLanding() const;
 
 	// Create a copy of this NPC but with the fleets replaced by the actual
 	// ships they represent, wildcards in the conversation text replaced, etc.
 	NPC Instantiate(const PlayerInfo &player, std::map<std::string, std::string> &subs, const System *origin,
-			const System *destination, int jumps, int64_t payload) const;
+			const Planet *destinationPlanet, int jumps, int64_t payload) const;
 
 
 private:
@@ -133,7 +136,20 @@ class NPC {
 	// Start out in a location matching this filter, or in a particular system:
 	LocationFilter location;
 	const System *system = nullptr;
+	const System *destination = nullptr;
 	bool isAtDestination = false;
+
+	// NPCs may have been given waypoints, stopovers, or a destination.
+	std::vector<const System *> waypoints;
+	std::vector<const Planet *> stopovers;
+	const Planet *finalDestination = nullptr;
+	bool missingWaypoint = false;
+	bool missingStopover = false;
+	bool missingDestination = false;
+	std::list<LocationFilter> waypointFilters;
+	std::list<LocationFilter> stopoverFilters;
+	LocationFilter destinationFilter;
+
 	// Start out landed on this planet.
 	const Planet *planet = nullptr;
 
diff --git a/source/PlayerInfo.cpp b/source/PlayerInfo.cpp
index 454e87793d85..44973306c2ad 100644
--- a/source/PlayerInfo.cpp
+++ b/source/PlayerInfo.cpp
@@ -4114,13 +4114,22 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetDestinationPlanet() == planet
+					&& npc.SucceedsOnLanding() && ship->AllStopoversVisited())
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
+			}
 
 	// Check missions for status changes from landing.
 	string visitText;
diff --git a/source/Ship.cpp b/source/Ship.cpp
index 1bf0b78019f3..83be3070ab15 100644
--- a/source/Ship.cpp
+++ b/source/Ship.cpp
@@ -570,6 +570,8 @@ void Ship::Load(const DataNode &node, const ConditionsStore *playerConditions)
 		}
 		else if(key == "destination system" && hasValue)
 			targetSystem = GameData::Systems().Get(child.Token(1));
+		else if(key == "waypoint index" && child.Size() >= 2)
+			waypoint = child.Value(1);
 		else if(key == "parked")
 			isParked = true;
 		else if(key == "description" && hasValue)
@@ -1165,6 +1167,8 @@ void Ship::Save(DataWriter &out) const
 			out.Write("planet", landingPlanet->TrueName());
 		if(targetSystem)
 			out.Write("destination system", targetSystem->TrueName());
+		if(waypoint > 0 && waypoint <= waypoints.size())
+			out.Write("waypoint index", waypoint);
 		if(isParked)
 			out.Write("parked");
 	}
@@ -1813,7 +1817,7 @@ shared_ptr<Ship> Ship::Board(bool autoPlunder, bool nonDocking)
 	hasBoarded = false;
 
 	shared_ptr<Ship> victim = GetTargetShip();
-	if(CannotAct(Ship::ActionType::BOARD) || !victim || victim->IsDestroyed() || victim->GetSystem() != GetSystem())
+	if(CannotAct(Ship::ActionType::BOARD) || !victim || victim->LandedOrDestroyed() || victim->GetSystem() != GetSystem())
 		return shared_ptr<Ship>();
 
 	// For a fighter or drone, "board" means "return to ship." Except when the ship is
@@ -2587,10 +2591,33 @@ bool Ship::IsDestroyed() const
 
 
 
-// Recharge and repair this ship (e.g. because it has landed).
+void Ship::LandForever()
+{
+	hasLanded = true;
+}
+
+
+
+// Check if this ship has permanently landed.
+bool Ship::HasLanded() const
+{
+	return hasLanded;
+}
+
+
+
+// Check if this ship has permanently landed or been destroyed.
+bool Ship::LandedOrDestroyed() const
+{
+	return (IsDestroyed() || HasLanded());
+}
+
+
+
+// Recharge and repair this ship (e.g. because it has landed temporarily).
 void Ship::Recharge(int rechargeType, bool hireCrew)
 {
-	if(IsDestroyed())
+	if(LandedOrDestroyed())
 		return;
 
 	if(hireCrew)
@@ -2733,6 +2760,11 @@ void Ship::ClearTargetsAndOrders()
 	targetFlotsam.reset();
 	hyperspaceSystem = nullptr;
 	landingPlanet = nullptr;
+	destinationSystem = nullptr;
+	destinationPlanet = nullptr;
+	stopovers.clear();
+	waypoints.clear();
+	waypoint = 0;
 }
 
 
@@ -3752,6 +3784,45 @@ const System *Ship::GetTargetSystem() const
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || !waypoints.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	return all_of(stopovers.begin(), stopovers.end(), [](const auto &it) { return it.second; });
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
 // Mining target.
 shared_ptr<Minable> Ship::GetTargetAsteroid() const
 {
@@ -3824,6 +3895,66 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &newStopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : newStopovers)
+		stopovers[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const vector<const System *> &newWaypoints)
+{
+	// Ships loaded from save files may have an existing waypoint that
+	// indicates which systems have already been visited.
+	if(waypoint < newWaypoints.size())
+	{
+		waypoints = newWaypoints;
+		if(targetSystem && personality.IsEntering())
+		{
+			--waypoint;
+			destinationSystem = targetSystem;
+		}
+		else
+			destinationSystem = newWaypoints[waypoint];
+	}
+	else
+		destinationSystem = nullptr;
+}
+
+
+
+const System *Ship::NextWaypoint()
+{
+	++waypoint;
+
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+	return destinationSystem;
+}
+
+
+
+void Ship::EraseWaypoint(const System *system)
+{
+	auto it = std::find(waypoints.begin(), waypoints.end(), system);
+	if(it == waypoints.end())
+		return;
+	if(waypoint > static_cast<unsigned>(std::distance(waypoints.begin(), it)))
+		--waypoint;
+	waypoints.erase(it);
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+}
+
+
 // Mining target.
 void Ship::SetTargetAsteroid(const shared_ptr<Minable> &asteroid)
 {
@@ -4618,8 +4749,9 @@ bool Ship::DoLandingLogic()
 
 	float landingSpeed = attributes.Get("landing speed");
 	landingSpeed = landingSpeed > 0 ? landingSpeed : .02f;
-	// Special ships do not disappear forever when they land; they
-	// just slowly refuel.
+	// Special ships do not disappear forever when they land; they just slowly refuel.
+	// Exception: mission NPCs given the "destination" directive will be removed
+	// when they land on their target.
 	if(landingPlanet && zoom)
 	{
 		// Move the ship toward the center of the planet while landing.
@@ -4641,7 +4773,7 @@ bool Ship::DoLandingLogic()
 				SetTargetSystem(nullptr);
 				landingPlanet = nullptr;
 			}
-			else if(!isSpecial || personality.IsFleeing())
+			else if(isSpecial && !isYours)
 			{
 				bool escortsLanded = true;
 				for(const auto &it : escorts)
@@ -4654,7 +4786,23 @@ bool Ship::DoLandingLogic()
 					break;
 				}
 				if(escortsLanded)
-					MarkForRemoval();
+				{
+					// This mission NPC has a directive to land on at least one specific planet.
+					// If this is one of them, this ship may land on a "destination" (permanently),
+					// or have a "stopover."
+					if(AllStopoversVisited() && destinationPlanet == landingPlanet)
+					{
+						MarkForRemoval();
+						LandForever();
+					return true;
+					}
+					else if(!stopovers.empty())
+					{
+						auto it = stopovers.find(landingPlanet);
+						if(it != stopovers.end())
+							it->second = true;
+					}
+				}
 				return true;
 			}
 
@@ -5249,3 +5397,9 @@ void Ship::Jettison(shared_ptr<Flotsam> toJettison)
 		++bayIndex;
 	}
 }
+
+void Ship::ResetStopovers()
+{
+	for(auto &stopover : stopovers)
+		stopover.second = false;
+}
diff --git a/source/Ship.h b/source/Ship.h
index e9e818a95f82..15ac9a56128f 100644
--- a/source/Ship.h
+++ b/source/Ship.h
@@ -337,6 +337,11 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	bool IsDamaged() const;
 	// Check if this ship has been destroyed.
 	bool IsDestroyed() const;
+	// Land/Check if this ship has permanently landed.
+	void LandForever();
+	bool HasLanded() const;
+	// Check if this ship has permanently landed or been destroyed.
+	bool LandedOrDestroyed() const;
 	// Recharge and repair this ship (e.g. because it has landed).
 	void Recharge(int rechargeType = Port::RechargeType::All, bool hireCrew = true);
 	// Check if this ship is able to give the given ship enough fuel to jump.
@@ -502,6 +507,14 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	const StellarObject *GetTargetStellar() const;
 	// Get ship's target system (it should always be one jump / wormhole pass away).
 	const System *GetTargetSystem() const;
+	// Targets for persistent ships (i.e. mission NPCs).
+	bool HasTravelDirective() const;
+	const std::map<const Planet *, bool> &GetStopovers() const;
+	bool AllStopoversVisited() const;
+	const Planet *GetDestinationPlanet() const;
+	const System *GetDestinationSystem() const;
+	bool ContinueAfterDestination() const;
+
 	// Mining target.
 	std::shared_ptr<Minable> GetTargetAsteroid() const;
 	std::shared_ptr<Flotsam> GetTargetFlotsam() const;
@@ -518,6 +531,13 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	void SetTargetStellar(const StellarObject *object);
 	// Set ship's target system (it should always be one jump / wormhole pass away).
 	void SetTargetSystem(const System *system);
+	// Persistent targets associated with mission NPCs.
+	void SetDestination(const Planet *destination);
+	void SetStopovers(const std::vector<const Planet *> &stopovers);
+	void SetWaypoints(const std::vector<const System *> &waypoints);
+	const System *NextWaypoint();
+	void EraseWaypoint(const System *system);
+
 	// Mining target.
 	void SetTargetAsteroid(const std::shared_ptr<Minable> &asteroid);
 	void SetTargetFlotsam(const std::shared_ptr<Flotsam> &flotsam);
@@ -591,6 +611,9 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	// Helper function for jettisoning flotsam.
 	void Jettison(std::shared_ptr<Flotsam> toJettison);
 
+	// Mark all stopovers as incomplete.
+	void ResetStopovers();
+
 
 private:
 	// Protected member variables of the Body class:
@@ -620,7 +643,8 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	int forget = 0;
 	bool isInSystem = true;
 	// "Special" ships cannot be forgotten, and if they land on a planet, they
-	// continue to exist and refuel instead of being deleted.
+	// continue to exist and refuel instead of being deleted, unless explicitly
+	// designed to do so by a mission's NPC specification.
 	bool isSpecial = false;
 	bool isYours = false;
 	bool isParked = false;
@@ -637,6 +661,7 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	bool neverDisabled = false;
 	bool isCapturable = true;
 	bool isInvisible = false;
+	bool hasLanded = false;
 	const Swizzle *customSwizzle = nullptr;
 	std::string customSwizzleName;
 	double cloak = 0.;
@@ -764,6 +789,17 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	std::set<const Flotsam *> tractorFlotsam;
 	const FormationPattern *formationPattern = nullptr;
 
+	// NPC travel directives
+	const System *destinationSystem = nullptr;
+	// The list of consecutive NPC destination systems.
+	std::vector<const System *> waypoints;
+	size_t waypoint = 0;
+	// The list of stopover planets this NPC should try to land on, and if
+	// they have already been landed on in this sequence.
+	std::map<const Planet *, bool> stopovers;
+	// The final destination of this NPC after it has landed on all its stopovers.
+	const Planet *destinationPlanet = nullptr;
+
 	// Links between escorts and parents.
 	std::vector<std::weak_ptr<Ship>> escorts;
 	std::weak_ptr<Ship> parent;
diff --git a/source/ShipEvent.h b/source/ShipEvent.h
index a235e57fbeba..aa3e09a595d7 100644
--- a/source/ShipEvent.h
+++ b/source/ShipEvent.h
@@ -68,6 +68,8 @@ class ShipEvent {
 		JUMP = (1 << 9),
 		// This ship just met another ship for the first time
 		ENCOUNTER = (1 << 10),
+		// This ship has permanently landed on a planet.
+		LAND = (1 << 11),
 	};
 
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1533ed4d67fb..b16424ac3101 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -65,6 +65,7 @@ list(APPEND INTEGRATION_TESTS
 	integration/config/plugins/integration-tests/data/tests/tests_illegal_atrocity.txt
 	integration/config/plugins/integration-tests/data/tests/tests_job_board_missions.txt
 	integration/config/plugins/integration-tests/data/tests/tests_landing.txt
+	integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
 	integration/config/plugins/integration-tests/data/tests/tests_take_ships.txt
 	integration/config/plugins/integration-tests/data/tests/tests_save_load.txt
 	integration/config/plugins/integration-tests/data/tests/tests_ship_sale.txt
diff --git a/tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt b/tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
new file mode 100644
index 000000000000..4e459c24a70e
--- /dev/null
+++ b/tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
@@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
+#
+# Endless Sky is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later version.
+#
+# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <https://www.gnu.org/licenses/>.
+
+test-data "NPC Destination Save"
+	category "savegame"
+	contents
+		pilot Bobbi Bughunter
+		date 16 11 3013
+		system Sol
+		planet Earth
+		clearance
+		# Set some reputations to positive to avoid combat.
+		"reputation with"
+			Bounty 1
+			"Bounty Hunter" 1
+			Pirate 1
+		# What you own:
+		ship "Star Barge"
+			name "Buggy Barge"
+			sprite "ship/star barge"
+			attributes
+				category "Light Freighter"
+				cost 190000
+				mass 70
+				bunks 3
+				"cargo space" 50
+				drag 2.1
+				"engine capacity" 400
+				"fuel capacity" 800
+				"heat dissipation" 0.8
+				hull 1000
+				"outfit space" 1300
+				"required crew" 1
+				shields 600
+				"turret mounts" 1
+				"weapon capacity" 200
+			outfits
+				"X1700 Ion Thruster"
+				"X1200 Ion Steering"
+				Hyperdrive
+				"Anti-Missile Turret"
+				"D14-RN Shield Generator"
+				"nGVF-BB Fuel Cell"
+				"LP036a Battery Pack"
+			crew 1
+			fuel 300
+			shields 600
+			hull 1000
+			engine -9 38 1
+			engine 9 38 1
+			turret 0 -8 "Anti-Missile Turret"
+			leak leak 60 50
+			explode "tiny explosion" 10
+			explode "small explosion" 10
+			system Sol
+			planet Earth
+		account
+			credits 131000
+			score 400
+			history
+		visited Sol
+		"visited planet" Earth
+		visited "Sirius"
+		visited "Procyon"
+		visited "Pollux"
+		visited "Castor"
+
+test-data "NPC Destination Mission"
+	category mission
+	contents
+		mission "npc"
+			landing
+			destination "Geminus"
+			npc land save
+				ship "Quarg Wardragon"
+				government "Quarg"
+				destination Geminus
+				waypoint
+				personality waiting
+
+
+test "NPC Destination Test"
+	status active
+	description "Starts the mission, accepts it and then goes to the destination making sure the Quarg got there first."
+	sequence
+		inject "NPC Destination Mission"
+		inject "NPC Destination Save"
+		call "Load First Savegame"
+		# Set desired travel plan.
+		navigate
+			travel Castor
+			travel Pollux
+			travel Procyon
+			travel Sirius
+			"travel destination" Geminus
+		call "Depart"
+		# Trigger the mission.
+		call "Land"
+		call "Depart"
+		input
+			command jump
+		label "moving"
+		branch "moving"
+			"flagship landed" != 1
+		call "Depart"
+		assert
+			"npc: offered" == 1
+			"npc: done" == 1
+			"flagship system: Castor" == 1


Reviews:
user: login: Hurleveur
body: I took a quick look but this looks promising!
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2023-07-01T20:00:55Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-02T01:24:06Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-02T01:25:16Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-06T03:57:27Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-06T04:03:17Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-06T04:05:15Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-06T04:10:34Z
user: login: Hurleveur
state: COMMENTED
author_association: MEMBER
submitted_at: 2023-07-06T05:34:31Z
user: login: Hurleveur
state: COMMENTED
author_association: MEMBER
submitted_at: 2023-07-21T06:06:01Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-07-21T17:43:33Z
user: login: Hurleveur
body: some more comments!

is it intentional that if you have stopovers but dont give their systems as waypoints the ship wont head there but would just land if they happen to be in the right system?
if its not mb we should add them automatically to the waypoints (I could have missed smth, too)
state: COMMENTED
author_association: MEMBER
submitted_at: 2023-10-02T17:41:03Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-10-06T00:57:31Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-10-06T00:59:30Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-10-06T01:13:00Z
user: login: Hurleveur
state: COMMENTED
author_association: MEMBER
submitted_at: 2023-10-06T05:22:25Z
user: login: Hurleveur
body: Some more comments as requested, the PR is almost there
About the destroyed function I'm not sure if its used for other things than skipping AI and such but if it isnt we could rename it even and not require haslanded
state: COMMENTED
author_association: MEMBER
submitted_at: 2023-12-01T08:46:38Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-12-02T19:27:21Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2023-12-02T19:41:41Z
user: login: Hurleveur
body: Looking good to me code wise, although a bit more testing never hurts
state: APPROVED
author_association: MEMBER
submitted_at: 2023-12-02T21:30:47Z
user: login: TomGoodIdea
body: I'll try to test this in a few days
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-04-22T17:09:28Z
user: login: TomGoodIdea
body: Did some manual testing - everything seems ok, but found this (not sure if it's just outdated PR description or a real problem): `npc land` doesn't work with stopovers.
If it's not intended, you need to change (in `Engine::MoveShip`) when `ShipEvent::LAND` is posted. Currently, the mission gets the event only for ships that have landed permanently.

Also when you make a mission with `npc land` but without specifying the NPC's destination/stopovers, the mission is impossible to complete. Again, I think it's because the mission receives no `ShipEvent::LAND` for the NPC. In this case, I'd be fine with leaving it as is, just add a warning.
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-01T22:31:05Z
user: login: TomGoodIdea
body: This is weird. I made a test `npc land` without any destination, but with a single stopover different from the mission's destination. When I landed on the stopover while the NPC was in different system, nothing happened. However, if the NPC was in the same system, the `land` objective succeeded and the NPC was removed, but only if my ship landed before the NPC. If the NPC landed first, it just refueled and took off.

I guess the problem is more complex than it seemed before.

Also, is it intended that NPCs can't have both a destination and stopovers?
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-03T07:14:30Z
user: login: TomGoodIdea
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-09T21:41:41Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-05-09T23:03:43Z
user: login: TomGoodIdea
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-09T23:14:08Z
user: login: TomGoodIdea
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-10T12:21:08Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-05-10T17:49:30Z
user: login: TomGoodIdea
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-05-13T12:31:43Z
user: login: TomGoodIdea
body: Didn't found any new bugs while testing this today, so I think it's finally ready for merging.

Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2024-05-13T15:49:23Z
user: login: warp-core
body: Has anyone tested how these instructions interact with travel restrictions?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-05-13T17:37:52Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-05-13T23:59:22Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-05-14T00:01:17Z
user: login: RisingLeaf
body: more review later, all that I found on a skim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-23T06:12:35Z
user: login: TheGiraffe3
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-07-23T07:18:23Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-23T18:20:23Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-23T18:21:42Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-23T18:27:05Z
user: login: RisingLeaf
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-30T08:46:18Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-30T17:33:16Z
user: login: RisingLeaf
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-31T07:05:42Z
user: login: RisingLeaf
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-31T07:06:36Z
user: login: RisingLeaf
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2024-07-31T07:06:49Z
user: login: TheGiraffe3
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-09-20T03:12:14Z
user: login: TheGiraffe3
state: APPROVED
author_association: MEMBER
submitted_at: 2024-09-20T03:17:49Z
user: login: warp-core
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-10T23:36:34Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T21:40:33Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T21:52:51Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T22:06:24Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T22:12:40Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T22:49:23Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T22:53:55Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-11T22:54:46Z
user: login: warp-core
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-16T15:36:50Z
user: login: warp-core
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-16T15:39:31Z
user: login: warp-core
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-16T15:45:03Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-16T15:56:20Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-17T02:33:50Z
user: login: mOctave
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T03:54:41Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:09:52Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:10:55Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:12:12Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:15:45Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:17:23Z
user: login: petervdmeer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-17T22:22:27Z
user: login: petervdmeer
body: The main functionality from this PR seems to be working and seems well reviewed over time (since the first version in 2017). There are still some small opens for this PR, but I feel that this PR is good to go as-is. (The minor open issues can go to a follow-up PR.)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-17T22:32:38Z
user: login: Amazinite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-06-02T19:35:31Z
user: login: Amazinite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-06-04T02:24:41Z

Review comments:
diff_hunk: @@ -1673,6 +1685,7 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 		}
 	}
 
+
path: source/AI.cpp
user: login: Hurleveur
body: ```suggestion
```
created_at: 2023-07-01T19:43:41Z
author_association: MEMBER
diff_hunk: @@ -1763,18 +1776,27 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 
 	if(ship.GetTargetSystem())
 	{
-		PrepareForHyperspace(ship, command);
-		// Issuing the JUMP command prompts the escorts to get ready to jump.
-		command |= Command::JUMP;
-		// Issuing the WAIT command will prevent this parent from jumping.
-		// When all its non-carried, in-system escorts that are not disabled and
-		// have the ability to jump are ready, the WAIT command will be omitted.
-		if(!EscortsReadyToJump(ship))
-			command |= Command::WAIT;
+		// Refuel if able to now, but unable to in the destination system.
path: source/AI.cpp
user: login: Hurleveur
body: I feel like this small section is just going back to a previous version and is unrelated to the rest?
created_at: 2023-07-01T19:44:52Z
author_association: MEMBER
diff_hunk: @@ -1838,9 +1860,9 @@ void AI::MoveEscort(Ship &ship, Command &command) const
 		// If the ship has no destination or the destination is unreachable, route to the parent's system.
 		if(!ship.GetTargetStellar() && (!ship.GetTargetSystem() || !ship.JumpNavigation().JumpFuel(ship.GetTargetSystem())))
 		{
-			// Route to the parent ship's system and check whether
-			// the ship should land (refuel or wormhole) or jump.
-			SelectRoute(ship, parent.GetSystem());
+			// Route to the destination (either the parent ship's system or a system
+			// marked by the NPC's mission definition) by landing or jumping.
+			SelectRoute(ship, ship.GetDestinationSystem() ? ship.GetDestinationSystem() : parent.GetSystem());
path: source/AI.cpp
user: login: Hurleveur
body: I'd put the destination system in a variable
created_at: 2023-07-01T19:46:02Z
author_association: MEMBER
diff_hunk: @@ -4568,3 +4590,51 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, patrol, visit, land) to define travel plans.
path: source/AI.cpp
user: login: Hurleveur
body: You said you didnt include patrol in the description, right?
created_at: 2023-07-01T19:46:36Z
author_association: MEMBER
diff_hunk: @@ -179,6 +180,8 @@ template <class Type>
 		// HARVEST is related to MINE and is for picking up flotsam after
 		// ATTACK.
 		static const int HARVEST = 0x003;
+		static const int TRAVEL_TO = 0x004;
+		static const int LAND_ON = 0x005;
path: source/AI.h
user: login: Hurleveur
body: Are these commands that make sense to assign to hotkeys? Then again mb we dont have a choice
I mean we do currently give it when right clicking on the map of a system but idk if that allows landing, is this giving new funcionalities? (Not half bad either, just thinking)
What keys are these anyway?
created_at: 2023-07-01T19:49:54Z
author_association: MEMBER
diff_hunk: @@ -195,13 +198,16 @@ template <class Type>
 		std::weak_ptr<Minable> targetAsteroid;
 		Point point;
 		const System *targetSystem = nullptr;
+		const Planet *targetPlanet = nullptr;
 	};
 
 
 private:
 	void IssueOrders(const PlayerInfo &player, const Orders &newOrders, const std::string &description);
 	// Convert order types based on fulfillment status.
 	void UpdateOrders(const Ship &ship);
+	// NPC commands from mission events.
path: source/AI.h
user: login: Hurleveur
body: This ought to be the same comment than in the cpp, I think
created_at: 2023-07-01T19:51:10Z
author_association: MEMBER
diff_hunk: @@ -407,8 +407,8 @@ void Engine::Place(const list<NPC> &npcs, shared_ptr<Ship> flagship)
 		map<string, map<Ship *, int>> carriers;
 		for(const shared_ptr<Ship> &ship : npc.Ships())
 		{
-			// Skip ships that have been destroyed.
-			if(ship->IsDestroyed() || ship->IsDisabled())
+			// Skip ships that have been destroyed or have landed
+			if(ship->IsDestroyed() || ship->IsDisabled() || ship->HasLanded())
path: source/Engine.cpp
user: login: Hurleveur
body: Just a thought but this prob would be good as an optimisation for fleeing ships and others that landed too
Prob best for another PR though
created_at: 2023-07-01T19:52:37Z
author_association: MEMBER
diff_hunk: @@ -461,8 +461,7 @@ bool MainPanel::ShowHailPanel()
 		// If the target is out of system, always report a generic response
 		// because the player has no way of telling if it's presently jumping or
 		// not. If it's in system and jumping, report that.
-		if(target->Zoom() < 1. || target->IsDestroyed() || target->GetSystem() != player.GetSystem()
-				|| target->Cloaking() == 1.)
+		if(!target->IsTargetable() || target->GetSystem() != player.GetSystem())
path: source/MainPanel.cpp
user: login: Hurleveur
body: Good change I think but unrelated?
created_at: 2023-07-01T19:54:02Z
author_association: MEMBER
diff_hunk: @@ -128,6 +130,46 @@ void NPC::Load(const DataNode &node, const string &missionName)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
+		else if(child.Token(0) == "land" || child.Token(0) == "visit")
+		{
+			// Stopovers can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "visit" / "land" nodes. If any nodes are "visit", all
+			// stopovers are visited (no permanent landing on the stopovers). If no
+			// planet is passed to the node, the mission's destination will be used.
+			doVisit |= child.Token(0) == "visit";
path: source/NPC.cpp
user: login: Hurleveur
body: Not sure but shouldnt this be called stopovers for consistency? (Caus if it was different on purpose then waypoint would have to be as well)
created_at: 2023-07-01T19:56:32Z
author_association: MEMBER
diff_hunk: @@ -128,6 +130,46 @@ void NPC::Load(const DataNode &node, const string &missionName)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
+		else if(child.Token(0) == "land" || child.Token(0) == "visit")
path: source/NPC.cpp
user: login: Hurleveur
body: Shouldnt land be "destination"?
I like land but I also like consistency x)

Not sure though
created_at: 2023-07-01T19:58:35Z
author_association: MEMBER
diff_hunk: @@ -128,6 +130,46 @@ void NPC::Load(const DataNode &node, const string &missionName)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
+		else if(child.Token(0) == "land" || child.Token(0) == "visit")
path: source/NPC.cpp
user: login: mOctave
body: It probably should be, and visit should probably be stopover, yes. I'll try to change things over when I can.
created_at: 2023-07-02T01:24:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4568,3 +4590,51 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, patrol, visit, land) to define travel plans.
path: source/AI.cpp
user: login: mOctave
body: ```suggestion
// Job / Mission NPC blocks may use keywords (waypoint, visit, land) to define travel plans.
```
Whoops, missed that one.
created_at: 2023-07-02T01:25:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1763,18 +1776,27 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 
 	if(ship.GetTargetSystem())
 	{
-		PrepareForHyperspace(ship, command);
-		// Issuing the JUMP command prompts the escorts to get ready to jump.
-		command |= Command::JUMP;
-		// Issuing the WAIT command will prevent this parent from jumping.
-		// When all its non-carried, in-system escorts that are not disabled and
-		// have the ability to jump are ready, the WAIT command will be omitted.
-		if(!EscortsReadyToJump(ship))
-			command |= Command::WAIT;
+		// Refuel if able to now, but unable to in the destination system.
path: source/AI.cpp
user: login: mOctave
body: Probably. I'll change it back and see if it breaks anything.
created_at: 2023-07-06T03:57:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +180,8 @@ template <class Type>
 		// HARVEST is related to MINE and is for picking up flotsam after
 		// ATTACK.
 		static const int HARVEST = 0x003;
+		static const int TRAVEL_TO = 0x004;
+		static const int LAND_ON = 0x005;
path: source/AI.h
user: login: mOctave
body: I'm not sure what you mean by this? The way I understand the code (which isn't my own), these are just orders that the AI can issue, linked to otherwise unused integer values.
created_at: 2023-07-06T04:03:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -461,8 +461,7 @@ bool MainPanel::ShowHailPanel()
 		// If the target is out of system, always report a generic response
 		// because the player has no way of telling if it's presently jumping or
 		// not. If it's in system and jumping, report that.
-		if(target->Zoom() < 1. || target->IsDestroyed() || target->GetSystem() != player.GetSystem()
-				|| target->Cloaking() == 1.)
+		if(!target->IsTargetable() || target->GetSystem() != player.GetSystem())
path: source/MainPanel.cpp
user: login: mOctave
body: Looks like it, yes. Do you think I should take it back out?
created_at: 2023-07-06T04:05:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +130,46 @@ void NPC::Load(const DataNode &node, const string &missionName)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
+		else if(child.Token(0) == "land" || child.Token(0) == "visit")
path: source/NPC.cpp
user: login: mOctave
body: There we go, should have all of them changed over now.
created_at: 2023-07-06T04:10:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +180,8 @@ template <class Type>
 		// HARVEST is related to MINE and is for picking up flotsam after
 		// ATTACK.
 		static const int HARVEST = 0x003;
+		static const int TRAVEL_TO = 0x004;
+		static const int LAND_ON = 0x005;
path: source/AI.h
user: login: Hurleveur
body: Nvm then i thought the integers represented keys
created_at: 2023-07-06T05:34:31Z
author_association: MEMBER
diff_hunk: @@ -461,8 +461,7 @@ bool MainPanel::ShowHailPanel()
 		// If the target is out of system, always report a generic response
 		// because the player has no way of telling if it's presently jumping or
 		// not. If it's in system and jumping, report that.
-		if(target->Zoom() < 1. || target->IsDestroyed() || target->GetSystem() != player.GetSystem()
-				|| target->Cloaking() == 1.)
+		if(!target->IsTargetable() || target->GetSystem() != player.GetSystem())
path: source/MainPanel.cpp
user: login: Hurleveur
body: actually mb it was done this way because cloaking is not likely, only a few causes can really happen
so yeah prob take it out?
created_at: 2023-07-21T06:06:01Z
author_association: MEMBER
diff_hunk: @@ -461,8 +461,7 @@ bool MainPanel::ShowHailPanel()
 		// If the target is out of system, always report a generic response
 		// because the player has no way of telling if it's presently jumping or
 		// not. If it's in system and jumping, report that.
-		if(target->Zoom() < 1. || target->IsDestroyed() || target->GetSystem() != player.GetSystem()
-				|| target->Cloaking() == 1.)
+		if(!target->IsTargetable() || target->GetSystem() != player.GetSystem())
path: source/MainPanel.cpp
user: login: mOctave
body: ```suggestion
		if(target->Zoom() < 1. || target->IsDestroyed() || target->GetSystem() != player.GetSystem()
				|| target->Cloaking() == 1.)
```
created_at: 2023-07-21T17:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -789,6 +789,10 @@ void AI::Step(const PlayerInfo &player, Command &activeCommands)
 			}
 		}
 
+		// Update any orders NPCs may have been given by their associated mission.
path: source/AI.cpp
user: login: Hurleveur
body: I dont think we usually put empty space before an else
no big deal but it does make it a bit less readable I think
created_at: 2023-10-02T17:26:53Z
author_association: MEMBER
diff_hunk: @@ -1505,11 +1509,12 @@ bool AI::FollowOrders(Ship &ship, Command &command) const
 		return false;
 
 	int type = it->second.type;
+	const bool isTravelOrder = (type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON);
path: source/AI.cpp
user: login: Hurleveur
body: not sure but wouldnt it be better named as "hasTravelOrder"?
created_at: 2023-10-02T17:28:10Z
author_association: MEMBER
diff_hunk: @@ -4622,3 +4637,51 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, visit, land) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *waypoint, const std::map<const Planet *, bool> stopovers)
path: source/AI.cpp
user: login: Hurleveur
body: I think I'd rename waypoint here, maybe to destination?
bc we dont know if its a waypoint or just the destination, right?
created_at: 2023-10-02T17:31:01Z
author_association: MEMBER
diff_hunk: @@ -109,8 +110,8 @@ class NPC {
 
 	// Create a copy of this NPC but with the fleets replaced by the actual
 	// ships they represent, wildcards in the conversation text replaced, etc.
-	NPC Instantiate(std::map<std::string, std::string> &subs, const System *origin, const System *destination,
-			int jumps, int64_t payload) const;
+	NPC Instantiate(std::map<std::string, std::string> &subs, const System *origin, const Planet *destinationPlanet,
path: source/NPC.h
user: login: Hurleveur
body: just thinking; this doesnt cause an issue if you want to go to a destination system with no planets?
created_at: 2023-10-02T17:34:59Z
author_association: MEMBER
diff_hunk: @@ -4094,10 +4205,22 @@ bool Ship::DoLandingLogic()
 				SetTargetSystem(nullptr);
 				landingPlanet = nullptr;
 			}
-			else if(!isSpecial || personality.IsFleeing())
+			else if(isSpecial && !isYours && !travelDestinations.empty())
 			{
-				MarkForRemoval();
-				return true;
+				// This mission NPC has a directive to land on at least one specific planet.
+				// If this is one of them, this ship may 'land' (permanently), or 'visit'.
+				auto it = travelDestinations.find(landingPlanet);
+				if(it != travelDestinations.end())
+				{
+					if(doVisit)
+					it->second = true;
path: source/Ship.cpp
user: login: Hurleveur
body: u gotta tab this one better ^^
created_at: 2023-10-02T17:39:59Z
author_association: MEMBER
diff_hunk: @@ -4622,3 +4637,51 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, visit, land) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *waypoint, const std::map<const Planet *, bool> stopovers)
path: source/AI.cpp
user: login: mOctave
body: At the same time, a destination is a waypoint, isn't it?

Maybe something like `targetSystem` would make more sense? Although it's used just above...
created_at: 2023-10-06T00:57:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1505,11 +1509,12 @@ bool AI::FollowOrders(Ship &ship, Command &command) const
 		return false;
 
 	int type = it->second.type;
+	const bool isTravelOrder = (type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON);
path: source/AI.cpp
user: login: mOctave
body: Probably. I'll change it.
created_at: 2023-10-06T00:59:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,8 +110,8 @@ class NPC {
 
 	// Create a copy of this NPC but with the fleets replaced by the actual
 	// ships they represent, wildcards in the conversation text replaced, etc.
-	NPC Instantiate(std::map<std::string, std::string> &subs, const System *origin, const System *destination,
-			int jumps, int64_t payload) const;
+	NPC Instantiate(std::map<std::string, std::string> &subs, const System *origin, const Planet *destinationPlanet,
path: source/NPC.h
user: login: mOctave
body: It doesn't appear to? Maybe there's some error or use case that I'm missing, but it seems to work fine.
created_at: 2023-10-06T01:12:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4622,3 +4637,51 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, visit, land) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *waypoint, const std::map<const Planet *, bool> stopovers)
path: source/AI.cpp
user: login: Hurleveur
body: Targetsystem's good
created_at: 2023-10-06T05:22:25Z
author_association: MEMBER
diff_hunk: @@ -2299,10 +2304,25 @@ bool Ship::IsDestroyed() const
 
 
 
-// Recharge and repair this ship (e.g. because it has landed).
+void Ship::Land()
+{
+	hasLanded = true;
path: source/Ship.cpp
user: login: Hurleveur
body: This doesnt make mission escorts that landed to refuel disapear, right?

Also since you seem to check for haslanded like you check for destroyed we should prob change it to a Removed() function?
created_at: 2023-12-01T08:40:43Z
author_association: MEMBER
diff_hunk: @@ -3391,6 +3414,94 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const std::map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Returns true for a mission NPC given a travel directive, when it has arrived
+// in one of its specified destination systems.
+const bool Ship::IsSurveying() const
+{
+	return stayingTime > 0 && currentSystem && currentSystem == destinationSystem;
path: source/Ship.cpp
user: login: Hurleveur
body: Stayingtime was set in another PR, right? Idk if the comment above is true since you dont mention that argument in it
created_at: 2023-12-01T08:42:21Z
author_association: MEMBER
diff_hunk: @@ -2299,10 +2304,25 @@ bool Ship::IsDestroyed() const
 
 
 
-// Recharge and repair this ship (e.g. because it has landed).
+void Ship::Land()
+{
+	hasLanded = true;
path: source/Ship.cpp
user: login: mOctave
body: > This doesnt make mission escorts that landed to refuel disapear, right?

Correct.
created_at: 2023-12-02T19:27:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3391,6 +3414,94 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const std::map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Returns true for a mission NPC given a travel directive, when it has arrived
+// in one of its specified destination systems.
+const bool Ship::IsSurveying() const
+{
+	return stayingTime > 0 && currentSystem && currentSystem == destinationSystem;
path: source/Ship.cpp
user: login: mOctave
body: Stayingtime seems to have just been part of the original PR. It looks like its a holdover from the other part of the code that I'm not implementing, though, so thanks for catching it!
created_at: 2023-12-02T19:41:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1866,9 +1879,10 @@ void AI::MoveEscort(Ship &ship, Command &command) const
 		// If the ship has no destination or the destination is unreachable, route to the parent's system.
 		if(!ship.GetTargetStellar() && (!ship.GetTargetSystem() || !ship.JumpNavigation().JumpFuel(ship.GetTargetSystem())))
 		{
-			// Route to the parent ship's system and check whether
-			// the ship should land (refuel or wormhole) or jump.
-			SelectRoute(ship, parent.GetSystem());
+			// Route to the destination (either the parent ship's system or a system
+			// marked by the NPC's mission definition) by landing or jumping.
+			const System * destinationSystem = ship.GetDestinationSystem();
path: source/AI.cpp
user: login: TomGoodIdea
body: ```suggestion
			const System *destinationSystem = ship.GetDestinationSystem();
```
created_at: 2024-04-22T16:55:39Z
author_association: MEMBER
diff_hunk: @@ -4723,3 +4737,50 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const std::map<const Planet *, bool> stopovers)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		DistanceMap distance(ship, targetSystem);
+		if(!distance.HasRoute(targetSystem))
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next destination, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+				{
+					newOrders.targetSystem = nextSystem;
+				}
path: source/AI.cpp
user: login: TomGoodIdea
body: ```suggestion
					newOrders.targetSystem = nextSystem;
```
created_at: 2024-04-22T16:55:54Z
author_association: MEMBER
diff_hunk: @@ -4723,3 +4737,50 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const std::map<const Planet *, bool> stopovers)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		DistanceMap distance(ship, targetSystem);
+		if(!distance.HasRoute(targetSystem))
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next destination, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+				{
+					newOrders.targetSystem = nextSystem;
+				}
+				else
+					newOrders.type = 0;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			if(!it.second && it.first->IsInSystem(from))
+			{
+				newOrders.type = Orders::LAND_ON;
+				newOrders.targetPlanet = it.first;
+				break;
+			}
+
+	// Update the NPC's orders.
+	Orders &existing = orders[&ship];
+	existing = newOrders;
+	if(existing.type == 0)
+		orders.erase(&ship);
path: source/AI.cpp
user: login: TomGoodIdea
body: ```suggestion
	if(newOrders.type == 0)
		orders.erase(&ship);
	else
		existing = newOrders;
```
created_at: 2024-04-22T16:56:47Z
author_association: MEMBER
diff_hunk: @@ -618,9 +680,17 @@ bool NPC::HasFailed() const
 		if(it.second & failIf)
 			return true;
 
-		// If we still need to perform an action on this NPC, then that ship
-		// being destroyed should cause the mission to fail.
-		if((~it.second & succeedIf) && (it.second & ShipEvent::DESTROY))
+		// If we still need to perform an action that requires the NPC ship be
+		// alive, then that ship being destroyed or landed causes the mission to fail.
+		if((~it.second & succeedIf) && (it.second & (ShipEvent::DESTROY | ShipEvent::LAND)))
+			return true;
+
+		// If this ship has landed permanently, the NPC has failed if
+		// 1) it must accompany and is not in the destination system, or
+		// 2) it must evade, and is in the destination system.
+		if((it.second & ShipEvent::LAND) && !doStopover && it.first->GetSystem()
+				&& ((mustAccompany && it.first->GetSystem() != destination)
+					|| (mustEvade && it.first->GetSystem() == destination)))
path: source/NPC.cpp
user: login: TomGoodIdea
body: ```suggestion
				|| (mustEvade && it.first->GetSystem() == destination)))
```
created_at: 2024-04-22T16:57:18Z
author_association: MEMBER
diff_hunk: @@ -3506,6 +3537,85 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const std::map<const Planet *, bool> Ship::GetStopovers() const
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
const map<const Planet *, bool> Ship::GetStopovers() const
```
created_at: 2024-04-22T16:59:04Z
author_association: MEMBER
diff_hunk: @@ -3506,6 +3537,85 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const std::map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetStopovers(const std::vector<const Planet *> planets, const bool shouldRelaunch)
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
void Ship::SetStopovers(const vector<const Planet *> planets, const bool shouldRelaunch)
```
created_at: 2024-04-22T16:59:16Z
author_association: MEMBER
diff_hunk: @@ -3506,6 +3537,85 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const std::map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetStopovers(const std::vector<const Planet *> planets, const bool shouldRelaunch)
+{
+	doStopover = shouldRelaunch;
+
+	// Mark each planet as not visited.
+	for(const auto &it : planets)
+		travelDestinations[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const std::vector<const System *> waypoints)
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
void Ship::SetWaypoints(const vector<const System *> waypoints)
```
created_at: 2024-04-22T16:59:36Z
author_association: MEMBER
diff_hunk: @@ -4723,3 +4737,49 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const std::map<const Planet *, bool> stopovers)
path: source/AI.cpp
user: login: TomGoodIdea
body: ```suggestion
void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const map<const Planet *, bool> stopovers)
```
created_at: 2024-05-03T07:11:48Z
author_association: MEMBER
diff_hunk: @@ -409,8 +409,8 @@ void Engine::Place(const list<NPC> &npcs, shared_ptr<Ship> flagship)
 		map<string, map<Ship *, int>> carriers;
 		for(const shared_ptr<Ship> &ship : npc.Ships())
 		{
-			// Skip ships that have been destroyed.
-			if(ship->IsDestroyed() || ship->IsDisabled())
+			// Skip ships that have been destroyed, have landed, or are disabled
path: source/Engine.cpp
user: login: TomGoodIdea
body: ```suggestion
			// Skip ships that have been destroyed, have landed, or are disabled.
```
created_at: 2024-05-03T07:11:58Z
author_association: MEMBER
diff_hunk: @@ -4111,13 +4111,21 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetStopovers().count(planet) && !ship->GetStopovers().at(planet))
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
path: source/PlayerInfo.cpp
user: login: TomGoodIdea
body: This is the situation when we land before an NPC that has a stopover on the planet. The NPC lands forever as if it was its final destination and the mission gets the LAND event.

If we're limiting `npc land` missions to complete only at the final destination, this should be either removed (if we want to make sure the NPC lands first for the mission to complete) or changed to use the NPC's destination (if we don't care who lands first; I believe this option would also make `npc outrun` missions fail if we land on the destination when the NPC is in the same system, even if it didn't physically land, making it pretty similar to `npc evade`).
created_at: 2024-05-09T21:40:46Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3616,96 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+
+void Ship::SetStopovers(const vector<const Planet *> stopovers, const Planet *destination)
+{
+	// Mark each planet as not visited.
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+		{
+			travelDestinations[it] = false;
+			Logger::LogError("Stopover: " + it->Name());
+		}
path: source/Ship.cpp
user: login: TomGoodIdea
body: Looks like leftover debug code
```suggestion
			travelDestinations[it] = false;
```
created_at: 2024-05-09T21:41:09Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3616,96 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !travelDestinations.empty() || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return travelDestinations;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+
+void Ship::SetStopovers(const vector<const Planet *> stopovers, const Planet *destination)
+{
+	// Mark each planet as not visited.
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+		{
+			travelDestinations[it] = false;
+			Logger::LogError("Stopover: " + it->Name());
+		}
+
+	if(destination)
+	{
+		travelDestinations[destination] = false;
+		continueAfterDestination = false;
+		Logger::LogError("Destination: " + destination->Name());
path: source/Ship.cpp
user: login: TomGoodIdea
body: same
```suggestion
```
created_at: 2024-05-09T21:41:26Z
author_association: MEMBER
diff_hunk: @@ -4111,13 +4111,21 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetStopovers().count(planet) && !ship->GetStopovers().at(planet))
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
path: source/PlayerInfo.cpp
user: login: mOctave
body: I'm not sure which way to go on this, honestly. Maybe if it checked against the success criteria and skipped this step for missions that didn't have `ShipEvent::LAND` as a success criterion but kept it in (with the NPC's destination) for those that did?
created_at: 2024-05-09T23:03:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4111,13 +4111,21 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetStopovers().count(planet) && !ship->GetStopovers().at(planet))
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
path: source/PlayerInfo.cpp
user: login: TomGoodIdea
body: Sounds fine to me
created_at: 2024-05-09T23:14:08Z
author_association: MEMBER
diff_hunk: @@ -4724,3 +4738,49 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const map<const Planet *, bool> stopovers)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		DistanceMap distance(ship, targetSystem);
+		if(!distance.HasRoute(targetSystem))
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next destination, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.type = 0;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			if(!it.second && it.first->IsInSystem(from))
+			{
+				newOrders.type = Orders::LAND_ON;
+				newOrders.targetPlanet = it.first;
+				break;
+			}
path: source/AI.cpp
user: login: TomGoodIdea
body: Just noticed that this doesn't see any difference between a stopover and a destination, making it possible for an NPC to land on its supposed destination as if it was a stopover and then land forever on a supposed stopover.

I think an NPC should land on its destination only if it has visited every waypoint and stopover it had assigned.
Or alternatively, it could be able to land on the destination before completing other travel instructions, but that should result in the `land` objective being satisfied.
created_at: 2024-05-10T12:21:08Z
author_association: MEMBER
diff_hunk: @@ -4724,3 +4738,49 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem, const map<const Planet *, bool> stopovers)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		DistanceMap distance(ship, targetSystem);
+		if(!distance.HasRoute(targetSystem))
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next destination, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.type = 0;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			if(!it.second && it.first->IsInSystem(from))
+			{
+				newOrders.type = Orders::LAND_ON;
+				newOrders.targetPlanet = it.first;
+				break;
+			}
path: source/AI.cpp
user: login: mOctave
body: Whoops, yes, it shouldn't be able to succeed until it's landed on all stopovers.
created_at: 2024-05-10T17:49:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4724,3 +4739,56 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> stopovers, const Planet *destination)
path: source/AI.cpp
user: login: TomGoodIdea
body: ```suggestion
	const map<const Planet *, bool> &stopovers, const Planet *destination)
```
created_at: 2024-05-13T12:26:24Z
author_association: MEMBER
diff_hunk: @@ -202,13 +205,17 @@ template <class Type>
 		std::weak_ptr<Minable> targetAsteroid;
 		Point point;
 		const System *targetSystem = nullptr;
+		const Planet *targetPlanet = nullptr;
 	};
 
 
 private:
 	void IssueOrders(const Orders &newOrders, const std::string &description);
 	// Convert order types based on fulfillment status.
 	void UpdateOrders(const Ship &ship);
+	// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
+	void IssueNPCOrders(Ship &ship, const System *targetSystem,
+		const std::map<const Planet *, bool> stopovers, const Planet *destination);
path: source/AI.h
user: login: TomGoodIdea
body: ```suggestion
		const std::map<const Planet *, bool> &stopovers, const Planet *destination);
```
created_at: 2024-05-13T12:26:35Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
const map<const Planet *, bool> &Ship::GetStopovers() const
```
created_at: 2024-05-13T12:27:26Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+const bool Ship::AllStopoversVisited() const
+{
+	bool allVisited = true;
+	if(stopovers.empty())
+		return allVisited;
+
+	for(auto const &it : stopovers)
+		allVisited &= it.second;
+	return allVisited;
path: source/Ship.cpp
user: login: TomGoodIdea
body: we don't need the variable
```suggestion
	if(stopovers.empty())
		return true;

	for(const auto &it : stopovers)
		if(!it.second)
			return false;
	return true;
```
created_at: 2024-05-13T12:28:38Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+const bool Ship::AllStopoversVisited() const
+{
+	bool allVisited = true;
+	if(stopovers.empty())
+		return allVisited;
+
+	for(auto const &it : stopovers)
+		allVisited &= it.second;
+	return allVisited;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	this->destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> stopovers)
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
void Ship::SetStopovers(const vector<const Planet *> &stopovers)
```
created_at: 2024-05-13T12:28:56Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+const bool Ship::AllStopoversVisited() const
+{
+	bool allVisited = true;
+	if(stopovers.empty())
+		return allVisited;
+
+	for(auto const &it : stopovers)
+		allVisited &= it.second;
+	return allVisited;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	this->destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> stopovers)
+{
+	// Mark each planet as not visited.
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			this->stopovers[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const vector<const System *> waypoints)
path: source/Ship.cpp
user: login: TomGoodIdea
body: ```suggestion
void Ship::SetWaypoints(const vector<const System *> &waypoints)
```
created_at: 2024-05-13T12:29:04Z
author_association: MEMBER
diff_hunk: @@ -456,6 +461,14 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	const StellarObject *GetTargetStellar() const;
 	// Get ship's target system (it should always be one jump / wormhole pass away).
 	const System *GetTargetSystem() const;
+	// Targets for persistent ships (i.e. mission NPCs).
+	const bool HasTravelDirective() const;
+	const std::map<const Planet *, bool> GetStopovers() const;
path: source/Ship.h
user: login: TomGoodIdea
body: ```suggestion
	const std::map<const Planet *, bool> &GetStopovers() const;
```
created_at: 2024-05-13T12:29:23Z
author_association: MEMBER
diff_hunk: @@ -470,6 +483,13 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	void SetTargetStellar(const StellarObject *object);
 	// Set ship's target system (it should always be one jump / wormhole pass away).
 	void SetTargetSystem(const System *system);
+	// Persistent targets associated with mission NPCs.
+	void SetDestination(const Planet *destination);
+	void SetStopovers(const std::vector<const Planet *> stopovers);
+	void SetWaypoints(const std::vector<const System *> waypoints);
path: source/Ship.h
user: login: TomGoodIdea
body: ```suggestion
	void SetStopovers(const std::vector<const Planet *> &stopovers);
	void SetWaypoints(const std::vector<const System *> &waypoints);
```
created_at: 2024-05-13T12:29:38Z
author_association: MEMBER
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+const bool Ship::AllStopoversVisited() const
+{
+	bool allVisited = true;
+	if(stopovers.empty())
+		return allVisited;
+
+	for(auto const &it : stopovers)
+		allVisited &= it.second;
+	return allVisited;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	this->destinationPlanet = destination;
path: source/Ship.cpp
user: login: TomGoodIdea
body: `this` is unnecessary here
```suggestion
	destinationPlanet = destination;
```
created_at: 2024-05-13T12:30:30Z
author_association: MEMBER
diff_hunk: @@ -69,6 +69,8 @@ class ShipEvent {
 		JUMP = (1 << 9),
 		// This ship just met another ship for the first time
 		ENCOUNTER = (1 << 10),
+		// This ship has permanently landed on a planet.
+		LAND = (1 << 11)
path: source/ShipEvent.h
user: login: warp-core
body: The old last entry has a comma after it.
```suggestion
		LAND = (1 << 11),
```
created_at: 2024-05-13T16:00:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,13 +205,17 @@ template <class Type>
 		std::weak_ptr<Minable> targetAsteroid;
 		Point point;
 		const System *targetSystem = nullptr;
+		const Planet *targetPlanet = nullptr;
 	};
 
 
 private:
 	void IssueOrders(const Orders &newOrders, const std::string &description);
 	// Convert order types based on fulfillment status.
 	void UpdateOrders(const Ship &ship);
+	// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
path: source/AI.h
user: login: warp-core
body: Jobs are just missions offered at the job board; they're still missions.
How travel instructions are defined for NPCs isn't really relevant here; we don't need to know what keywords might be involved. It's actually confusing that this mentions "waypoints" but the method signature doesn't look like it handles waypoints.
created_at: 2024-05-13T16:11:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -777,6 +777,10 @@ void AI::Step(Command &activeCommands)
 				continue;
 			}
 		}
+		// Update any orders NPCs may have been given by their associated mission.
+		else if(it->IsSpecial() && !it->IsYours() && it->HasTravelDirective())
+			IssueNPCOrders(*it, it->GetDestinationSystem(), it->GetStopovers(),
+			it->AllStopoversVisited() ? it->GetDestinationPlanet() : nullptr);
path: source/AI.cpp
user: login: warp-core
body: If this expression is going to be split across multiple lines, lines after the first need to be indented more than the first.
But, I'd prefer to use multiple expressions that can each fit on one line rather than splitting one longer expression:
```suggestion
		{
			const Planet *destination = it->AllStopoversVisited() ? it->GetDestinationPlanet() : nullptr;
			IssueNPCOrders(*it, it->GetDestinationSystem(), it->GetStopovers(), destination);
		}
```
created_at: 2024-05-13T16:19:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4724,3 +4739,56 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
path: source/AI.cpp
user: login: warp-core
body: Style.
```suggestion



```
created_at: 2024-05-13T16:30:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4724,3 +4739,56 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+// Job / Mission NPC blocks may use keywords (waypoint, stopover, destination) to define travel plans.
path: source/AI.cpp
user: login: warp-core
body: Same as in the header.
created_at: 2024-05-13T16:30:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -107,11 +108,13 @@ class NPC {
 	// Determine if the NPC is in a failed state. A failed state is irrecoverable, except for
 	// NPCs which would despawn upon the player's next landing.
 	bool HasFailed() const;
+	// Determine if the NPC should has the land success criterion.
path: source/NPC.h
user: login: warp-core
body: ```suggestion
	// Determine if the NPC should have the land success criterion.
```
created_at: 2024-05-13T16:34:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,7 +136,20 @@ class NPC {
 	// Start out in a location matching this filter, or in a particular system:
 	LocationFilter location;
 	const System *system = nullptr;
+	const System *destination = nullptr;
 	bool isAtDestination = false;
+
+	// NPCs may have been given a waypoint, destination or stopover.
path: source/NPC.h
user: login: warp-core
body: ```suggestion
	// NPCs may have been given waypoints, stopovers, or a destination.
```
created_at: 2024-05-13T16:36:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,63 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
path: source/NPC.cpp
user: login: warp-core
body: The description of #2990 (and this PR, though indentations are missing here) describes the "waypoint" syntax as:
```
waypoint/patrol
    LocationFilter 1, line 1
    ...
    LocationFilter 1, line N
waypoint/patrol
    LocationFilter 2, line 1
    ...
    LocationFilter 2, line N
```
But, this code doesn't seem to match that.

We check the `DataNode` `child` for the token `"waypoint"`. The example usage has this as the top level node.
If the `"waypoint"` node has children, we look at those children. For each of the `"waypoint"` nodes children (referred to by the identifier `grand`) we construct a new `LocationFilter` with that `grand` node.

Going back to the example usage, it looks like the first `LocationFilter` is passed the node containing `LocationFilter 1, line 1`, then a new `LocationFilter` will be constructed with the node containing `LocationFilter 1, line 2`, and so on until the node containing `LocationFilter 1, line N`.

This doesn't seem right.

The else block at the bottom should probably contain just:
`waypointFilters.emplace_back(child);`

The usage in the test file you've linked seems to match this code, not the PR description.
I'm not really a fan of essentially just having dummy nodes, and would prefer the format that is described in the PR description, unless we are expecting to have the "names" of those filters actually mean something at some point?

As it is, I think:
```
npc
	waypoint
		[filter specification]
	waypoint
		[filter specification]
	...
```
Is a lot better than:
```
npc
	waypoint
		whatever
			[filter specification]
		another thing
			[filter specification]
	waypoint
		ioerubgwkgwna
			[filter specification]
	...
```
Especially since the former matches how waypoints are already defined as children of a mission.
created_at: 2024-05-13T16:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,63 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					needsWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					waypointFilters.emplace_back(grand);
+		}
+		else if(child.Token(0) == "destination")
+		{
+			// Only the last defined "destination" node is used to determine the final
+			// destination of the NPC. This destination is visited only after all stopovers
+			// have been, and landing on it completes the mission. If no planet
+			// is passed to the node, the mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "destination" or "destination <planet>"
+				if(child.Size() == 1)
+					needsDestination = true;
+				else
+					finalDestination = GameData::Planets().Get(child.Token(1));
+			}
+			// Given "destination" and child nodes. These get processed during NPC instantiation.
+			else
+				destinationFilter.Load(child);
+		}
+		else if(child.Token(0) == "stopover")
+		{
+			// Stopovers can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "stopover" nodes. All stopovers are visited (no permanent
+			// landing on the stopovers). If no planet is passed to the node, the
+			// mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "stopover" or "destination <planet 1> ... <planet N>".
+				if(child.Size() == 1)
+					needsStopover = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						stopovers.push_back(GameData::Planets().Get(child.Token(i)));
+			}
+			// Given "stopover" and child nodes. These get processed during NPC instantiation.
+			else
+				for(const DataNode &grand : child)
+					stopoverFilters.emplace_back(grand);
+		}
path: source/NPC.cpp
user: login: warp-core
body: Same as the above comment about waypoints.
created_at: 2024-05-13T16:49:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +337,22 @@ void NPC::Load(const DataNode &node)
 		ship->SetPersonality(personality);
 		ship->SetIsSpecial();
 		ship->FinishLoading(false);
+		if(!waypoints.empty())
+			ship->SetWaypoints(waypoints);
+		if(!stopovers.empty())
+			ship->SetStopovers(stopovers);
+		if(finalDestination)
+			ship->SetDestination(finalDestination);
+	}
+
+
+	// NPCs given the "land" or "outrun" completion criteria condition should also have a destination.
+	if(!(finalDestination || needsDestination) && destinationFilter.IsEmpty())
+	{
+		if(succeedIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to land is impossible without a destination.");
+		else if(failIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to outrun is impossible without a destination.");
 	}
path: source/NPC.cpp
user: login: warp-core
body: `needsDestination` doesn't seem like a great name in this usage.
This first part of this expression can also be written as:
`!finalDestination && !needsDestination`
So the whole thing becomes:
`!finalDestination && !needsDestination && destinationFilter.IsEmpty()`
Which I would say as:
"There is no final destination, and it does not need a destination, and the destination filter is empty."
So, the whole thing is:
"If there is no final destination, and it does not need a destination, and the destination filter is empty, send a warning that the lack of destination makes the objective impossible."
Which is strange because we're only sending this warning if "it does not need a destination," but the warning is saying it does need a destination?
created_at: 2024-05-13T17:03:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -672,6 +794,40 @@ NPC NPC::Instantiate(map<string, string> &subs, const System *origin, const Syst
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
+
+	if(needsWaypoint)
+		result.waypoints.push_back(result.destination);
+	for(const LocationFilter &filter : waypointFilters)
+	{
+		// Each new waypoint calculates "distance X Y" from the previous waypoint / origin.
+		const System *choice = filter.PickSystem(result.waypoints.empty() ?
+				origin : result.waypoints.back());
path: source/NPC.cpp
user: login: warp-core
body: If we're doing this, we probably shouldn't be using the same keyword as mission waypoints, which don't care about the order they are defined.
created_at: 2024-05-13T17:14:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -701,6 +857,14 @@ NPC NPC::Instantiate(map<string, string> &subs, const System *origin, const Syst
 		if(result.personality.IsDerelict())
 			ship->Disable();
 
+		// Use the destinations stored in the NPC copy, in case they were auto-generated.
path: source/NPC.cpp
user: login: warp-core
body: ```suggestion
		// Use the destinations stored in the instantiated NPC, in case they were auto-generated.
```
created_at: 2024-05-13T17:22:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +461,14 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	const StellarObject *GetTargetStellar() const;
 	// Get ship's target system (it should always be one jump / wormhole pass away).
 	const System *GetTargetSystem() const;
+	// Targets for persistent ships (i.e. mission NPCs).
+	const bool HasTravelDirective() const;
path: source/Ship.h
user: login: warp-core
body: Primitive return types don't need to be `const`.
```suggestion
	bool HasTravelDirective() const;
```
created_at: 2024-05-13T17:26:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +461,14 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	const StellarObject *GetTargetStellar() const;
 	// Get ship's target system (it should always be one jump / wormhole pass away).
 	const System *GetTargetSystem() const;
+	// Targets for persistent ships (i.e. mission NPCs).
+	const bool HasTravelDirective() const;
+	const std::map<const Planet *, bool> &GetStopovers() const;
+	const bool AllStopoversVisited() const;
path: source/Ship.h
user: login: warp-core
body: ```suggestion
	bool AllStopoversVisited() const;
```
created_at: 2024-05-13T17:26:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -456,6 +461,14 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	const StellarObject *GetTargetStellar() const;
 	// Get ship's target system (it should always be one jump / wormhole pass away).
 	const System *GetTargetSystem() const;
+	// Targets for persistent ships (i.e. mission NPCs).
+	const bool HasTravelDirective() const;
+	const std::map<const Planet *, bool> &GetStopovers() const;
+	const bool AllStopoversVisited() const;
+	const Planet *GetDestinationPlanet() const;
+	const System *GetDestinationSystem() const;
+	const bool ContinueAfterDestination() const;
path: source/Ship.h
user: login: warp-core
body: ```suggestion
	bool ContinueAfterDestination() const;
```
created_at: 2024-05-13T17:27:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1718,7 +1722,8 @@ shared_ptr<Ship> Ship::Board(bool autoPlunder, bool nonDocking)
 	hasBoarded = false;
 
 	shared_ptr<Ship> victim = GetTargetShip();
-	if(CannotAct(Ship::ActionType::BOARD) || !victim || victim->IsDestroyed() || victim->GetSystem() != GetSystem())
+	if(CannotAct(Ship::ActionType::BOARD) || !victim || victim->IsRemoved()
+		|| victim->GetSystem() != GetSystem())
path: source/Ship.cpp
user: login: warp-core
body: Can't this stay on one line?
```suggestion
	if(CannotAct(Ship::ActionType::BOARD) || !victim || victim->IsRemoved() || victim->GetSystem() != GetSystem())
```
created_at: 2024-05-13T17:31:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3585,6 +3617,111 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+const bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+const bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &stopovers)
+{
+	// Mark each planet as not visited.
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			this->stopovers[it] = false;
path: source/Ship.cpp
user: login: warp-core
body: If it's empty, the range based for loop won't do anything anyway.
```suggestion
	// Mark each planet as not visited.
	for(const auto &it : stopovers)
		this->stopovers[it] = false;
```
created_at: 2024-05-13T17:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +337,22 @@ void NPC::Load(const DataNode &node)
 		ship->SetPersonality(personality);
 		ship->SetIsSpecial();
 		ship->FinishLoading(false);
+		if(!waypoints.empty())
+			ship->SetWaypoints(waypoints);
+		if(!stopovers.empty())
+			ship->SetStopovers(stopovers);
+		if(finalDestination)
+			ship->SetDestination(finalDestination);
+	}
+
+
+	// NPCs given the "land" or "outrun" completion criteria condition should also have a destination.
+	if(!(finalDestination || needsDestination) && destinationFilter.IsEmpty())
+	{
+		if(succeedIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to land is impossible without a destination.");
+		else if(failIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to outrun is impossible without a destination.");
 	}
path: source/NPC.cpp
user: login: mOctave
body: I renamed `needsDestination`, `needsStopover`, and `needsWaypoint` to `missingDestination`, `missingStopover`, and `missingWaypoint`. Is that more understandable, or should it be something along the lines of `nongeneratedDestination`?
created_at: 2024-05-13T23:59:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -672,6 +794,40 @@ NPC NPC::Instantiate(map<string, string> &subs, const System *origin, const Syst
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
+
+	if(needsWaypoint)
+		result.waypoints.push_back(result.destination);
+	for(const LocationFilter &filter : waypointFilters)
+	{
+		// Each new waypoint calculates "distance X Y" from the previous waypoint / origin.
+		const System *choice = filter.PickSystem(result.waypoints.empty() ?
+				origin : result.waypoints.back());
path: source/NPC.cpp
user: login: mOctave
body: Hmmm... I'm not sure what else to call them, though. Any ideas?
created_at: 2024-05-14T00:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1724,6 +1724,9 @@ void Engine::MoveShip(const shared_ptr<Ship> &ship)
 				if(bay.ship)
 					eventQueue.emplace_back(nullptr, bay.ship, ShipEvent::DESTROY);
 		}
+		else if(ship->HasLanded() && ship->IsSpecial())
+			eventQueue.emplace_back(nullptr, ship, ShipEvent::LAND);
+		// No additional actions can occur for this ship.
path: source/Engine.cpp
user: login: RisingLeaf
body: ```suggestion
```
created_at: 2024-07-23T06:06:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1663,14 +1669,15 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 
 	int type = it->second.type;
+	const bool hasTravelOrder = (type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON);
 
 	// Ships without an (alive) parent don't follow orders.
 	shared_ptr<Ship> parent = ship.GetParent();
 	if(!parent)
 		return false;
 	// If your parent is jumping or absent, that overrides your orders unless
-	// your orders are to hold position.
-	if(parent && type != Orders::HOLD_POSITION && type != Orders::HOLD_ACTIVE && type != Orders::MOVE_TO)
+	// your orders are to hold position, or a travel directive.
path: source/AI.cpp
user: login: TheGiraffe3
body: ```suggestion
	// Your orders are to hold position, or a travel directive.
```
created_at: 2024-07-23T07:11:20Z
author_association: MEMBER
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
path: source/NPC.cpp
user: login: TheGiraffe3
body: ```suggestion
				// Given "waypoint" or "waypoint <system 1> .... <system N>".
```
created_at: 2024-07-23T07:12:43Z
author_association: MEMBER
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				waypointFilters.emplace_back(child);
+		}
+		else if(child.Token(0) == "destination")
+		{
+			// Only the last defined "destination" node is used to determine the final
+			// destination of the NPC. This destination is visited only after all stopovers
+			// have been, and landing on it completes the mission. If no planet
+			// is passed to the node, the mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "destination" or "destination <planet>"
path: source/NPC.cpp
user: login: TheGiraffe3
body: ```suggestion
				// Given "destination" or "destination <planet>".
```
created_at: 2024-07-23T07:12:56Z
author_association: MEMBER
diff_hunk: @@ -705,6 +729,17 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	std::set<const Flotsam *> tractorFlotsam;
 	const FormationPattern *formationPattern = nullptr;
 
+	// NPC travel directives
path: source/Ship.h
user: login: TheGiraffe3
body: ```suggestion
	// NPC travel directives.
```
created_at: 2024-07-23T07:15:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: TheGiraffe3
body: 👀
Hurleveur wrote this?
```suggestion
# Copyright (c) 2024 by Hurleveur
```
created_at: 2024-07-23T07:16:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
+#
+# Endless Sky is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later version.
+#
+# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <https://www.gnu.org/licenses/>.
+
+test-data "Npc Destination Save"
+	category "savegame"
+	contents
+		pilot Bobbi Bughunter
+		date 16 11 3013
+		system Sol
+		planet Earth
+		clearance
+		# Set some reputations to positive to avoid combat
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: TheGiraffe3
body: ```suggestion
		# Set some reputations to positive to avoid combat.
```
created_at: 2024-07-23T07:16:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
+#
+# Endless Sky is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later version.
+#
+# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <https://www.gnu.org/licenses/>.
+
+test-data "Npc Destination Save"
+	category "savegame"
+	contents
+		pilot Bobbi Bughunter
+		date 16 11 3013
+		system Sol
+		planet Earth
+		clearance
+		# Set some reputations to positive to avoid combat
+		"reputation with"
+			Bounty 1
+			"Bounty Hunter" 1
+			Pirate 1
+		# What you own:
+		ship "Star Barge"
+			name "Buggy Barge"
+			sprite "ship/star barge"
+			attributes
+				category "Light Freighter"
+				cost 190000
+				mass 70
+				bunks 3
+				"cargo space" 50
+				drag 2.1
+				"engine capacity" 400
+				"fuel capacity" 800
+				"heat dissipation" 0.8
+				hull 1000
+				"outfit space" 1300
+				"required crew" 1
+				shields 600
+				"turret mounts" 1
+				"weapon capacity" 200
+			outfits
+				"X1700 Ion Thruster"
+				"X1200 Ion Steering"
+				Hyperdrive
+				"Anti-Missile Turret"
+				"D14-RN Shield Generator"
+				"nGVF-BB Fuel Cell"
+				"LP036a Battery Pack"
+			crew 1
+			fuel 300
+			shields 600
+			hull 1000
+			engine -9 38 1
+			engine 9 38 1
+			turret 0 -8 "Anti-Missile Turret"
+			leak leak 60 50
+			explode "tiny explosion" 10
+			explode "small explosion" 10
+			system Sol
+			planet Earth
+		account
+			credits 131000
+			score 400
+			history
+		visited Sol
+		"visited planet" Earth
+		visited "Sirius"
+		visited "Procyon"
+		visited "Pollux"
+		visited "Castor"
+
+test-data "Npc Destination Mission"
+	category mission
+	contents
+		mission "npc"
+			landing
+			destination "Geminus"
+			npc land save
+				ship "Quarg Wardragon"
+				government "Quarg"
+				destination Geminus
+				waypoint
+				personality waiting
+
+
+test "Npc Destination Test"
+	status active
+	description "Starts the mission, accepts it and then goes to the destination making sure the Quarg got there first."
+	sequence
+		inject "Npc Destination Mission"
+		inject "Npc Destination Save"
+		call "Load First Savegame"
+		# Set desired travel plan.
+		navigate
+			travel Castor
+			travel Pollux
+			travel Procyon
+			travel Sirius
+			"travel destination" Geminus
+		call "Depart"
+		# trigger the mission
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: TheGiraffe3
body: ```suggestion
		# Trigger the mission.
```
created_at: 2024-07-23T07:18:12Z
author_association: MEMBER
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
path: source/NPC.cpp
user: login: mOctave
body: I'm going to leave this as-is, because it's technically invalid grammar with American English to have the period outside the quotes, and obviously it wouldn't be a good idea to put them inside.
created_at: 2024-07-23T18:20:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -705,6 +729,17 @@ class Ship : public Body, public std::enable_shared_from_this<Ship> {
 	std::set<const Flotsam *> tractorFlotsam;
 	const FormationPattern *formationPattern = nullptr;
 
+	// NPC travel directives
path: source/Ship.h
user: login: mOctave
body: Not a full sentence, so I'm not sure a period would make sense here
created_at: 2024-07-23T18:21:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: mOctave
body: Yes, Hurleveur wrote this, in November of last year (this PR's been open for a while now), so I'm going to leave the copyright header as-is.
created_at: 2024-07-23T18:27:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1663,14 +1669,15 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 
 	int type = it->second.type;
+	const bool hasTravelOrder = (type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON);
 
 	// Ships without an (alive) parent don't follow orders.
 	shared_ptr<Ship> parent = ship.GetParent();
 	if(!parent)
 		return false;
 	// If your parent is jumping or absent, that overrides your orders unless
-	// your orders are to hold position.
-	if(parent && type != Orders::HOLD_POSITION && type != Orders::HOLD_ACTIVE && type != Orders::MOVE_TO)
+	// Your orders are to hold position, or a travel directive.
path: source/AI.cpp
user: login: RisingLeaf
body: ```suggestion
	// your orders are to hold position, or a travel directive.
```
created_at: 2024-07-30T08:38:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4953,3 +4970,58 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		DistanceMap distance(ship, targetSystem);
+		if(!distance.HasRoute(targetSystem))
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next destination, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.type = 0;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	if(!stopovers.empty())
+		for(const auto &it : stopovers)
+			if(!it.second && it.first->IsInSystem(from))
+			{
+				newOrders.type = Orders::LAND_ON;
+				newOrders.targetPlanet = it.first;
+				break;
+			}
path: source/AI.cpp
user: login: RisingLeaf
body: ```suggestion
	for(const auto &it : stopovers)
		if(!it.second && it.first->IsInSystem(from))
		{
			newOrders.type = Orders::LAND_ON;
			newOrders.targetPlanet = it.first;
			break;
		}
```
created_at: 2024-07-30T08:42:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,8 +439,8 @@ void Engine::Place(const list<NPC> &npcs, shared_ptr<Ship> flagship)
 		map<string, map<Ship *, int>> carriers;
 		for(const shared_ptr<Ship> &ship : npc.Ships())
 		{
-			// Skip ships that have been destroyed.
-			if(ship->IsDestroyed() || ship->IsDisabled())
+			// Skip ships that have been destroyed, have landed, or are disabled.
+			if(ship->IsRemoved() || ship->IsDisabled())
path: source/Engine.cpp
user: login: RisingLeaf
body: a destroyed ship is not necessary removed yet iirc
created_at: 2024-07-30T08:42:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
path: source/NPC.cpp
user: login: RisingLeaf
body: what if there was a waypoint node before this one that added something?
created_at: 2024-07-30T08:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
path: source/NPC.cpp
user: login: mOctave
body: Hmm... I'm not sure. I _think_ it would behave normally, though, since it only checks once if there's a missing waypoint in `Instantiate` and adds the mission destination if there is.
created_at: 2024-07-30T17:33:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
path: source/NPC.cpp
user: login: RisingLeaf
body: ah ok ye works
created_at: 2024-07-31T07:05:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				waypointFilters.emplace_back(child);
+		}
+		else if(child.Token(0) == "destination")
+		{
+			// Only the last defined "destination" node is used to determine the final
+			// destination of the NPC. This destination is visited only after all stopovers
+			// have been, and landing on it completes the mission. If no planet
+			// is passed to the node, the mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "destination" or "destination <planet>"
+				if(child.Size() == 1)
+					missingDestination = true;
+				else
+					finalDestination = GameData::Planets().Get(child.Token(1));
+			}
+			// Given "destination" and child nodes. These get processed during NPC instantiation.
path: source/NPC.cpp
user: login: RisingLeaf
body: ```suggestion
			// Given "destination" and a location filter. These get processed during NPC instantiation.
```
created_at: 2024-07-31T07:06:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
+				if(child.Size() == 1)
+					missingWaypoint = true;
+				else
+					for(int i = 1; i < child.Size(); ++i)
+						waypoints.push_back(GameData::Systems().Get(child.Token(i)));
+			}
+			// Given "waypoint" and child nodes. These get processed during NPC instantiation.
+			else
+				waypointFilters.emplace_back(child);
+		}
+		else if(child.Token(0) == "destination")
+		{
+			// Only the last defined "destination" node is used to determine the final
+			// destination of the NPC. This destination is visited only after all stopovers
+			// have been, and landing on it completes the mission. If no planet
+			// is passed to the node, the mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "destination" or "destination <planet>"
+				if(child.Size() == 1)
+					missingDestination = true;
+				else
+					finalDestination = GameData::Planets().Get(child.Token(1));
+			}
+			// Given "destination" and a location filter. These get processed during NPC instantiation.
+			else
+				destinationFilter.Load(child);
+		}
+		else if(child.Token(0) == "stopover")
+		{
+			// Stopovers can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "stopover" nodes. All stopovers are visited (no permanent
+			// landing on the stopovers). If no planet is passed to the node, the
+			// mission's destination will be used.
+			if(!child.HasChildren())
+			{
+				// Given "stopover" or "destination <planet 1> ... <planet N>".
path: source/NPC.cpp
user: login: TheGiraffe3
body: Based on what you said in an earlier review comment.
```suggestion
				// Given "stopover" or "destination <planet 1> ... <planet N>"
```
created_at: 2024-09-20T03:08:11Z
author_association: MEMBER
diff_hunk: @@ -2031,6 +2047,8 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 	else if(ship.GetTargetStellar())
 	{
 		MoveToPlanet(ship, command);
+		// Ships should land on their destination planet if they are free to
+		// move about, or have a travel directive indicating they should land.
path: source/AI.cpp
user: login: warp-core
body: I don't see anywhere that the following code considers if the ship has a travel directive.
Mentioning it in this comment like this suggests it should be considered, which is confusing if the code is actually correct and doesn't need to consider that. If the idea of the comment is accurate and that should be considered, then the code is incorrect.

I think the idea is that a stellar object would not be targeted if the ship didn't want to land theere for some reason, such as for a travel directive, but this code also requires that the ship have fuel capacity if it is to be landed, and ships without fuel capacity will just hover over/close to the stellar object and then move on. I expect this is for surveillance drone type ships which want to target and fly by stellar objects, but don't want to land.
Ships with a travel directive should probably be handled explicitly here (for example, they shouldn't require a fuel capacity in order to be landed.)
created_at: 2025-01-09T12:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
path: source/AI.cpp
user: login: warp-core
body: The `if(from == targetSystem)` check feels like it ought to be done first.
If `from == targetSystem`, we don't need to check if a route is possible, (even though, in this case, that operation would not be too expensive) but then this also just adds the next system to the orders without checking if a route is available?
Do we need to check if a route is available or is it safe to assume that if a system is in the list of waypoints, a route is available or not?
If it is, then the `RoutePlan` in this is just being used as a really convoluted way to check if the current system is the target system, and if it isn't, then it seems like that should also be checked for the next system?
created_at: 2025-01-09T13:04:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4196,13 +4196,22 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetDestinationPlanet() == planet
+					&& npc.SucceedsOnLanding() && ship->AllStopoversVisited())
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
+			}
path: source/PlayerInfo.cpp
user: login: warp-core
body: What if the ship is not capable of landing on this planet?
created_at: 2025-01-09T14:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2533,10 +2537,33 @@ bool Ship::IsDestroyed() const
 
 
 
-// Recharge and repair this ship (e.g. because it has landed).
+void Ship::LandForever()
+{
+	hasLanded = true;
+}
+
+
+
+// Check if this ship has permanently landed.
+bool Ship::HasLanded() const
+{
+	return hasLanded;
+}
+
+
+
+// Check if this ship has permanently landed or been destroyed.
+bool Ship::LandedOrDestroyed() const
+{
+	return (hull < 0. || hasLanded);
path: source/Ship.cpp
user: login: warp-core
body: Reuse the existing methods rather than repeating their content here.
```suggestion
	return (IsDestroyed() || HasLanded());
```
created_at: 2025-01-09T14:46:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2679,6 +2706,10 @@ void Ship::ClearTargetsAndOrders()
 	targetFlotsam.reset();
 	hyperspaceSystem = nullptr;
 	landingPlanet = nullptr;
+	destinationSystem = nullptr;
+	destinationPlanet = nullptr;
+	stopovers.clear();
+	waypoints.clear();
path: source/Ship.cpp
user: login: warp-core
body: We don't need to include the waypoint index value, either.
```suggestion
	waypoints.clear();
	waypoint = 0;
```
created_at: 2025-01-09T14:48:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
path: source/Ship.cpp
user: login: warp-core
body: What about waypoints?
created_at: 2025-01-09T14:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
path: source/Ship.cpp
user: login: warp-core
body: ```suggestion
	return all_of(stopovers.begin(), stopovers.end(), [](const auto &it) { return it.second; });
```
created_at: 2025-01-10T01:49:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
path: source/Ship.cpp
user: login: warp-core
body: The methods should appear in the same order in this file as the declarations in the header.
created_at: 2025-01-10T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &stopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : stopovers)
+		this->stopovers[it] = false;
path: source/Ship.cpp
user: login: warp-core
body: Can we just call the parameter `newStopovers` (and `newWaypoints` for the other method)?
created_at: 2025-01-10T01:55:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &stopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : stopovers)
+		this->stopovers[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const vector<const System *> &waypoints)
+{
+	// Ships loaded from save files may have an existing waypoint that
+	// indicates which systems have already been visited.
+	if(waypoint < waypoints.size())
+	{
+		this->waypoints = waypoints;
+		if(targetSystem && personality.IsEntering())
+		{
+			--waypoint;
+			destinationSystem = targetSystem;
+		}
+		else
+			destinationSystem = waypoints[waypoint];
+	}
+	else
+		destinationSystem = nullptr;
+}
+
+
+
+const System *Ship::NextWaypoint()
+{
+	++waypoint;
+
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+	return destinationSystem;
+}
+
+
+
+void Ship::EraseWaypoint(const System *system)
+{
+	for(size_t i = 0; i < waypoints.size(); ++i)
+		if(waypoints[i] == system)
+		{
+			waypoints.erase(waypoints.begin() + i);
+			if(waypoint > i)
+				--waypoint;
+			destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+			break;
+		}
path: source/Ship.cpp
user: login: warp-core
body: Please double check the logic of this.
```suggestion
	auto it = waypoints.find(system);
	if(it == waypoints.end())
		return;
	if(waypoint > std::distance(waypoints.begin(), it))
		--waypoint;
	waypoints.erase(it);
	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
```
created_at: 2025-01-10T02:07:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5136,3 +5288,12 @@ void Ship::Jettison(shared_ptr<Flotsam> toJettison)
 		++bayIndex;
 	}
 }
+
+void Ship::ResetStopovers()
+{
+	if(stopovers.empty())
+		return;
+
path: source/Ship.cpp
user: login: warp-core
body: If the container is empty, the for loop is already not going to do anything.
```suggestion
```
created_at: 2025-01-10T02:12:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,11 +107,13 @@ class NPC {
 	// Determine if the NPC is in a failed state. A failed state is irrecoverable, except for
 	// NPCs which would despawn upon the player's next landing.
 	bool HasFailed() const;
+	// Determine if the NPC should have the land success criterion.
path: source/NPC.h
user: login: warp-core
body: I don't think this comment says anything that "SucceedsOnLanding" doesn't. I actually think this comment is less clear than the method name because "land success criterion" just seems like a worse ordering of words.
```suggestion
```
created_at: 2025-01-10T02:26:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
path: source/NPC.cpp
user: login: warp-core
body: The comments like this are just stating what the code does, not summarising particularly large sections of code, providing additional information, or explaining particularly complex code. I think the comments are far too verbose and should be removed.
created_at: 2025-01-10T02:43:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -330,6 +407,29 @@ void NPC::Save(DataWriter &out) const
 			out.Write("government", government->GetTrueName());
 		personality.Save(out);
 
+		if(!waypoints.empty())
+		{
+			out.WriteToken("waypoint");
+			for(const auto &waypoint : waypoints)
+				out.WriteToken(waypoint->Name());
+			out.Write();
+		}
+
+		if(finalDestination)
+		{
+			out.WriteToken("destination");
+			out.WriteToken(finalDestination->Name());
+			out.Write();
+		}
path: source/NPC.cpp
user: login: warp-core
body: ```suggestion
			out.Write("destination", finalDestination->Name());
```
created_at: 2025-01-10T02:48:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -705,6 +859,14 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 		if(result.personality.IsDerelict())
 			ship->Disable();
 
+		// Use the destinations stored in the instantiated NPC, in case they were auto-generated.
path: source/NPC.cpp
user: login: warp-core
body: I read this comment and think "do the correct thing, don't do the incorrect thing."
"Auto-generated" isn't even a good description. At best, that could describe the case where the destination from the mission is used, that would be automatic, since the user doesn't do anything but ask for whatever the mission is using to be used here. Locations selected from a filter are not auto-generated; a person defined that filter.
```suggestion
```
created_at: 2025-01-10T23:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -673,6 +793,40 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
+
+	if(missingWaypoint)
+		result.waypoints.push_back(result.destination);
+	for(const LocationFilter &filter : waypointFilters)
+	{
+		// Each new waypoint calculates "distance X Y" from the previous waypoint / origin.
path: source/NPC.cpp
user: login: warp-core
body: Comment just repeats what the code already says. And, I don't think it describes it very well, anyway.
```suggestion
```
If the code is really that difficult to understand:
```suggestion
		// Each waypoint is used as the center for the filter that selects the next waypoint;
		// the origin of the mission is used as the center for selecting the first waypoint.
```
Similar for the comments on stopovers and the destination.
created_at: 2025-01-10T23:22:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
+#
+# Endless Sky is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later version.
+#
+# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <https://www.gnu.org/licenses/>.
+
+test-data "Npc Destination Save"
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: warp-core
body: "NPC" not "Npc"
created_at: 2025-01-10T23:24:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,11 +107,13 @@ class NPC {
 	// Determine if the NPC is in a failed state. A failed state is irrecoverable, except for
 	// NPCs which would despawn upon the player's next landing.
 	bool HasFailed() const;
+	// Determine if the NPC should have the land success criterion.
path: source/NPC.h
user: login: mOctave
body: I don't think deleting this comment would be appropriate, because then it would be grouped with the comment above, but it probably could use a reword...

Maybe:
```suggestion
	// Determine if the NPC needs to land in order for successful mission completion.
```

created_at: 2025-01-11T21:34:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023 by Hurleveur
+#
+# Endless Sky is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later version.
+#
+# Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <https://www.gnu.org/licenses/>.
+
+test-data "Npc Destination Save"
path: tests/integration/config/plugins/integration-tests/data/tests/tests_npc_destination.txt
user: login: mOctave
body: ```suggestion
test-data "NPC Destination Save"
```
created_at: 2025-01-11T21:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2031,6 +2047,8 @@ void AI::MoveIndependent(Ship &ship, Command &command) const
 	else if(ship.GetTargetStellar())
 	{
 		MoveToPlanet(ship, command);
+		// Ships should land on their destination planet if they are free to
+		// move about, or have a travel directive indicating they should land.
path: source/AI.cpp
user: login: mOctave
body: I believe the "travel directive" part was intended as a reference to `GetTargetStellar()`, and this block only executes if there is a target planet.

I think the fuel capacity thing was a typo and should have been:
`!shouldStay || !ship.Attributes.Get("fuel capacity")`

I think maybe it started off as a !(A && B)?
created_at: 2025-01-11T21:52:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &stopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : stopovers)
+		this->stopovers[it] = false;
path: source/Ship.cpp
user: login: mOctave
body: Not sure it's really necessary, but I guess it could make things slightly more readable so why not.
created_at: 2025-01-11T22:06:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
path: source/NPC.cpp
user: login: mOctave
body: Maybe it's just because I'm not really a C++ programmer, but I think the comments are useful. They could probably be edited for brevity, but I think it's best to include them, since most of what they say isn't actually about that section of the code but does describe how what is defined in that part of the code will be used by the NPC.
created_at: 2025-01-11T22:12:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
path: source/AI.cpp
user: login: mOctave
body: I've reordered it with this suggestion, but I'm not sure this version is necessarily better, so I haven't committed it.

The purpose of the `RoutePlan` is to check if a route is available. I don't think it's a safe assumption that any system in the waypoint list is reachable. Route availability *could* be checked for every waypoint until a working one was found, but as it stands I believe it should be picked up by the AI the next time it steps through its logic? I think this is probably a better way to do it than looping through the entire waypoint list in one go, because it's simpler and has no impacts that I can see other than a possible slight delay (which would fit with a pilot removing a waypoint because they can't get there anyways).

```suggestion
	Orders newOrders;
	const System *from = ship.GetSystem();
	if(targetSystem)
	{
		if(from == targetSystem)
		{
			// Travel to the next waypoint, if it exists.
			ship.SetTargetStellar(nullptr);
			newOrders.type = Orders::TRAVEL_TO;
			const System *nextSystem = ship.NextWaypoint();
			if(nextSystem)
				newOrders.targetSystem = nextSystem;
			else
				newOrders.targetSystem = nullptr;
		}
		else
		{
			RoutePlan routePlan(ship, *targetSystem, nullptr);
			// If the next waypoint is unreachable, forget it.
			if(!routePlan.HasRoute())
				ship.EraseWaypoint(targetSystem);
			else
			{
				newOrders.type = Orders::TRAVEL_TO;
				newOrders.targetSystem = targetSystem;
			}
		}
	}
```
created_at: 2025-01-11T22:49:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4196,13 +4196,22 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetDestinationPlanet() == planet
+					&& npc.SucceedsOnLanding() && ship->AllStopoversVisited())
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
+			}
path: source/PlayerInfo.cpp
user: login: mOctave
body: Hmm... good point.

I almost wonder if it should just be allowed to land anyways, and this is one of those times where we trust content creators not to make bad missions. Do planet filters automatically check to make sure their result is landable?
created_at: 2025-01-11T22:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3737,6 +3768,110 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+bool Ship::HasTravelDirective() const
+{
+	return !stopovers.empty() || destinationPlanet || destinationSystem;
+}
+
+
+
+const map<const Planet *, bool> &Ship::GetStopovers() const
+{
+	return stopovers;
+}
+
+
+
+bool Ship::AllStopoversVisited() const
+{
+	if(stopovers.empty())
+		return true;
+
+	for(const auto &it : stopovers)
+		if(!it.second)
+			return false;
+	return true;
+}
+
+
+
+const Planet *Ship::GetDestinationPlanet() const
+{
+	return destinationPlanet;
+}
+
+
+
+const System *Ship::GetDestinationSystem() const
+{
+	return destinationSystem;
+}
+
+
+
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &stopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : stopovers)
+		this->stopovers[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const vector<const System *> &waypoints)
+{
+	// Ships loaded from save files may have an existing waypoint that
+	// indicates which systems have already been visited.
+	if(waypoint < waypoints.size())
+	{
+		this->waypoints = waypoints;
+		if(targetSystem && personality.IsEntering())
+		{
+			--waypoint;
+			destinationSystem = targetSystem;
+		}
+		else
+			destinationSystem = waypoints[waypoint];
+	}
+	else
+		destinationSystem = nullptr;
+}
+
+
+
+const System *Ship::NextWaypoint()
+{
+	++waypoint;
+
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+	return destinationSystem;
+}
+
+
+
+void Ship::EraseWaypoint(const System *system)
+{
+	for(size_t i = 0; i < waypoints.size(); ++i)
+		if(waypoints[i] == system)
+		{
+			waypoints.erase(waypoints.begin() + i);
+			if(waypoint > i)
+				--waypoint;
+			destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+			break;
+		}
path: source/Ship.cpp
user: login: mOctave
body: I think this should work
created_at: 2025-01-11T22:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
path: source/AI.cpp
user: login: warp-core
body: I'm not really a fan of creating orders that are impossible on the assumption that the order will be corrected later. Are you going to trace `AI::Step` manually and make sure the ship will behave reasonably in that case?
I also note that this method is going to recalculate routes a lot, with most of those executions calculating a route that was calculated in the last step already. You should probably check the existing orders and only try to calculate the next step if either the sources of the orders have changed (I doubt this is relevant for NPCs, though) or if the ship has fulfilled the existing orders.
Additionally, you're erasing items from the `waypoints` collection belonging to the ship. But, the ships aren't saving this collection themselves, the NPC stores and saves it, and redistributes it to the ships when it is loaded. This will cause problems if a ship erases some items from its collection and then the game is saved and reloaded because the ships own waypoint pointer will refer to a lower item in the collection than it actually should.

Also, consider a situation where a mission author wishes an NPC be brought to a particular system, but the system is not immediately accessible (to the NPC, or at all) and the player is required to do something else to make it accessible. In this case, it would be quite undesirable to discard systems that are not currently accessible.
created_at: 2025-01-16T15:36:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	for(const auto &it : stopovers)
+		if(!it.second && it.first->IsInSystem(from))
+		{
+			newOrders.type = Orders::LAND_ON;
+			newOrders.targetPlanet = it.first;
+			break;
+		}
path: source/AI.cpp
user: login: warp-core
body: This seems to assume that the final destination and any stopovers will be along the path the ship chooses to take between the waypoints, but I don't think this requirement was ever enforced by `NPC`? This is going to be especially problematic for NPCs using stopovers selected by filters where it may not be possible to ensure that the systems appear in the waypoints list.
Is this handled elsewhere? Otherwise, it appears very easy for a ship to just never even try to visit some or all of its stopovers.
created_at: 2025-01-16T15:39:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -938,6 +938,12 @@ void AI::Step(Command &activeCommands)
 				continue;
 			}
 		}
+		// Update any orders NPCs may have been given by their associated mission.
+		else if(it->IsSpecial() && !it->IsYours() && it->HasTravelDirective())
+		{
+			const Planet *destination = it->AllStopoversVisited() ? it->GetDestinationPlanet() : nullptr;
+			IssueNPCOrders(*it, it->GetDestinationSystem(), it->GetStopovers(), destination);
path: source/AI.cpp
user: login: warp-core
body: These parameters seem like they ought to be retrieved inside `IssueNPCOrders`, not here.
It's already being given a reference to the `Ship` and the rest of these just come from that object anyway.
It would make that method much easier to read to know where those variables are coming from, rather than having to guess or come up here and check.
created_at: 2025-01-16T15:45:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	for(const auto &it : stopovers)
+		if(!it.second && it.first->IsInSystem(from))
+		{
+			newOrders.type = Orders::LAND_ON;
+			newOrders.targetPlanet = it.first;
+			break;
+		}
path: source/AI.cpp
user: login: mOctave
body: Stopovers aren't supposed to tell the ship which planets to fly to, just where it needs to land. This is so that you could have, for example, and escort mission where the ship lands at its destination without you and then you go somewhere else.
created_at: 2025-01-16T15:56:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	for(const auto &it : stopovers)
+		if(!it.second && it.first->IsInSystem(from))
+		{
+			newOrders.type = Orders::LAND_ON;
+			newOrders.targetPlanet = it.first;
+			break;
+		}
path: source/AI.cpp
user: login: mOctave
body: I could add a personality tag or something that adds all stopover systems to the waypoint list, but that might be best done as a separate PR since we're nearing the end of this cycle? Not sure.
created_at: 2025-01-17T02:33:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
path: source/AI.cpp
user: login: mOctave
body: Okay, so I've finally gotten back to thinking about this.

I agree that my assumptions maybe weren't great, and that it might be a good idea to have the loop thing. Would it work better if the waypoints were stored in some kind of map that kept track of if they had been visited yet?

If so, that would probably be slightly more resource-intensive to scan, since it would involve checking the route for every single one of them if the current waypoint was visited.

How should I go about ensuring the ship waypoints aren't lost? Should I be modifying the NPC's waypoints directly instead of going through the ships'?
created_at: 2025-02-25T03:54:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +337,22 @@ void NPC::Load(const DataNode &node)
 		ship->SetPersonality(personality);
 		ship->SetIsSpecial();
 		ship->FinishLoading(false);
+		if(!waypoints.empty())
+			ship->SetWaypoints(waypoints);
+		if(!stopovers.empty())
+			ship->SetStopovers(stopovers);
+		if(finalDestination)
+			ship->SetDestination(finalDestination);
+	}
+
+
+	// NPCs given the "land" or "outrun" completion criteria condition should also have a destination.
+	if(!(finalDestination || needsDestination) && destinationFilter.IsEmpty())
+	{
+		if(succeedIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to land is impossible without a destination.");
+		else if(failIf & ShipEvent::LAND)
+			node.PrintTrace("Warning: NPC mission objective to outrun is impossible without a destination.");
 	}
path: source/NPC.cpp
user: login: petervdmeer
body: The new names sound more clear.
created_at: 2025-05-17T22:09:52Z
author_association: MEMBER
diff_hunk: @@ -672,6 +794,40 @@ NPC NPC::Instantiate(map<string, string> &subs, const System *origin, const Syst
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
+
+	if(needsWaypoint)
+		result.waypoints.push_back(result.destination);
+	for(const LocationFilter &filter : waypointFilters)
+	{
+		// Each new waypoint calculates "distance X Y" from the previous waypoint / origin.
+		const System *choice = filter.PickSystem(result.waypoints.empty() ?
+				origin : result.waypoints.back());
path: source/NPC.cpp
user: login: petervdmeer
body: I guess waypoints is fine; one is waypoints that the player needs to visit, the other one is waypoints that the NPC will visit. It seems sufficiently similar.
created_at: 2025-05-17T22:10:55Z
author_association: MEMBER
diff_hunk: @@ -4196,13 +4196,22 @@ void PlayerInfo::SortAvailable()
 // Visit, Complete, Fail), and remove now-complete or now-failed missions.
 void PlayerInfo::StepMissions(UI *ui)
 {
-	// Check for NPCs that have been destroyed without their destruction
-	// being registered, e.g. by self-destruct:
+	// Check for NPCs that have been destroyed without their destruction being
+	// registered, e.g. by self-destruct, or landed due to the player landing.
 	for(Mission &mission : missions)
 		for(const NPC &npc : mission.NPCs())
 			for(const shared_ptr<Ship> &ship : npc.Ships())
+			{
 				if(ship->IsDestroyed())
 					mission.Do(ShipEvent(nullptr, ship, ShipEvent::DESTROY), *this, ui);
+				else if(ship->GetSystem() == system && !ship->IsDisabled()
+					&& ship->GetDestinationPlanet() == planet
+					&& npc.SucceedsOnLanding() && ship->AllStopoversVisited())
+				{
+					ship->LandForever();
+					mission.Do(ShipEvent(nullptr, ship, ShipEvent::LAND), *this, ui);
+				}
+			}
path: source/PlayerInfo.cpp
user: login: petervdmeer
body: Trusting the content creators here sounds good to me. Just performing the landing seems to be the most sensible thing to do.
created_at: 2025-05-17T22:12:12Z
author_association: MEMBER
diff_hunk: @@ -131,6 +137,61 @@ void NPC::Load(const DataNode &node)
 			else
 				location.Load(child);
 		}
+		else if(child.Token(0) == "waypoint")
+		{
+			// Waypoints can be added one-by-one (e.g. if from LocationFilters) or
+			// from multiple "waypoint" nodes. If no system or filter to pick a system is
+			// passed to the node, the mission's destination system will be used.
+			if(!child.HasChildren())
+			{
+				// Given "waypoint" or "waypoint <system 1> .... <system N>"
path: source/NPC.cpp
user: login: petervdmeer
body: This last comment on line 147 might be a bit verbose. The comments from line 142 to 144 do seem useful to me.
created_at: 2025-05-17T22:15:45Z
author_association: MEMBER
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
+
+	// If one of the planets in this system is a destination or stopover, it
+	// supercedes the order to travel to the next waypoint (unless already visited).
+	if(destination && destination->IsInSystem(from))
+	{
+		newOrders.type = Orders::LAND_ON;
+		newOrders.targetPlanet = destination;
+	}
+
+	for(const auto &it : stopovers)
+		if(!it.second && it.first->IsInSystem(from))
+		{
+			newOrders.type = Orders::LAND_ON;
+			newOrders.targetPlanet = it.first;
+			break;
+		}
path: source/AI.cpp
user: login: petervdmeer
body: I think this is fine for now. NPCs can have waypoints and stopovers, and having both of them should allow content creators to combine them when needed.
created_at: 2025-05-17T22:17:22Z
author_association: MEMBER
diff_hunk: @@ -5024,3 +5043,57 @@ void AI::UpdateOrders(const Ship &ship)
 		order.targetSystem = ship.GetSystem();
 	}
 }
+
+
+
+// Mission NPC blocks may define specific travel plans.
+void AI::IssueNPCOrders(Ship &ship, const System *targetSystem,
+	const map<const Planet *, bool> &stopovers, const Planet *destination)
+{
+	Orders newOrders;
+	const System *from = ship.GetSystem();
+	if(targetSystem)
+	{
+		RoutePlan routePlan(ship, *targetSystem, nullptr);
+		if(!routePlan.HasRoute())
+			ship.EraseWaypoint(targetSystem);
+		else
+		{
+			newOrders.type = Orders::TRAVEL_TO;
+			newOrders.targetSystem = targetSystem;
+			if(from == targetSystem)
+			{
+				// Travel to the next waypoint, if it exists.
+				ship.SetTargetStellar(nullptr);
+				const System *nextSystem = ship.NextWaypoint();
+				if(nextSystem)
+					newOrders.targetSystem = nextSystem;
+				else
+					newOrders.targetSystem = nullptr;
+			}
+		}
+	}
path: source/AI.cpp
user: login: petervdmeer
body: We all seem to agree that this code could be better (don't calculate each step what was already calculated in the last step), but the code does seem to be working and the calculations only happen whenever waypoints are actually used.
So with that I'm also okay to keep this as an open for a follow-up PR.
created_at: 2025-05-17T22:22:26Z
author_association: MEMBER
diff_hunk: @@ -1707,14 +1713,16 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 
 	int type = it->second.type;
+	const bool hasTravelOrder = type == Orders::MOVE_TO || type == Orders::TRAVEL_TO || type == Orders::LAND_ON;
+
 
path: source/AI.cpp
user: login: Amazinite
body: Double empty lines.
created_at: 2025-06-02T19:32:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1728,8 +1736,16 @@ bool AI::FollowOrders(Ship &ship, Command &command)
 		return false;
 	}
 
+
path: source/AI.cpp
user: login: Amazinite
body: Ditto.
created_at: 2025-06-02T19:33:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -276,6 +337,22 @@ void NPC::Load(const DataNode &node, const ConditionsStore *playerConditions)
 		ship->SetPersonality(personality);
 		ship->SetIsSpecial();
 		ship->FinishLoading(false);
+		if(!waypoints.empty())
+			ship->SetWaypoints(waypoints);
+		if(!stopovers.empty())
+			ship->SetStopovers(stopovers);
+		if(finalDestination)
+			ship->SetDestination(finalDestination);
+	}
+
+
path: source/NPC.cpp
user: login: Amazinite
body: Ditto.
created_at: 2025-06-02T19:33:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3824,6 +3895,66 @@ void Ship::SetTargetSystem(const System *system)
 
 
 
+// Persistent targets for mission NPCs.
+void Ship::SetDestination(const Planet *destination)
+{
+	destinationPlanet = destination;
+}
+
+
+
+void Ship::SetStopovers(const vector<const Planet *> &newStopovers)
+{
+	// Mark each planet as not visited.
+	for(const auto &it : newStopovers)
+		stopovers[it] = false;
+}
+
+
+
+void Ship::SetWaypoints(const vector<const System *> &newWaypoints)
+{
+	// Ships loaded from save files may have an existing waypoint that
+	// indicates which systems have already been visited.
+	if(waypoint < newWaypoints.size())
+	{
+		waypoints = newWaypoints;
+		if(targetSystem && personality.IsEntering())
+		{
+			--waypoint;
+			destinationSystem = targetSystem;
+		}
+		else
+			destinationSystem = newWaypoints[waypoint];
+	}
+	else
+		destinationSystem = nullptr;
+}
+
+
+
+const System *Ship::NextWaypoint()
+{
+	++waypoint;
+
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+	return destinationSystem;
+}
+
+
+
+void Ship::EraseWaypoint(const System *system)
+{
+	auto it = std::find(waypoints.begin(), waypoints.end(), system);
+	if(it == waypoints.end())
+		return;
+	if(waypoint > static_cast<unsigned>(std::distance(waypoints.begin(), it)))
+		--waypoint;
+	waypoints.erase(it);
+	destinationSystem = (waypoint < waypoints.size()) ? waypoints[waypoint] : nullptr;
+}
+
+
path: source/Ship.cpp
user: login: Amazinite
body: Missing newline.
created_at: 2025-06-02T19:35:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5249,3 +5397,9 @@ void Ship::Jettison(shared_ptr<Flotsam> toJettison)
 		++bayIndex;
 	}
 }
+
path: source/Ship.cpp
user: login: Amazinite
body: Two missing newlines.
created_at: 2025-06-02T19:35:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -675,6 +791,39 @@ NPC NPC::Instantiate(const PlayerInfo &player, map<string, string> &subs, const
 		result.system = location.PickSystem(origin);
 	if(!result.system)
 		result.system = (isAtDestination && destination) ? destination : origin;
path: source/NPC.cpp
user: login: Amazinite
body: Warp figured out that #11506 was caused by this line. `destination` used to be the system pointer parameter to this function. There is now a `destination` field on the class that is always null when Instantiate is called. The class variable having the same name allowed this to compile even though its meaning changed.

It seems like `result.destination` will always be non-null, so we can also simplify this while we're here.
```suggestion
		result.system = isAtDestination ? result.destination : origin;
```
created_at: 2025-06-04T02:22:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: mOctave
created_at: 2023-05-18T03:59:57Z
body: All right, I think this is back to a merge-ready position, although it'll definitely need review. I'm going to open it now.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2023-09-04T03:24:24Z
body: Last time it failed MacOS, now it's failed Windows Clang. I presume this is just the tests bugging out.
author_association: CONTRIBUTOR
user: login: Hurleveur
created_at: 2023-10-06T05:25:04Z
body: We should prob do an integration test for this, if you dont know how I can help you in discord or just open a pr to this pr
author_association: MEMBER
user: login: mOctave
created_at: 2023-10-06T14:41:16Z
body: I don't. This is pretty much all code from the original PR with merge conflicts, etc. fixed. I think a PR to the branch would probably work best.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2023-12-03T01:05:55Z
body: Yes, it probably does need more testing, as well as another set of eyes to point out comments I missed, etc. in the code, but maybe we can finally get this merged after 6 years, soon.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2024-05-03T03:38:19Z
body: I've decided to make land just work as-is and introduce the warning/change the PR description, since I think that way makes more sense.

Let me know if there's a better way to implement this.
author_association: CONTRIBUTOR
user: login: TomGoodIdea
created_at: 2024-05-03T07:46:04Z
body: I can make a PR to your branch if you need help, but please describe what's the intended behavior.
author_association: MEMBER
user: login: mOctave
created_at: 2024-05-05T19:47:29Z
body: It is intended that an NPC can have both a destination and stopovers. Probably only a single destination, and a map of stopovers. I think I know how to implement this, but it might take me a while.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2024-05-08T23:48:14Z
body: Okay, I think this works now, although I haven't tested everything. Please let me know if there's a better way to deal with this, or if it's okay as-is.

I've also added another criterion, `"outrun"`, that I thought was able to be worked around with `"evade"` but apparently isn't.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2024-05-13T02:54:13Z
body: Okay, so I've pulled the destination out of the stopover list and readjusted based on that, and things seem to be working now. I've done some tests with stopover+destination missions and destination-only missions, and they behave as expected, but more testing is probably needed.
author_association: CONTRIBUTOR
user: login: TomGoodIdea
created_at: 2024-05-13T18:07:46Z
body: > Has anyone tested how these instructions interact with travel restrictions?

~~lol I forgot that those exist~~, so nope, at least not me
author_association: MEMBER
user: login: mOctave
created_at: 2024-05-13T23:55:32Z
body: > Has anyone tested how these instructions interact with travel restrictions?

I also forgot those were a thing, so I haven't tested with them either.
author_association: CONTRIBUTOR
user: login: mOctave
created_at: 2024-07-09T04:09:34Z
body: Okay, so I think I've gotten this all working again. I've done tests and everything works as expected with stopovers/destinations/waypoints.

Travel restrictions appear to be ignored by this, which I think is probably the best way to go about it, since it would be incredibly complex to take them into account, and provide very little (if any) benefit.

The one thing is, I think maybe the behaviour of `uninterested` has changed? Ships with it don't try to get to their destinations, stopovers, and waypoints now. However, I'm inclined to call this a feature and not a bug, since they fulfill the right completion criteria when they do land.

@warp-core, I'd love another review when you're free.
author_association: CONTRIBUTOR
user: login: TheGiraffe3
created_at: 2025-01-08T10:27:29Z
body: @warp-core would you be able to get around to reviewing this any time soon?
author_association: MEMBER
user: login: warp-core
created_at: 2025-01-08T10:28:54Z
body: > @warp-core would you be able to get around to reviewing this any time soon?

That is the plan.
author_association: CONTRIBUTOR
user: login: ziproot
created_at: 2025-03-27T14:17:02Z
body: How does this work with `shrouded` systems? If you put a series of waypoints through the shroud so that once reaching one waypoint, it can reach the next one, but it can’t reach every waypoint immediately, will it make it through the shroud?
author_association: CONTRIBUTOR
user: login: petervdmeer
created_at: 2025-05-17T22:24:37Z
body: > How does this work with `shrouded` systems? If you put a series of waypoints through the shroud so that once reaching one waypoint, it can reach the next one, but it can’t reach every waypoint immediately, will it make it through the shroud?

It probably doesn't work, but the feature can already be useful for content creators when it is used outside of the shrouded systems. So here I feel that we should'nt focus on making this PR perfect, but to just accept the PR as good enough for now and tackle this in a follow-up.
author_association: MEMBER
user: login: bene-dictator
created_at: 2025-05-18T02:25:11Z
body: Has the wiki been updated yet?
author_association: MEMBER
user: login: mOctave
created_at: 2025-05-18T04:16:07Z
body: Yes
author_association: CONTRIBUTOR