OpenRCT2/src/openrct2/peep/GuestPathfinding.cpp

2148 lines
87 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2024 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "GuestPathfinding.h"
#include "../GameState.h"
#include "../core/Guard.hpp"
#include "../entity/Guest.h"
#include "../entity/Staff.h"
#include "../profiling/Profiling.h"
#include "../ride/RideData.h"
#include "../ride/Station.h"
#include "../ride/Track.h"
#include "../scenario/Scenario.h"
#include "../util/Util.h"
#include "../world/Entrance.h"
#include "../world/Footpath.h"
#include <bitset>
#include <cstring>
bool gPeepPathFindIgnoreForeignQueues;
RideId gPeepPathFindQueueRideIndex;
namespace OpenRCT2::PathFinding
{
static int8_t _peepPathFindNumJunctions;
static int8_t _peepPathFindMaxJunctions;
static int32_t _peepPathFindTilesChecked;
static int32_t GuestSurfacePathFinding(Peep& peep);
/* A junction history for the peep pathfinding heuristic search
* The magic number 16 is the largest value returned by
* PeepPathfindGetMaxNumberJunctions() which should eventually
* be declared properly. */
static struct
{
TileCoordsXYZ location;
Direction direction;
} _peepPathFindHistory[16];
enum class PathSearchResult
{
DeadEnd, // Path is a dead end, i.e. < 2 edges.
Wide, // Path with wide flag set.
Thin, // Path is simple.
Junction, // Path is a junction, i.e. > 2 edges.
RideQueue, // Queue path connected to a ride.
RideEntrance, // Map element is a ride entrance.
RideExit, // Map element is a ride exit.
ParkExit, // Park entrance / exit (map element is a park entrance/exit).
ShopEntrance, // Map element is a shop entrance.
Other, // Path is other than the above.
Loop, // Loop detected.
LimitReached, // Search limit reached without reaching path end.
Failed, // No path element found.
};
#pragma region Pathfinding Logging
// In case this is set to true it will enable code paths that log path finding. The peep will additionally
// require to have PEEP_FLAGS_DEBUG_PATHFINDING set in PeepFlags in order to activate logging.
static constexpr bool kLogPathfinding = false;
template<typename... TArgs>
static void LogPathfinding([[maybe_unused]] const Peep* peep, [[maybe_unused]] const char* format, TArgs&&... args)
{
if constexpr (kLogPathfinding)
{
if ((peep->PeepFlags & PEEP_FLAGS_DEBUG_PATHFINDING) == 0)
return;
char buffer[256];
snprintf(buffer, sizeof(buffer), format, std::forward<TArgs>(args)...);
if (peep != nullptr)
{
LOG_INFO("[%05u:%s] %s", peep->Id.ToUnderlying(), peep->GetName().c_str(), buffer);
}
else
{
LOG_INFO("%s", buffer);
}
}
}
static constexpr const char* PathSearchToString(PathSearchResult pathFindSearchResult)
{
switch (pathFindSearchResult)
{
case PathSearchResult::DeadEnd:
return "DeadEnd";
case PathSearchResult::Wide:
return "Wide";
case PathSearchResult::Thin:
return "Thin";
case PathSearchResult::Junction:
return "Junction";
case PathSearchResult::RideQueue:
return "RideQueue";
case PathSearchResult::RideEntrance:
return "RideEntrance";
case PathSearchResult::RideExit:
return "RideExit";
case PathSearchResult::ParkExit:
return "ParkEntryExit";
case PathSearchResult::ShopEntrance:
return "ShopEntrance";
case PathSearchResult::LimitReached:
return "LimitReached";
case PathSearchResult::Other:
return "Other";
case PathSearchResult::Loop:
return "Loop";
case PathSearchResult::Failed:
return "Failed";
// The default case is omitted intentionally.
}
return "Unknown";
}
#pragma endregion
static TileElement* GetBannerOnPath(TileElement* pathElement)
{
// This is an improved version of original.
// That only checked for one fence in the way.
if (pathElement->IsLastForTile())
return nullptr;
TileElement* bannerElement = pathElement + 1;
do
{
// Path on top, so no banners
if (bannerElement->GetType() == TileElementType::Path)
return nullptr;
// Found a banner
if (bannerElement->GetType() == TileElementType::Banner)
return bannerElement;
// Last element so there can't be any other banners
if (bannerElement->IsLastForTile())
return nullptr;
} while (bannerElement++ != nullptr);
return nullptr;
}
static int32_t BannerClearPathEdges(bool ignoreBanners, PathElement* pathElement, int32_t edges)
{
if (ignoreBanners)
return edges;
TileElement* bannerElement = GetBannerOnPath(reinterpret_cast<TileElement*>(pathElement));
if (bannerElement != nullptr)
{
do
{
edges &= bannerElement->AsBanner()->GetAllowedEdges();
} while ((bannerElement = GetBannerOnPath(bannerElement)) != nullptr);
}
return edges;
}
/**
* Gets the connected edges of a path that are permitted (i.e. no 'no entry' signs)
*/
static int32_t PathGetPermittedEdges(bool ignoreBanners, PathElement* pathElement)
{
return BannerClearPathEdges(ignoreBanners, pathElement, pathElement->GetEdgesAndCorners()) & 0x0F;
}
/**
*
* rct2: 0x0069524E
*/
static int32_t PeepMoveOneTile(Direction direction, Peep& peep)
{
assert(DirectionValid(direction));
auto newTile = CoordsXY{ CoordsXY{ peep.NextLoc } + CoordsDirectionDelta[direction] }.ToTileCentre();
if (newTile.x >= MAXIMUM_MAP_SIZE_BIG || newTile.y >= MAXIMUM_MAP_SIZE_BIG)
{
// This could loop!
return GuestSurfacePathFinding(peep);
}
peep.PeepDirection = direction;
if (peep.State != PeepState::Queuing)
{
// When peeps are walking along a path, we would like them to be spread out across the width of the path,
// instead of all walking along the exact centre line of the path.
//
// Setting a random DestinationTolerance does not work very well for this. It means that peeps will make
// their new pathfinding decision at a random time, and so will distribute a bit when they are turning
// corners (which is good); but, as they walk along a straight path, they will - eventually - have had a
// low tolerance value which forced them back to the centre of the path, where they stay until they turn
// a corner.
//
// What we want instead is to apply that randomness in the direction they are walking ONLY, and keep their
// other coordinate constant.
//
// However, we have also seen some situations where guests end up too far from the centre of paths. We've
// not identified exactly what causes this yet, but to limit the impact of it, we don't just keep the other
// coordinate constant, but instead clamp it to an acceptable range. This brings in 'outlier' guests from
// the edges of the path, while allowing guests who are already in an acceptable position to stay there.
const int8_t offset = (ScenarioRand() & 7) - 3;
if (direction == 0 || direction == 2)
{
// Peep is moving along X, so apply the offset to the X position of the destination and clamp their current Y
const int32_t centreLine = (peep.y & 0xFFE0) + COORDS_XY_HALF_TILE;
newTile.x += offset;
newTile.y = std::clamp<int32_t>(peep.y, centreLine - 3, centreLine + 3);
}
else
{
// Peep is moving along Y, so apply the offset to the Y position of the destination and clamp their current X
const int32_t centreLine = (peep.x & 0xFFE0) + COORDS_XY_HALF_TILE;
newTile.x = std::clamp<int32_t>(peep.x, centreLine - 3, centreLine + 3);
newTile.y += offset;
}
}
peep.SetDestination(newTile, 2);
return 0;
}
/**
*
* rct2: 0x00694C41
*/
static int32_t GuestSurfacePathFinding(Peep& peep)
{
auto pathPos = CoordsXYRangedZ{ peep.NextLoc, peep.NextLoc.z, peep.NextLoc.z + PATH_CLEARANCE };
Direction randDirection = ScenarioRand() & 3;
if (!WallInTheWay(pathPos, randDirection))
{
pathPos.x += CoordsDirectionDelta[randDirection].x;
pathPos.y += CoordsDirectionDelta[randDirection].y;
Direction backwardsDirection = DirectionReverse(randDirection);
if (!WallInTheWay(pathPos, backwardsDirection))
{
if (!MapSurfaceIsBlocked(pathPos))
{
return PeepMoveOneTile(randDirection, peep);
}
}
}
randDirection++;
uint8_t rand_backwards = ScenarioRand() & 1;
if (rand_backwards)
{
randDirection -= 2;
}
randDirection &= 3;
pathPos.x = peep.NextLoc.x;
pathPos.y = peep.NextLoc.y;
if (!WallInTheWay(pathPos, randDirection))
{
pathPos.x += CoordsDirectionDelta[randDirection].x;
pathPos.y += CoordsDirectionDelta[randDirection].y;
Direction backwardsDirection = DirectionReverse(randDirection);
if (!WallInTheWay(pathPos, backwardsDirection))
{
if (!MapSurfaceIsBlocked(pathPos))
{
return PeepMoveOneTile(randDirection, peep);
}
}
}
randDirection -= 2;
randDirection &= 3;
pathPos.x = peep.NextLoc.x;
pathPos.y = peep.NextLoc.y;
if (!WallInTheWay(pathPos, randDirection))
{
pathPos.x += CoordsDirectionDelta[randDirection].x;
pathPos.y += CoordsDirectionDelta[randDirection].y;
Direction backwardsDirection = DirectionReverse(randDirection);
if (!WallInTheWay(pathPos, backwardsDirection))
{
if (!MapSurfaceIsBlocked(pathPos))
{
return PeepMoveOneTile(randDirection, peep);
}
}
}
randDirection--;
if (rand_backwards)
{
randDirection += 2;
}
randDirection &= 3;
return PeepMoveOneTile(randDirection, peep);
}
/**
*
* Returns:
* PathSearchResult::Wide - path with wide flag set
* PathSearchResult::RideQueue - queue path connected to a ride
* PathSearchResult::Other - other path than the above
* PathSearchResult::Failed - no path element found
*
* rct2: 0x00694BAE
*
* Returns the type of the next footpath tile a peep can get to from x,y,z /
* inputTileElement in the given direction.
*/
static PathSearchResult FootpathElementNextInDirection(
TileCoordsXYZ loc, PathElement* pathElement, Direction chosenDirection)
{
TileElement* nextTileElement;
if (pathElement->IsSloped())
{
if (pathElement->GetSlopeDirection() == chosenDirection)
{
loc.z += 2;
}
}
loc += TileDirectionDelta[chosenDirection];
nextTileElement = MapGetFirstElementAt(loc);
do
{
if (nextTileElement == nullptr)
break;
if (nextTileElement->IsGhost())
continue;
if (nextTileElement->GetType() != TileElementType::Path)
continue;
if (!IsValidPathZAndDirection(nextTileElement, loc.z, chosenDirection))
continue;
if (nextTileElement->AsPath()->IsWide())
return PathSearchResult::Wide;
// Only queue tiles that are connected to a ride are returned as ride queues.
if (nextTileElement->AsPath()->IsQueue() && !nextTileElement->AsPath()->GetRideIndex().IsNull())
return PathSearchResult::RideQueue;
return PathSearchResult::Other;
} while (!(nextTileElement++)->IsLastForTile());
return PathSearchResult::Failed;
}
/**
*
* Returns:
* PathSearchResult::DeadEnd - path is a dead end, i.e. < 2 edges
* PathSearchResult::Wide - path with wide flag set
* PathSearchResult::Junction - path is a junction, i.e. > 2 edges
* PathSearchResult::RideEntrance - map element is a ride entrance
* PathSearchResult::RideExit - map element is a ride exit
* PathSearchResult::ParkExit - park entrance / exit (map element is a park entrance/exit)
* PathSearchResult::ShopEntrance - map element is a shop entrance
* PathSearchResult::LimitReached - search limit reached without reaching path end
* PathSearchResult::Failed - no path element found
* For return values RideEntrance, RideExit & ShopEntrance the rideIndex is stored in outRideIndex.
*
* rct2: 0x006949B9
*
* This is the recursive portion of FootpathElementDestinationInDirection().
*/
static PathSearchResult FootpathElementDestInDir(
bool ignoreBanners, TileCoordsXYZ loc, Direction chosenDirection, RideId* outRideIndex, int32_t level)
{
TileElement* tileElement;
Direction direction;
if (level > 25)
return PathSearchResult::LimitReached;
loc += TileDirectionDelta[chosenDirection];
tileElement = MapGetFirstElementAt(loc);
if (tileElement == nullptr)
{
return PathSearchResult::Failed;
}
do
{
if (tileElement->IsGhost())
continue;
switch (tileElement->GetType())
{
case TileElementType::Track:
{
if (loc.z != tileElement->BaseHeight)
continue;
RideId rideIndex = tileElement->AsTrack()->GetRideIndex();
auto ride = GetRide(rideIndex);
if (ride != nullptr && ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
{
*outRideIndex = rideIndex;
return PathSearchResult::ShopEntrance;
}
}
break;
case TileElementType::Entrance:
if (loc.z != tileElement->BaseHeight)
continue;
switch (tileElement->AsEntrance()->GetEntranceType())
{
case ENTRANCE_TYPE_RIDE_ENTRANCE:
direction = tileElement->GetDirection();
if (direction == chosenDirection)
{
*outRideIndex = tileElement->AsEntrance()->GetRideIndex();
return PathSearchResult::RideEntrance;
}
break;
case ENTRANCE_TYPE_RIDE_EXIT:
direction = tileElement->GetDirection();
if (direction == chosenDirection)
{
*outRideIndex = tileElement->AsEntrance()->GetRideIndex();
return PathSearchResult::RideExit;
}
break;
case ENTRANCE_TYPE_PARK_ENTRANCE:
return PathSearchResult::ParkExit;
}
break;
case TileElementType::Path:
{
if (!IsValidPathZAndDirection(tileElement, loc.z, chosenDirection))
continue;
if (tileElement->AsPath()->IsWide())
return PathSearchResult::Wide;
uint8_t edges = PathGetPermittedEdges(ignoreBanners, tileElement->AsPath());
edges &= ~(1 << DirectionReverse(chosenDirection));
loc.z = tileElement->BaseHeight;
for (Direction dir : ALL_DIRECTIONS)
{
if (!(edges & (1 << dir)))
continue;
edges &= ~(1 << dir);
if (edges != 0)
return PathSearchResult::Junction;
if (tileElement->AsPath()->IsSloped())
{
if (tileElement->AsPath()->GetSlopeDirection() == dir)
{
loc.z += 2;
}
}
return FootpathElementDestInDir(ignoreBanners, loc, dir, outRideIndex, level + 1);
}
return PathSearchResult::DeadEnd;
}
default:
break;
}
} while (!(tileElement++)->IsLastForTile());
return PathSearchResult::Failed;
}
/**
* Returns:
* PathSearchResult::DeadEnd - path is a dead end, i.e. < 2 edges
* PathSearchResult::Wide - path with wide flag set
* PathSearchResult::Junction - path is a junction, i.e. > 2 edges
* PathSearchResult::RideEntrance - map element is a ride entrance
* PathSearchResult::RideExit - map element is a ride exit
* PathSearchResult::ParkExit - ark entrance / exit (map element is a park entrance/exit
* PathSearchResult::ShopEntrance - map element is a shop entrance
* PathSearchResult::LimitReached - search limit reached without reaching path end
* PathSearchResult::Failed - no path element found
* For return values RideEntrance, RideExit & ShopEntrance the rideIndex is stored in outRideIndex.
*
* rct2: 0x006949A4
*
* Returns the destination tile type a peep can get to from x,y,z /
* inputTileElement in the given direction following single width paths only
* and stopping as soon as a path junction is encountered.
* Note that a junction is a path with > 2 reachable neighbouring path tiles,
* so wide paths have LOTS of junctions.
* This is useful for finding out what is at the end of a short single
* width path, for example that leads from a ride exit back to the main path.
*/
static PathSearchResult FootpathElementDestinationInDirection(
TileCoordsXYZ loc, PathElement* pathElement, Direction chosenDirection, RideId* outRideIndex)
{
if (pathElement->IsSloped())
{
if (pathElement->GetSlopeDirection() == chosenDirection)
{
loc.z += 2;
}
}
// This function is only called for guests, never ignore the banners.
return FootpathElementDestInDir(false, loc, chosenDirection, outRideIndex, 0);
}
/**
*
* rct2: 0x00695225
*/
static int32_t GuestPathfindAimless(Peep& peep, uint8_t edges)
{
if (ScenarioRand() & 1)
{
// If possible go straight
if (edges & (1 << peep.PeepDirection))
{
return PeepMoveOneTile(peep.PeepDirection, peep);
}
}
while (true)
{
Direction direction = ScenarioRand() & 3;
// Otherwise go in a random direction allowed from the tile.
if (edges & (1 << direction))
{
return PeepMoveOneTile(direction, peep);
}
}
}
/**
*
* rct2: 0x0069A60A
*/
static uint8_t PeepPathfindGetMaxNumberJunctions(Peep& peep)
{
if (peep.Is<Staff>())
return 8;
auto* guest = peep.As<Guest>();
if (guest == nullptr)
return 8;
if (guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK && guest->GuestIsLostCountdown < 90)
{
return 8;
}
if (guest->HasItem(ShopItem::Map))
return 7;
if (guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
return 7;
return 5;
}
/**
* Returns if the path as xzy is a 'thin' junction.
* A junction is considered 'thin' if it has more than 2 edges
* leading to/from non-wide path elements; edges leading to/from non-path
* elements (e.g. ride/shop entrances) or ride queues are not counted,
* since entrances and ride queues coming off a path should not result in
* the path being considered a junction.
*/
static bool PathIsThinJunction(PathElement* path, const TileCoordsXYZ& loc)
{
PROFILED_FUNCTION();
uint8_t edges = path->GetEdges();
int32_t testEdge = UtilBitScanForward(edges);
if (testEdge == -1)
return false;
bool isThinJunction = false;
int32_t thinCount = 0;
do
{
auto nextFootpathResult = FootpathElementNextInDirection(loc, path, testEdge);
/* Ignore non-paths (e.g. ride entrances, shops), wide paths
* and ride queues (per ignoreQueues) when counting
* neighbouring tiles. */
if (nextFootpathResult != PathSearchResult::Failed && nextFootpathResult != PathSearchResult::Wide
&& nextFootpathResult != PathSearchResult::RideQueue)
{
thinCount++;
}
if (thinCount > 2)
{
isThinJunction = true;
break;
}
edges &= ~(1 << testEdge);
} while ((testEdge = UtilBitScanForward(edges)) != -1);
return isThinJunction;
}
static int32_t CalculateHeuristicPathingScore(const TileCoordsXYZ& loc1, const TileCoordsXYZ& loc2)
{
auto xDelta = abs(loc1.x - loc2.x) * 32;
auto yDelta = abs(loc1.y - loc2.y) * 32;
auto zDelta = abs(loc1.z - loc2.z) * 2;
if (xDelta < yDelta)
xDelta >>= 4;
else
yDelta >>= 4;
return xDelta + yDelta + zDelta;
}
/**
* Searches for the tile with the best heuristic score within the search limits
* starting from the given tile x,y,z and going in the given direction test_edge.
* The best heuristic score is tracked and returned in the call parameters
* along with the corresponding tile location and search path telemetry
* (junctions passed through and directions taken).
*
* The primary heuristic used is distance from the goal; the secondary
* heuristic used (when the primary heuristic gives equal scores) is the number
* of steps. i.e. the search gets as close as possible to the goal in as few
* steps as possible.
*
* Each tile is checked to determine if the goal is reached.
* When the goal is not reached the search result is only updated at the END
* of each search path (some map element that is not a path or a path at which
* a search limit is reached), NOT at each step along the way.
* This means that the search ignores thin paths that are "no through paths"
* no matter how close to the goal they get, but will follow possible "through
* paths".
*
* The implementation is a depth first search of the path layout in xyz
* according to the search limits.
* Unlike an A* search, which tracks for each tile a heuristic score (a
* function of the xyz distances to the goal) and cost of reaching that tile
* (steps to the tile), a single best result "so far" (best heuristic score
* with least cost) is tracked via the score parameter.
* With this approach, explicit loop detection is necessary to limit the
* search space, and each alternate route through the same tile can be
* returned as the best result, rather than only the shortest route with A*.
*
* The parameters that hold the best search result so far are:
* - score - the least heuristic distance from the goal
* - endSteps - the least number of steps that achieve the score.
*
* The following parameters provide telemetry information on best search path so far:
* - endXYZ tracks the end location of the search path.
* - endSteps tracks the number of steps to the end of the search path.
* - endJunctions tracks the number of junctions passed through in the
* search path.
* - junctionList[] and directionList[] track the junctions and
* corresponding directions of the search path.
* Other than debugging purposes, these could potentially be used to visualise
* the pathfinding on the map.
*
* The parameters/variables that limit the search space are:
* - counter (param) - number of steps walked in the current search path;
* - _peepPathFindTilesChecked (variable) - cumulative number of tiles that can be
* checked in the entire search;
* - _peepPathFindNumJunctions (variable) - number of thin junctions that can be
* checked in a single search path;
*
* Other global variables/state that affect the search space are:
* - Wide paths - to handle broad paths (> 1 tile wide), the search navigates
* along non-wide (or 'thin' paths) and stops as soon as it encounters a
* wide path. This means peeps heading for a destination will only leave
* thin paths if walking 1 tile onto a wide path is closer than following
* non-wide paths;
* - gPeepPathFindIgnoreForeignQueues
* - gPeepPathFindQueueRideIndex - the ride the peep is heading for
* - _peepPathFindHistory - the search path telemetry consisting of the
* starting point and all thin junctions with directions navigated
* in the current search path - also used to detect path loops.
*
* The score is only updated when:
* - the goal is reached;
* - a wide tile is encountered with a better search result - the goal may
* still be reachable from here (only if the current tile is also wide);
* - a junction is encountered with a better search result and
* maxNumJunctions is exceeded - the goal may still be reachable from here;
* - returning from a recursive call if a search limit (i.e. either
* maxNumStep or maxTilesChecked) was reached and the current tile has a
* better search result and the goal may still be reachable from here
* (i.e. not a dead end path tile).
*
* rct2: 0x0069A997
*/
static void PeepPathfindHeuristicSearch(
TileCoordsXYZ loc, const TileCoordsXYZ& goal, const Peep& peep, TileElement* currentTileElement,
const bool inPatrolArea, uint8_t numSteps, uint16_t* endScore, Direction testEdge, uint8_t* endJunctions,
TileCoordsXYZ junctionList[16], uint8_t directionList[16], TileCoordsXYZ* endXYZ, uint8_t* endSteps)
{
PathSearchResult searchResult = PathSearchResult::Failed;
bool currentElementIsWide = currentTileElement->AsPath()->IsWide();
if (currentElementIsWide)
{
const Staff* staff = peep.As<Staff>();
if (staff != nullptr && staff->CanIgnoreWideFlag(loc.ToCoordsXYZ(), currentTileElement))
currentElementIsWide = false;
}
loc += TileDirectionDelta[testEdge];
++numSteps;
_peepPathFindTilesChecked--;
/* If this is where the search started this is a search loop and the
* current search path ends here.
* Return without updating the parameters (best result so far). */
if (_peepPathFindHistory[0].location == loc)
{
LogPathfinding(&peep, "Return from %d,%d,%d; Steps: %u; At start", loc.x >> 5, loc.y >> 5, loc.z, numSteps);
return;
}
bool nextInPatrolArea = inPatrolArea;
auto* staff = peep.As<Staff>();
if (staff != nullptr && staff->IsMechanic())
{
nextInPatrolArea = staff->IsLocationInPatrol(loc.ToCoordsXY());
if (inPatrolArea && !nextInPatrolArea)
{
/* The mechanic will leave his patrol area by taking
* the test_edge so the current search path ends here.
* Return without updating the parameters (best result so far). */
LogPathfinding(
&peep, "Return from %d,%d,%d; Steps: %u; Left patrol area", loc.x >> 5, loc.y >> 5, loc.z, numSteps);
return;
}
}
/* Get the next map element of interest in the direction of testEdge. */
bool found = false;
TileElement* tileElement = MapGetFirstElementAt(loc);
if (tileElement == nullptr)
{
return;
}
do
{
/* Look for all map elements that the peep could walk onto while
* navigating to the goal, including the goal tile. */
if (tileElement->IsGhost())
continue;
RideId rideIndex = RideId::GetNull();
switch (tileElement->GetType())
{
case TileElementType::Track:
{
if (loc.z != tileElement->BaseHeight)
continue;
/* For peeps heading for a shop, the goal is the shop
* tile. */
rideIndex = tileElement->AsTrack()->GetRideIndex();
auto ride = GetRide(rideIndex);
if (ride == nullptr || !ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP_OR_FACILITY))
continue;
found = true;
searchResult = PathSearchResult::ShopEntrance;
break;
}
case TileElementType::Entrance:
if (loc.z != tileElement->BaseHeight)
continue;
Direction direction;
searchResult = PathSearchResult::Other;
switch (tileElement->AsEntrance()->GetEntranceType())
{
case ENTRANCE_TYPE_RIDE_ENTRANCE:
/* For peeps heading for a ride without a queue, the
* goal is the ride entrance tile.
* For mechanics heading for the ride entrance
* (in the case when the station has no exit),
* the goal is the ride entrance tile. */
direction = tileElement->GetDirection();
if (direction == testEdge)
{
/* The rideIndex will be useful for
* adding transport rides later. */
rideIndex = tileElement->AsEntrance()->GetRideIndex();
searchResult = PathSearchResult::RideEntrance;
found = true;
break;
}
continue; // Ride entrance is not facing the right direction.
case ENTRANCE_TYPE_PARK_ENTRANCE:
/* For peeps leaving the park, the goal is the park
* entrance/exit tile. */
searchResult = PathSearchResult::ParkExit;
found = true;
break;
case ENTRANCE_TYPE_RIDE_EXIT:
/* For mechanics heading for the ride exit, the
* goal is the ride exit tile. */
direction = tileElement->GetDirection();
if (direction == testEdge)
{
searchResult = PathSearchResult::RideExit;
found = true;
break;
}
continue; // Ride exit is not facing the right direction.
default:
continue;
}
break;
case TileElementType::Path:
{
/* For peeps heading for a ride with a queue, the goal is the last
* queue path.
* Otherwise, peeps walk on path tiles to get to the goal. */
if (!IsValidPathZAndDirection(tileElement, loc.z, testEdge))
continue;
// Path may be sloped, so set z to path base height.
loc.z = tileElement->BaseHeight;
if (tileElement->AsPath()->IsWide())
{
/* Check if staff can ignore this wide flag. */
if (staff == nullptr || !staff->CanIgnoreWideFlag(loc.ToCoordsXYZ(), tileElement))
{
searchResult = PathSearchResult::Wide;
found = true;
break;
}
}
searchResult = PathSearchResult::Thin;
uint8_t numEdges = BitCount(tileElement->AsPath()->GetEdges());
if (numEdges < 2)
{
searchResult = PathSearchResult::DeadEnd;
}
else if (numEdges > 2)
{
searchResult = PathSearchResult::Junction;
}
else
{ // numEdges == 2
if (tileElement->AsPath()->IsQueue()
&& tileElement->AsPath()->GetRideIndex() != gPeepPathFindQueueRideIndex)
{
if (gPeepPathFindIgnoreForeignQueues && !tileElement->AsPath()->GetRideIndex().IsNull())
{
// Path is a queue we aren't interested in
/* The rideIndex will be useful for
* adding transport rides later. */
rideIndex = tileElement->AsPath()->GetRideIndex();
searchResult = PathSearchResult::RideQueue;
}
}
}
found = true;
}
break;
default:
continue;
}
LogPathfinding(
&peep, "Checking map element at %d,%d,%d; Type: %s; Steps: %u", loc.x >> 5, loc.y >> 5, loc.z,
PathSearchToString(searchResult), numSteps);
/* At this point tileElement is of interest to the pathfinding. */
/* Should we check that this tileElement is connected in the
* reverse direction? For some tileElement types this was
* already done above (e.g. ride entrances), but for others not.
* Ignore for now. */
// Calculate the heuristic score of this map element.
uint16_t newScore = CalculateHeuristicPathingScore(loc, goal);
/* If this map element is the search goal the current search path ends here. */
if (newScore == 0)
{
/* If the search result is better than the best so far (in the parameters),
* then update the parameters with this search before continuing to the next map element. */
if (newScore < *endScore || (newScore == *endScore && numSteps < *endSteps))
{
// Update the search results
*endScore = newScore;
*endSteps = numSteps;
// Update the end x,y,z
*endXYZ = loc;
// Update the telemetry
*endJunctions = _peepPathFindMaxJunctions - _peepPathFindNumJunctions;
for (uint8_t junctInd = 0; junctInd < *endJunctions; junctInd++)
{
uint8_t histIdx = _peepPathFindMaxJunctions - junctInd;
junctionList[junctInd].x = _peepPathFindHistory[histIdx].location.x;
junctionList[junctInd].y = _peepPathFindHistory[histIdx].location.y;
junctionList[junctInd].z = _peepPathFindHistory[histIdx].location.z;
directionList[junctInd] = _peepPathFindHistory[histIdx].direction;
}
}
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; At goal; Score: %d", loc.x >> 5, loc.y >> 5, loc.z,
numSteps, newScore);
continue;
}
/* At this point the map element tile is not the goal. */
/* If this map element is not a path, the search cannot be continued.
* Continue to the next map element without updating the parameters (best result so far). */
if (searchResult != PathSearchResult::DeadEnd && searchResult != PathSearchResult::Thin
&& searchResult != PathSearchResult::Junction && searchResult != PathSearchResult::Wide)
{
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; Not a path", loc.x >> 5, loc.y >> 5, loc.z, numSteps);
continue;
}
/* At this point the map element is a path. */
/* If this is a wide path the search ends here. */
if (searchResult == PathSearchResult::Wide)
{
/* Ignore Wide paths as continuing paths UNLESS
* the current path is also Wide (and, for staff, not ignored).
* This permits a peep currently on a wide path to
* cross other wide paths to reach a thin path.
*
* So, if the current path is also wide the goal could
* still be reachable from here.
* If the search result is better than the best so far
* (in the parameters), then update the parameters with
* this search before continuing to the next map element. */
if (currentElementIsWide && (newScore < *endScore || (newScore == *endScore && numSteps < *endSteps)))
{
// Update the search results
*endScore = newScore;
*endSteps = numSteps;
// Update the end x,y,z
*endXYZ = loc;
// Update the telemetry
*endJunctions = _peepPathFindMaxJunctions - _peepPathFindNumJunctions;
for (uint8_t junctInd = 0; junctInd < *endJunctions; junctInd++)
{
uint8_t histIdx = _peepPathFindMaxJunctions - junctInd;
junctionList[junctInd].x = _peepPathFindHistory[histIdx].location.x;
junctionList[junctInd].y = _peepPathFindHistory[histIdx].location.y;
junctionList[junctInd].z = _peepPathFindHistory[histIdx].location.z;
directionList[junctInd] = _peepPathFindHistory[histIdx].direction;
}
}
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; Wide path; Score: %d", loc.x >> 5, loc.y >> 5, loc.z,
numSteps, newScore);
continue;
}
/* At this point the map element is a non-wide path.*/
/* Get all the permitted_edges of the map element. */
Guard::Assert(tileElement->AsPath() != nullptr);
uint8_t edges = PathGetPermittedEdges(staff != nullptr, tileElement->AsPath());
LogPathfinding(
&peep, "Path element at %d,%d,%d; Steps: %u; Edges (0123):%d%d%d%d; Reverse: %d", loc.x >> 5, loc.y >> 5, loc.z,
numSteps, edges & 1, (edges & 2) >> 1, (edges & 4) >> 2, (edges & 8) >> 3, testEdge ^ 2);
/* Remove the reverse edge (i.e. the edge back to the previous map element.) */
edges &= ~(1 << DirectionReverse(testEdge));
int32_t nextTestEdge = UtilBitScanForward(edges);
/* If there are no other edges the current search ends here.
* Continue to the next map element without updating the parameters (best result so far). */
if (nextTestEdge == -1)
{
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; No more edges/dead end", loc.x >> 5, loc.y >> 5, loc.z,
numSteps);
continue;
}
/* Check if either of the search limits has been reached:
* - max number of steps or max tiles checked. */
if (numSteps >= 200 || _peepPathFindTilesChecked <= 0)
{
/* The current search ends here.
* The path continues, so the goal could still be reachable from here.
* If the search result is better than the best so far (in the parameters),
* then update the parameters with this search before continuing to the next map element. */
if (newScore < *endScore || (newScore == *endScore && numSteps < *endSteps))
{
// Update the search results
*endScore = newScore;
*endSteps = numSteps;
// Update the end x,y,z
*endXYZ = loc;
// Update the telemetry
*endJunctions = _peepPathFindMaxJunctions - _peepPathFindNumJunctions;
for (uint8_t junctInd = 0; junctInd < *endJunctions; junctInd++)
{
uint8_t histIdx = _peepPathFindMaxJunctions - junctInd;
junctionList[junctInd].x = _peepPathFindHistory[histIdx].location.x;
junctionList[junctInd].y = _peepPathFindHistory[histIdx].location.y;
junctionList[junctInd].z = _peepPathFindHistory[histIdx].location.z;
directionList[junctInd] = _peepPathFindHistory[histIdx].direction;
}
}
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; Search limit reached; Score: %d", loc.x >> 5, loc.y >> 5,
loc.z, numSteps, newScore);
continue;
}
bool isThinJunction = false;
if (searchResult == PathSearchResult::Junction)
{
/* Check if this is a thin junction. And perform additional
* necessary checks. */
isThinJunction = PathIsThinJunction(tileElement->AsPath(), loc);
if (isThinJunction)
{
/* The current search path is passing through a thin
* junction on this map element. Only 'thin' junctions
* are counted towards the junction search limit. */
/* First check if going through the junction would be
* a loop. If so, the current search path ends here.
* Path finding loop detection can take advantage of both the
* peep.PathfindHistory - loops through remembered junctions
* the peep has already passed through getting to its
* current position while on the way to its current goal;
* _peepPathFindHistory - loops in the current search path. */
bool pathLoop = false;
/* Check the peep.PathfindHistory to see if this junction has
* already been visited by the peep while heading for this goal. */
for (auto& pathfindHistory : peep.PathfindHistory)
{
if (pathfindHistory == loc)
{
if (pathfindHistory.direction == 0)
{
/* If all directions have already been tried while
* heading to this goal, this is a loop. */
pathLoop = true;
}
else
{
/* The peep remembers walking through this junction
* before, but has not yet tried all directions.
* Limit the edges to search to those not yet tried. */
edges &= pathfindHistory.direction;
}
break;
}
}
if (!pathLoop)
{
/* Check the _peepPathFindHistory to see if this junction has been
* previously passed through in the current search path.
* i.e. this is a loop in the current search path. */
for (int32_t junctionNum = _peepPathFindNumJunctions + 1; junctionNum <= _peepPathFindMaxJunctions;
junctionNum++)
{
if (_peepPathFindHistory[junctionNum].location == loc)
{
pathLoop = true;
break;
}
}
}
if (pathLoop)
{
/* Loop detected. The current search path ends here.
* Continue to the next map element without updating the parameters (best result so far). */
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; Loop", loc.x >> 5, loc.y >> 5, loc.z, numSteps);
continue;
}
/* If the junction search limit is reached, the
* current search path ends here. The goal may still
* be reachable from here.
* If the search result is better than the best so far (in the parameters),
* then update the parameters with this search before continuing to the next map element. */
if (_peepPathFindNumJunctions <= 0)
{
if (newScore < *endScore || (newScore == *endScore && numSteps < *endSteps))
{
// Update the search results
*endScore = newScore;
*endSteps = numSteps;
// Update the end x,y,z
*endXYZ = loc;
// Update the telemetry
*endJunctions = _peepPathFindMaxJunctions; // - _peepPathFindNumJunctions;
for (uint8_t junctInd = 0; junctInd < *endJunctions; junctInd++)
{
uint8_t histIdx = _peepPathFindMaxJunctions - junctInd;
junctionList[junctInd] = _peepPathFindHistory[histIdx].location;
directionList[junctInd] = _peepPathFindHistory[histIdx].direction;
}
}
LogPathfinding(
&peep, "Search path ends at %d,%d,%d; Steps: %u; NumJunctions < 0; Score: %d", loc.x >> 5,
loc.y >> 5, loc.z, numSteps, newScore);
continue;
}
/* This junction was NOT previously visited in the current
* search path, so add the junction to the history. */
_peepPathFindHistory[_peepPathFindNumJunctions].location = loc;
// .direction take is added below.
_peepPathFindNumJunctions--;
}
}
/* Continue searching down each remaining edge of the path
* (recursive call). */
do
{
edges &= ~(1 << nextTestEdge);
uint8_t savedNumJunctions = _peepPathFindNumJunctions;
uint8_t height = loc.z;
if (tileElement->AsPath()->IsSloped() && tileElement->AsPath()->GetSlopeDirection() == nextTestEdge)
{
height += 2;
}
if constexpr (kLogPathfinding)
{
if (searchResult == PathSearchResult::Junction)
{
if (isThinJunction)
LogPathfinding(
&peep, "Recurse from %d,%d,%d; Steps: %u; edge: %d; Thin-Junction", loc.x >> 5, loc.y >> 5,
loc.z, numSteps, nextTestEdge);
else
LogPathfinding(
&peep, "Recurse from %d,%d,%d; Steps: %u; edge: %d; Wide-Junction", loc.x >> 5, loc.y >> 5,
loc.z, numSteps, nextTestEdge);
}
else
{
LogPathfinding(
&peep, "Recurse from %d,%d,%d; Steps: %u; edge: %d; Segment", loc.x >> 5, loc.y >> 5, loc.z,
numSteps, nextTestEdge);
}
}
if (isThinJunction)
{
/* Add the current test_edge to the history. */
_peepPathFindHistory[_peepPathFindNumJunctions + 1].direction = nextTestEdge;
}
PeepPathfindHeuristicSearch(
{ loc.x, loc.y, height }, goal, peep, tileElement, nextInPatrolArea, numSteps, endScore, nextTestEdge,
endJunctions, junctionList, directionList, endXYZ, endSteps);
_peepPathFindNumJunctions = savedNumJunctions;
LogPathfinding(
&peep, "Returned to %d,%d,%d; Steps: %u; edge: %d; Score: %d", loc.x >> 5, loc.y >> 5, loc.z, numSteps,
nextTestEdge, *endScore);
} while ((nextTestEdge = UtilBitScanForward(edges)) != -1);
} while (!(tileElement++)->IsLastForTile());
if (!found)
{
/* No map element could be found.
* Return without updating the parameters (best result so far). */
LogPathfinding(
&peep, "Returning from %d,%d,%d; Steps: %u; No relevant map element found", loc.x >> 5, loc.y >> 5, loc.z,
numSteps);
}
else
{
LogPathfinding(
&peep, "Returning from %d,%d,%d; Steps: %u; All map elements checked", loc.x >> 5, loc.y >> 5, loc.z, numSteps);
}
}
/**
* Returns:
* -1 - no direction chosen
* 0..3 - chosen direction
*
* rct2: 0x0069A5F0
*/
Direction ChooseDirection(const TileCoordsXYZ& loc, const TileCoordsXYZ& goal, Peep& peep)
{
PROFILED_FUNCTION();
// The max number of thin junctions searched - a per-search-path limit.
_peepPathFindMaxJunctions = PeepPathfindGetMaxNumberJunctions(peep);
/* The max number of tiles to check - a whole-search limit.
* Mainly to limit the performance impact of the path finding. */
int32_t maxTilesChecked = (peep.Is<Staff>()) ? 50000 : 15000;
LogPathfinding(&peep, "Choose direction for goal %d,%d,%d from %d,%d,%d", goal.x, goal.y, goal.z, loc.x, loc.y, loc.z);
// Get the path element at this location
TileElement* destTileElement = MapGetFirstElementAt(loc);
/* Where there are multiple matching map elements placed with zero
* clearance, save the first one for later use to determine the path
* slope - this maintains the original behaviour (which only processes
* the first matching map element found) and is consistent with peep
* placement (i.e. height) on such paths with differing slopes.
*
* I cannot see a legitimate reason for building overlaid paths with
* differing slopes and do not recall ever seeing this in practise.
* Normal cases I have seen in practise are overlaid paths with the
* same slope (flat) in order to place scenery (e.g. benches) in the
* middle of a wide path that can still be walked through.
* Anyone attempting to overlay paths with different slopes should
* EXPECT to experience path finding irregularities due to those paths!
* In particular common edges at different heights will not work
* in a useful way. Simply do not do it! :-) */
TileElement* firstTileElement = nullptr;
bool found = false;
uint8_t permittedEdges = 0;
bool isThin = false;
do
{
if (destTileElement == nullptr)
break;
if (destTileElement->BaseHeight != loc.z)
continue;
if (destTileElement->GetType() != TileElementType::Path)
continue;
found = true;
if (firstTileElement == nullptr)
{
firstTileElement = destTileElement;
}
/* Check if this path element is a thin junction.
* Only 'thin' junctions are remembered in peep.PathfindHistory.
* NO attempt is made to merge the overlaid path elements and
* check if the combination is 'thin'!
* The junction is considered 'thin' simply if any of the
* overlaid path elements there is a 'thin junction'. */
isThin = isThin || PathIsThinJunction(destTileElement->AsPath(), loc);
// Collect the permitted edges of ALL matching path elements at this location.
permittedEdges |= PathGetPermittedEdges(peep.Is<Staff>(), destTileElement->AsPath());
} while (!(destTileElement++)->IsLastForTile());
// Peep is not on a path.
if (!found)
return INVALID_DIRECTION;
permittedEdges &= 0xF;
uint8_t edges = permittedEdges;
if (isThin && peep.PathfindGoal == goal)
{
/* Use of peep.PathfindHistory[]:
* When walking to a goal, the peep PathfindHistory stores
* the last 4 thin junctions that the peep walked through.
* For each of these 4 thin junctions the peep remembers
* those edges it has not yet taken.
* If a peep returns to one of the 4 thin junctions that it
* remembers, it will only choose from the directions that it
* did not try yet.
* This forces to the peep pathfinding to try the "next best"
* direction after trying the "best" direction(s) and finding
* that the goal could not be reached. */
/* If the peep remembers walking through this junction
* previously while heading for its goal, retrieve the
* directions it has not yet tried. */
for (auto& pathfindHistory : peep.PathfindHistory)
{
if (pathfindHistory == loc)
{
/* Fix broken PathfindHistory[i].direction
* which have untried directions that are not
* currently possible - could be due to pathing
* changes or in earlier code .directions was
* initialised to 0xF rather than the permitted
* edges. */
pathfindHistory.direction &= permittedEdges;
edges = pathfindHistory.direction;
LogPathfinding(
&peep, "Getting untried edges from pf_history for %d,%d,%d: %s,%s,%s,%s", loc.x, loc.y, loc.z,
(edges & 1) ? "0" : "-", (edges & 2) ? "1" : "-", (edges & 4) ? "2" : "-", (edges & 8) ? "3" : "-");
if (edges == 0)
{
/* If peep has tried all edges, reset to
* all edges are untried.
* This permits the pathfinding to try
* again, which is good for getting
* unstuck when the player has edited
* the paths or the pathfinding itself
* has changed (been fixed) since
* the game was saved. */
pathfindHistory.direction = permittedEdges;
edges = pathfindHistory.direction;
LogPathfinding(&peep, "All edges tried for %d,%d,%d - resetting to all untried", loc.x, loc.y, loc.z);
}
break;
}
}
}
/* If this is a new goal for the peep. Store it and reset the peep's
* PathfindHistory. */
if (!DirectionValid(peep.PathfindGoal.direction) || peep.PathfindGoal != goal)
{
peep.PathfindGoal = { goal, 0 };
// Clear pathfinding history
TileCoordsXYZD nullPos;
nullPos.SetNull();
std::fill(std::begin(peep.PathfindHistory), std::end(peep.PathfindHistory), nullPos);
LogPathfinding(&peep, "New goal; clearing pf_history.");
}
// Peep has tried all edges.
if (edges == 0)
return INVALID_DIRECTION;
int32_t chosenEdge = UtilBitScanForward(edges);
// Peep has multiple edges still to try.
if (edges & ~(1 << chosenEdge))
{
uint8_t bestJunctions = 0;
TileCoordsXYZ bestJunctionList[16];
uint8_t bestDirectionList[16];
TileCoordsXYZ bestXYZ;
uint16_t bestScore = 0xFFFF;
uint8_t bestSub = 0xFF;
LogPathfinding(
&peep, "Pathfind start for goal %d,%d,%d from %d,%d,%d", goal.x, goal.y, goal.z, loc.x, loc.y, loc.z);
/* Call the search heuristic on each edge, keeping track of the
* edge that gives the best (i.e. smallest) value (best_score)
* or for different edges with equal value, the edge with the
* least steps (best_sub). */
int32_t numEdges = BitCount(edges);
for (int32_t testEdge = chosenEdge; testEdge != -1; testEdge = UtilBitScanForward(edges))
{
edges &= ~(1 << testEdge);
uint8_t height = loc.z;
if (firstTileElement->AsPath()->IsSloped() && firstTileElement->AsPath()->GetSlopeDirection() == testEdge)
{
height += 0x2;
}
/* Divide the maxTilesChecked global search limit
* between the remaining edges to ensure the search
* covers all of the remaining edges. */
_peepPathFindTilesChecked = maxTilesChecked / numEdges;
_peepPathFindNumJunctions = _peepPathFindMaxJunctions;
// Initialise _peepPathFindHistory.
for (auto& entry : _peepPathFindHistory)
{
entry.location.SetNull();
entry.direction = INVALID_DIRECTION;
}
/* The pathfinding will only use elements
* 1.._peepPathFindMaxJunctions, so the starting point
* is placed in element 0 */
_peepPathFindHistory[0].location = loc;
_peepPathFindHistory[0].direction = 0xF;
uint16_t score = 0xFFFF;
/* Variable endXYZ contains the end location of the
* search path. */
TileCoordsXYZ endXYZ;
endXYZ.x = 0;
endXYZ.y = 0;
endXYZ.z = 0;
uint8_t endSteps = 255;
/* Variable endJunctions is the number of junctions
* passed through in the search path.
* Variables endJunctionList and endDirectionList
* contain the junctions and corresponding directions
* of the search path.
* In the future these could be used to visualise the
* pathfinding on the map. */
uint8_t endJunctions = 0;
TileCoordsXYZ endJunctionList[16];
uint8_t endDirectionList[16] = { 0 };
bool inPatrolArea = false;
auto* staff = peep.As<Staff>();
if (staff != nullptr && staff->IsMechanic())
{
/* Mechanics are the only staff type that
* pathfind to a destination. Determine if the
* mechanic is in their patrol area. */
inPatrolArea = staff->IsLocationInPatrol(peep.NextLoc);
}
LogPathfinding(
&peep, "Pathfind searching in direction: %d from %d,%d,%d", testEdge, loc.x >> 5, loc.y >> 5, loc.z);
PeepPathfindHeuristicSearch(
{ loc.x, loc.y, height }, goal, peep, firstTileElement, inPatrolArea, 0, &score, testEdge, &endJunctions,
endJunctionList, endDirectionList, &endXYZ, &endSteps);
if constexpr (kLogPathfinding)
{
LogPathfinding(
&peep, "Pathfind test edge: %d score: %d steps: %d end: %d,%d,%d junctions: %d", testEdge, score,
endSteps, endXYZ.x, endXYZ.y, endXYZ.z, endJunctions);
for (uint8_t listIdx = 0; listIdx < endJunctions; listIdx++)
{
LogPathfinding(
&peep, "Junction#%d %d,%d,%d Direction %d", listIdx + 1, endJunctionList[listIdx].x,
endJunctionList[listIdx].y, endJunctionList[listIdx].z, endDirectionList[listIdx]);
}
}
if (score < bestScore || (score == bestScore && endSteps < bestSub))
{
chosenEdge = testEdge;
bestScore = score;
bestSub = endSteps;
if constexpr (kLogPathfinding)
{
bestJunctions = endJunctions;
for (uint8_t index = 0; index < endJunctions; index++)
{
bestJunctionList[index].x = endJunctionList[index].x;
bestJunctionList[index].y = endJunctionList[index].y;
bestJunctionList[index].z = endJunctionList[index].z;
bestDirectionList[index] = endDirectionList[index];
}
bestXYZ.x = endXYZ.x;
bestXYZ.y = endXYZ.y;
bestXYZ.z = endXYZ.z;
}
}
}
/* Check if the heuristic search failed. e.g. all connected
* paths are within the search limits and none reaches the
* goal. */
if (bestScore == 0xFFFF)
{
LogPathfinding(&peep, "Pathfind heuristic search failed.");
return INVALID_DIRECTION;
}
if constexpr (kLogPathfinding)
{
LogPathfinding(&peep, "Pathfind best edge %d with score %d steps %d", chosenEdge, bestScore, bestSub);
for (uint8_t listIdx = 0; listIdx < bestJunctions; listIdx++)
{
LogPathfinding(
&peep, "Junction#%d %d,%d,%d Direction %d", listIdx + 1, bestJunctionList[listIdx].x,
bestJunctionList[listIdx].y, bestJunctionList[listIdx].z, bestDirectionList[listIdx]);
}
LogPathfinding(&peep, "End at %d,%d,%d", bestXYZ.x, bestXYZ.y, bestXYZ.z);
}
}
if (isThin)
{
for (std::size_t i = 0; i < peep.PathfindHistory.size(); ++i)
{
if (peep.PathfindHistory[i] == loc)
{
/* Peep remembers this junction, so remove the
* chosen_edge from those left to try. */
peep.PathfindHistory[i].direction &= ~(1 << chosenEdge);
/* Also remove the edge through which the peep
* entered the junction from those left to try. */
peep.PathfindHistory[i].direction &= ~(1 << DirectionReverse(peep.PeepDirection));
LogPathfinding(
&peep, "Updating existing pf_history (in index: %u) for %d,%d,%d without entry edge %d & exit edge %d.",
i, loc.x, loc.y, loc.z, DirectionReverse(peep.PeepDirection), chosenEdge);
return chosenEdge;
}
}
/* Peep does not remember this junction, so forget a junction
* and remember this junction. */
int32_t i = peep.PathfindGoal.direction++;
peep.PathfindGoal.direction &= 3;
peep.PathfindHistory[i] = { loc, permittedEdges };
/* Remove the chosen_edge from those left to try. */
peep.PathfindHistory[i].direction &= ~(1 << chosenEdge);
/* Also remove the edge through which the peep
* entered the junction from those left to try. */
peep.PathfindHistory[i].direction &= ~(1 << DirectionReverse(peep.PeepDirection));
LogPathfinding(
&peep, "Storing new pf_history (in index: %d) for %d,%d,%d without entry edge %d & exit edge %d.", i, loc.x,
loc.y, loc.z, DirectionReverse(peep.PeepDirection), chosenEdge);
}
return chosenEdge;
}
/**
* Gets the nearest park entrance relative to point, by using Manhattan distance.
* @param x x coordinate of location
* @param y y coordinate of location
* @return Index of gParkEntrance (or 0xFF if no park entrances exist).
*/
static std::optional<CoordsXYZ> GetNearestParkEntrance(const CoordsXY& loc)
{
std::optional<CoordsXYZ> chosenEntrance = std::nullopt;
uint16_t nearestDist = 0xFFFF;
for (const auto& parkEntrance : GetGameState().Park.Entrances)
{
auto dist = abs(parkEntrance.x - loc.x) + abs(parkEntrance.y - loc.y);
if (dist < nearestDist)
{
nearestDist = dist;
chosenEntrance = parkEntrance;
}
}
return chosenEntrance;
}
/**
*
* rct2: 0x006952C0
*/
int32_t GuestPathFindParkEntranceEntering(Peep& peep, uint8_t edges)
{
// Send peeps to the nearest park entrance.
auto chosenEntrance = GetNearestParkEntrance(peep.NextLoc);
// If no defined park entrances are found, walk aimlessly.
if (!chosenEntrance.has_value())
return GuestPathfindAimless(peep, edges);
gPeepPathFindIgnoreForeignQueues = true;
gPeepPathFindQueueRideIndex = RideId::GetNull();
const auto goalPos = TileCoordsXYZ(chosenEntrance.value());
Direction chosenDirection = ChooseDirection(TileCoordsXYZ{ peep.NextLoc }, goalPos, peep);
if (chosenDirection == INVALID_DIRECTION)
return GuestPathfindAimless(peep, edges);
return PeepMoveOneTile(chosenDirection, peep);
}
/**
* Gets the nearest peep spawn relative to point, by using Manhattan distance.
* @param x x coordinate of location
* @param y y coordinate of location
* @return Index of gameState.PeepSpawns (or 0xFF if no peep spawns exist).
*/
static uint8_t GetNearestPeepSpawnIndex(uint16_t x, uint16_t y)
{
uint8_t chosenSpawn = 0xFF;
uint16_t nearestDist = 0xFFFF;
uint8_t i = 0;
for (const auto& spawn : GetGameState().PeepSpawns)
{
uint16_t dist = abs(spawn.x - x) + abs(spawn.y - y);
if (dist < nearestDist)
{
nearestDist = dist;
chosenSpawn = i;
}
i++;
}
return chosenSpawn;
}
/**
*
* rct2: 0x0069536C
*/
int32_t GuestPathFindPeepSpawn(Peep& peep, uint8_t edges)
{
// Send peeps to the nearest spawn point.
uint8_t chosenSpawn = GetNearestPeepSpawnIndex(peep.NextLoc.x, peep.NextLoc.y);
// If no defined spawns were found, walk aimlessly.
if (chosenSpawn == 0xFF)
return GuestPathfindAimless(peep, edges);
const auto peepSpawnLoc = GetGameState().PeepSpawns[chosenSpawn].ToTileStart();
Direction direction = peepSpawnLoc.direction;
if (peepSpawnLoc.x == peep.NextLoc.x && peepSpawnLoc.y == peep.NextLoc.y)
{
return PeepMoveOneTile(direction, peep);
}
gPeepPathFindIgnoreForeignQueues = true;
gPeepPathFindQueueRideIndex = RideId::GetNull();
const auto goalPos = TileCoordsXYZ(peepSpawnLoc);
direction = ChooseDirection(TileCoordsXYZ{ peep.NextLoc }, goalPos, peep);
if (direction == INVALID_DIRECTION)
return GuestPathfindAimless(peep, edges);
return PeepMoveOneTile(direction, peep);
}
/**
*
* rct2: 0x00695161
*/
int32_t GuestPathFindParkEntranceLeaving(Peep& peep, uint8_t edges)
{
TileCoordsXYZ entranceGoal{};
if (peep.PeepFlags & PEEP_FLAGS_PARK_ENTRANCE_CHOSEN)
{
entranceGoal = peep.PathfindGoal;
auto* entranceElement = MapGetParkEntranceElementAt(entranceGoal.ToCoordsXYZ(), false);
// If entrance no longer exists, choose a new one
if (entranceElement == nullptr)
{
peep.PeepFlags &= ~(PEEP_FLAGS_PARK_ENTRANCE_CHOSEN);
}
}
if (!(peep.PeepFlags & PEEP_FLAGS_PARK_ENTRANCE_CHOSEN))
{
auto chosenEntrance = GetNearestParkEntrance(peep.NextLoc);
if (!chosenEntrance.has_value())
return GuestPathfindAimless(peep, edges);
peep.PeepFlags |= PEEP_FLAGS_PARK_ENTRANCE_CHOSEN;
entranceGoal = TileCoordsXYZ(*chosenEntrance);
}
gPeepPathFindIgnoreForeignQueues = true;
gPeepPathFindQueueRideIndex = RideId::GetNull();
Direction chosenDirection = ChooseDirection(TileCoordsXYZ{ peep.NextLoc }, entranceGoal, peep);
if (chosenDirection == INVALID_DIRECTION)
return GuestPathfindAimless(peep, edges);
return PeepMoveOneTile(chosenDirection, peep);
}
/**
*
* rct2: 0x006A72C5
* param dist is not used.
*
* In case where the map element at (x, y) is invalid or there is no entrance
* or queue leading to it the function will not update its arguments.
*/
static void GetRideQueueEnd(TileCoordsXYZ& loc)
{
TileCoordsXY queueEnd = { 0, 0 };
TileElement* tileElement = MapGetFirstElementAt(loc);
if (tileElement == nullptr)
{
return;
}
bool found = false;
do
{
if (tileElement->GetType() != TileElementType::Entrance)
continue;
if (loc.z != tileElement->BaseHeight)
continue;
found = true;
break;
} while (!(tileElement++)->IsLastForTile());
if (!found)
return;
Direction direction = DirectionReverse(tileElement->GetDirection());
TileElement* lastPathElement = nullptr;
TileElement* firstPathElement = nullptr;
int16_t baseZ = tileElement->BaseHeight;
TileCoordsXY nextTile = { loc.x, loc.y };
while (true)
{
if (tileElement->GetType() == TileElementType::Path)
{
lastPathElement = tileElement;
// Update the current queue end
queueEnd = nextTile;
// queueEnd.direction = direction;
if (tileElement->AsPath()->IsSloped())
{
if (tileElement->AsPath()->GetSlopeDirection() == direction)
{
baseZ += 2;
}
}
}
nextTile += TileDirectionDelta[direction];
tileElement = MapGetFirstElementAt(nextTile);
found = false;
if (tileElement == nullptr)
break;
do
{
if (tileElement == firstPathElement)
continue;
if (tileElement->GetType() != TileElementType::Path)
continue;
if (baseZ == tileElement->BaseHeight)
{
if (tileElement->AsPath()->IsSloped())
{
if (tileElement->AsPath()->GetSlopeDirection() != direction)
{
break;
}
}
found = true;
break;
}
if (baseZ - 2 == tileElement->BaseHeight)
{
if (!tileElement->AsPath()->IsSloped())
break;
if (tileElement->AsPath()->GetSlopeDirection() != DirectionReverse(direction))
break;
baseZ -= 2;
found = true;
break;
}
} while (!(tileElement++)->IsLastForTile());
if (!found)
break;
if (!tileElement->AsPath()->IsQueue())
break;
if (!(tileElement->AsPath()->GetEdges() & (1 << DirectionReverse(direction))))
break;
if (firstPathElement == nullptr)
firstPathElement = tileElement;
// More queue to go.
if (tileElement->AsPath()->GetEdges() & (1 << (direction)))
continue;
direction++;
direction &= 3;
// More queue to go.
if (tileElement->AsPath()->GetEdges() & (1 << (direction)))
continue;
direction = DirectionReverse(direction);
// More queue to go.
if (tileElement->AsPath()->GetEdges() & (1 << (direction)))
continue;
break;
}
if (loc.z == MAX_ELEMENT_HEIGHT)
return;
tileElement = lastPathElement;
if (tileElement == nullptr)
return;
if (!tileElement->AsPath()->IsQueue())
return;
loc.x = queueEnd.x;
loc.y = queueEnd.y;
loc.z = tileElement->BaseHeight;
}
/*
* If a ride has multiple entrance stations and is set to sync with
* adjacent stations, cycle through the entrance stations (based on
* number of rides the peep has been on) so the peep will try the
* different sections of the ride.
* In this case, the ride's various entrance stations will typically,
* though not necessarily, be adjacent to one another and consequently
* not too far for the peep to walk when cycling between them.
* Note: the same choice of station must made while the peep navigates
* to the station. Consequently a truly random station selection here is not
* appropriate.
*/
static StationIndex GuestPathfindingSelectRandomStation(
const Guest& guest, int32_t numEntranceStations, BitSet<OpenRCT2::Limits::MaxStationsPerRide>& entranceStations)
{
int32_t select = guest.GuestNumRides % numEntranceStations;
while (select > 0)
{
for (StationIndex::UnderlyingType i = 0; i < OpenRCT2::Limits::MaxStationsPerRide; i++)
{
if (entranceStations[i])
{
entranceStations[i] = false;
select--;
break;
}
}
}
for (StationIndex::UnderlyingType i = 0; i < OpenRCT2::Limits::MaxStationsPerRide; i++)
{
if (entranceStations[i])
{
return StationIndex::FromUnderlying(i);
}
}
return StationIndex::FromUnderlying(0);
}
/**
*
* rct2: 0x00694C35
*/
int32_t CalculateNextDestination(Guest& peep)
{
LogPathfinding(&peep, "Starting CalculateNextDestination");
if (peep.GetNextIsSurface())
{
return GuestSurfacePathFinding(peep);
}
TileCoordsXYZ loc{ peep.NextLoc };
auto* pathElement = MapGetPathElementAt(loc);
if (pathElement == nullptr)
{
return 1;
}
// Because this function is called for guests only, never ignore banners.
uint8_t edges = PathGetPermittedEdges(false, pathElement);
if (edges == 0)
{
return GuestSurfacePathFinding(peep);
}
if (!peep.OutsideOfPark && peep.HeadingForRideOrParkExit())
{
/* If this tileElement is adjacent to any non-wide paths,
* remove all of the edges to wide paths. */
uint8_t adjustedEdges = edges;
for (Direction chosenDirection : ALL_DIRECTIONS)
{
// If there is no path in that direction try another
if (!(adjustedEdges & (1 << chosenDirection)))
continue;
/* If there is a wide path in that direction,
remove that edge and try another */
if (FootpathElementNextInDirection(loc, pathElement, chosenDirection) == PathSearchResult::Wide)
{
adjustedEdges &= ~(1 << chosenDirection);
}
}
if (adjustedEdges != 0)
edges = adjustedEdges;
}
int32_t direction = DirectionReverse(peep.PeepDirection);
// Check if in a dead end (i.e. only edge is where the peep came from)
if (!(edges & ~(1 << direction)))
{
// In a dead end. Check if peep is lost, etc.
peep.CheckIfLost();
peep.CheckCantFindRide();
peep.CheckCantFindExit();
}
else
{
/* Not a dead end. Remove edge peep came from so peep will
* continue on rather than going back where it came from */
edges &= ~(1 << direction);
}
direction = UtilBitScanForward(edges);
// IF only one edge to choose from
if ((edges & ~(1 << direction)) == 0)
{
LogPathfinding(&peep, "Completed CalculateNextDestination - taking only direction available: %d.", direction);
return PeepMoveOneTile(direction, peep);
}
// Peep still has multiple edges to choose from.
// Peep is outside the park.
// Loc694F19:
if (peep.OutsideOfPark)
{
LogPathfinding(&peep, "Completed CalculateNextDestination - peep is outside the park.");
switch (peep.State)
{
case PeepState::EnteringPark:
return GuestPathFindParkEntranceEntering(peep, edges);
case PeepState::LeavingPark:
return GuestPathFindPeepSpawn(peep, edges);
default:
return GuestPathfindAimless(peep, edges);
}
}
/* Peep is inside the park.
* If the peep does not have food, randomly cull the useless directions
* (dead ends, ride exits, wide paths) from the edges.
* In principle, peeps with food are not paying as much attention to
* where they are going and are consequently more like to walk up
* dead end paths, paths to ride exits, etc. */
if (!peep.HasFoodOrDrink() && (ScenarioRand() & 0xFFFF) >= 2184)
{
uint8_t adjustedEdges = edges;
for (Direction chosenDirection : ALL_DIRECTIONS)
{
// If there is no path in that direction try another
if (!(adjustedEdges & (1 << chosenDirection)))
continue;
RideId rideIndex = RideId::GetNull();
auto pathSearchResult = FootpathElementDestinationInDirection(loc, pathElement, chosenDirection, &rideIndex);
switch (pathSearchResult)
{
case PathSearchResult::DeadEnd:
case PathSearchResult::RideExit:
case PathSearchResult::Wide:
adjustedEdges &= ~(1 << chosenDirection);
break;
default:
break;
}
}
if (adjustedEdges != 0)
edges = adjustedEdges;
}
/* If there are still multiple directions to choose from,
* peeps with maps will randomly read the map: probability of doing so
* is much higher when heading for a ride or the park exit. */
if (peep.HasItem(ShopItem::Map))
{
// If at least 2 directions consult map
if (BitCount(edges) >= 2)
{
uint16_t probability = 1638;
if (peep.HeadingForRideOrParkExit())
{
probability = 9362;
}
if ((ScenarioRand() & 0xFFFF) < probability)
{
peep.ReadMap();
}
}
}
if (peep.PeepFlags & PEEP_FLAGS_LEAVING_PARK)
{
LogPathfinding(&peep, "Completed CalculateNextDestination - peep is leaving the park.");
return GuestPathFindParkEntranceLeaving(peep, edges);
}
if (peep.GuestHeadingToRideId.IsNull())
{
LogPathfinding(&peep, "Completed CalculateNextDestination - peep is aimless.");
return GuestPathfindAimless(peep, edges);
}
// Peep is heading for a ride.
RideId rideIndex = peep.GuestHeadingToRideId;
auto ride = GetRide(rideIndex);
if (ride == nullptr || ride->status != RideStatus::Open)
{
LogPathfinding(&peep, "Completed CalculateNextDestination - peep is heading to closed ride == aimless.");
return GuestPathfindAimless(peep, edges);
}
// The ride is open.
gPeepPathFindQueueRideIndex = rideIndex;
/* Find the ride's closest entrance station to the peep.
* At the same time, count how many entrance stations there are and
* which stations are entrance stations. */
auto bestScore = std::numeric_limits<int32_t>::max();
StationIndex closestStationNum = StationIndex::FromUnderlying(0);
int32_t numEntranceStations = 0;
BitSet<OpenRCT2::Limits::MaxStationsPerRide> entranceStations = {};
for (const auto& station : ride->GetStations())
{
// Skip if stationNum has no entrance (so presumably an exit only station)
if (station.Entrance.IsNull())
continue;
const auto stationIndex = ride->GetStationIndex(&station);
numEntranceStations++;
entranceStations[stationIndex.ToUnderlying()] = true;
TileCoordsXYZD entranceLocation = station.Entrance;
auto score = CalculateHeuristicPathingScore(entranceLocation, TileCoordsXYZ{ peep.NextLoc });
if (score < bestScore)
{
bestScore = score;
closestStationNum = stationIndex;
continue;
}
}
// Ride has no stations with an entrance, so head to station 0.
if (numEntranceStations == 0)
closestStationNum = StationIndex::FromUnderlying(0);
if (numEntranceStations > 1 && (ride->depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS))
{
closestStationNum = GuestPathfindingSelectRandomStation(peep, numEntranceStations, entranceStations);
}
if (numEntranceStations == 0)
{
// closestStationNum is always 0 here.
const auto& closestStation = ride->GetStation(closestStationNum);
auto entranceXY = TileCoordsXY(closestStation.Start);
loc.x = entranceXY.x;
loc.y = entranceXY.y;
loc.z = closestStation.Height;
}
else
{
TileCoordsXYZD entranceXYZD = ride->GetStation(closestStationNum).Entrance;
loc.x = entranceXYZD.x;
loc.y = entranceXYZD.y;
loc.z = entranceXYZD.z;
}
GetRideQueueEnd(loc);
gPeepPathFindIgnoreForeignQueues = true;
direction = ChooseDirection(TileCoordsXYZ{ peep.NextLoc }, loc, peep);
if (direction == INVALID_DIRECTION)
{
/* Heuristic search failed for all directions.
* Reset the PathfindGoal - this means that the PathfindHistory
* will be reset in the next call to ChooseDirection().
* This lets the heuristic search "try again" in case the player has
* edited the path layout or the mechanic was already stuck in the
* save game (e.g. with a worse version of the pathfinding). */
peep.ResetPathfindGoal();
LogPathfinding(&peep, "Completed CalculateNextDestination - failed to choose a direction == aimless.");
return GuestPathfindAimless(peep, edges);
}
LogPathfinding(&peep, "Completed CalculateNextDestination - direction chosen: %d.", direction);
return PeepMoveOneTile(direction, peep);
}
bool IsValidPathZAndDirection(TileElement* tileElement, int32_t currentZ, int32_t currentDirection)
{
if (tileElement->AsPath()->IsSloped())
{
int32_t slopeDirection = tileElement->AsPath()->GetSlopeDirection();
if (slopeDirection == currentDirection)
{
if (currentZ != tileElement->BaseHeight)
return false;
}
else
{
slopeDirection = DirectionReverse(slopeDirection);
if (slopeDirection != currentDirection)
return false;
if (currentZ != tileElement->BaseHeight + 2)
return false;
}
}
else
{
if (currentZ != tileElement->BaseHeight)
return false;
}
return true;
}
} // namespace OpenRCT2::PathFinding