Skip to content

Commit

Permalink
Fix OpenTTD#12228, Fix OpenTTD#12231, Fix OpenTTD#12333: Ship pathfin…
Browse files Browse the repository at this point in the history
…der always considers reversing
  • Loading branch information
Kuhnovic committed Apr 5, 2024
1 parent e8c78df commit a757935
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 71 deletions.
20 changes: 16 additions & 4 deletions src/pathfinder/yapf/yapf_common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class CYapfOriginTileT
protected:
TileIndex m_orgTile; ///< origin tile
TrackdirBits m_orgTrackdirs; ///< origin trackdir mask
TrackdirBits m_orgReverseTrackdirs; ///< origin reverse trackdir mask
int m_reverse_penalty; ///< penalty applied to reverse directions

/** to access inherited path finder */
inline Tpf &Yapf()
Expand All @@ -31,22 +33,32 @@ class CYapfOriginTileT

public:
/** Set origin tile / trackdir mask */
void SetOrigin(TileIndex tile, TrackdirBits trackdirs)
void SetOrigin(TileIndex tile, TrackdirBits trackdirs, TrackdirBits reverse_trackdirs = TRACKDIR_BIT_NONE, int reverse_penalty = 0)
{
assert((trackdirs & reverse_trackdirs) == TRACKDIR_BIT_NONE); // Can't set single trackdirs as both regular and reverse origin.
m_orgTile = tile;
m_orgTrackdirs = trackdirs;
m_orgReverseTrackdirs = reverse_trackdirs;
m_reverse_penalty = reverse_penalty;
}

/** Called when YAPF needs to place origin nodes into open list */
void PfSetStartupNodes()
{
bool is_choice = (KillFirstBit(m_orgTrackdirs) != TRACKDIR_BIT_NONE);
for (TrackdirBits tdb = m_orgTrackdirs; tdb != TRACKDIR_BIT_NONE; tdb = KillFirstBit(tdb)) {
Trackdir td = (Trackdir)FindFirstBit(tdb);
const bool is_choice = CountBits(m_orgTrackdirs | m_orgReverseTrackdirs) > 1;

for (Trackdir td : SetTrackdirBitIterator(m_orgTrackdirs)) {
Node &n1 = Yapf().CreateNewNode();
n1.Set(nullptr, m_orgTile, td, is_choice);
Yapf().AddStartupNode(n1);
}

for (Trackdir td : SetTrackdirBitIterator(m_orgReverseTrackdirs)) {
Node &n2 = Yapf().CreateNewNode();
n2.Set(nullptr, m_orgTile, td, is_choice);
n2.m_cost = m_reverse_penalty;
Yapf().AddStartupNode(n2);
}
}
};

Expand Down
110 changes: 51 additions & 59 deletions src/pathfinder/yapf/yapf_ship.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

constexpr int NUMBER_OR_WATER_REGIONS_LOOKAHEAD = 4;
constexpr int MAX_SHIP_PF_NODES = (NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1) * WATER_REGION_NUMBER_OF_TILES * 4; // 4 possible exit dirs per tile.

constexpr int SHIP_REVERSAL_PENALTY = YAPF_TILE_LENGTH * 10;
constexpr int SHIP_LOST_PATH_LENGTH = 8; // The length of the (aimless) path assigned when a ship is lost.

template <class Types>
Expand Down Expand Up @@ -172,6 +172,14 @@ class CYapfFollowShipT
return 'w';
}

/** Returns a random trackdir out of a set of trackdirs. */
static Trackdir GetRandomTrackdir(TrackdirBits trackdirs)
{
const int strip_amount = RandomRange(CountBits(trackdirs));
for (int s = 0; s < strip_amount; ++s) RemoveFirstTrackdir(&trackdirs);
return FindFirstTrackdir(trackdirs);
}

/** Returns a random tile/trackdir that can be reached from the current tile/trackdir, or tile/INVALID_TRACK if none is available. */
static std::pair<TileIndex, Trackdir> GetRandomFollowUpTileTrackdir(const Ship *v, TileIndex tile, Trackdir dir)
{
Expand All @@ -180,17 +188,15 @@ class CYapfFollowShipT
TrackdirBits dirs = follower.m_new_td_bits;
const TrackdirBits dirs_without_90_degree = dirs & ~TrackdirCrossesTrackdirs(dir);
if (dirs_without_90_degree != TRACKDIR_BIT_NONE) dirs = dirs_without_90_degree;
const int strip_amount = RandomRange(CountBits(dirs));
for (int s = 0; s < strip_amount; ++s) RemoveFirstTrackdir(&dirs);
return { follower.m_new_tile, FindFirstTrackdir(dirs) };
return { follower.m_new_tile, GetRandomTrackdir(dirs) };
}
return { follower.m_new_tile, INVALID_TRACKDIR };
}

/** Creates a random path, avoids 90 degree turns. */
static Trackdir CreateRandomPath(const Ship *v, Trackdir dir, ShipPathCache &path_cache, int path_length)
static Trackdir CreateRandomPath(const Ship *v, ShipPathCache &path_cache, int path_length)
{
std::pair<TileIndex, Trackdir> tile_dir = { v->tile, dir };
std::pair<TileIndex, Trackdir> tile_dir = { v->tile, v->GetVehicleTrackdir()};
for (int i = 0; i < path_length; ++i) {
tile_dir = GetRandomFollowUpTileTrackdir(v, tile_dir.first, tile_dir.second);
if (tile_dir.second == INVALID_TRACKDIR) break;
Expand All @@ -204,19 +210,17 @@ class CYapfFollowShipT
return result;
}

static Trackdir ChooseShipTrack(const Ship *v, TileIndex tile, bool &path_found, ShipPathCache &path_cache)
static Trackdir ChooseShipTrack(const Ship *v, TileIndex tile, TrackdirBits origin_dirs, TrackdirBits reverse_origin_dirs,
bool &path_found, ShipPathCache &path_cache, Trackdir &best_origin_dir)
{
const Trackdir trackdir = v->GetVehicleTrackdir();
assert(IsValidTrackdir(trackdir));

/* Convert origin trackdir to TrackdirBits. */
const TrackdirBits trackdirs = TrackdirToTrackdirBits(trackdir);

const std::vector<WaterRegionPatchDesc> high_level_path = YapfShipFindWaterRegionPath(v, tile, NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1);
if (high_level_path.empty()) {
path_found = false;
/* Make the ship move around aimlessly. This prevents repeated pathfinder calls and clearly indicates that the ship is lost. */
return CreateRandomPath(v, trackdir, path_cache, SHIP_LOST_PATH_LENGTH);
return CreateRandomPath(v, path_cache, SHIP_LOST_PATH_LENGTH);
}

/* Try one time without restricting the search area, which generally results in better and more natural looking paths.
Expand All @@ -226,7 +230,7 @@ class CYapfFollowShipT
Tpf pf(MAX_SHIP_PF_NODES);

/* Set origin and destination nodes */
pf.SetOrigin(v->tile, trackdirs);
pf.SetOrigin(v->tile, origin_dirs, reverse_origin_dirs, SHIP_REVERSAL_PENALTY);
pf.SetDestination(v);
const bool is_intermediate_destination = static_cast<int>(high_level_path.size()) >= NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1;
if (is_intermediate_destination) pf.SetIntermediateDestination(high_level_path.back());
Expand All @@ -239,14 +243,16 @@ class CYapfFollowShipT
path_found = pf.FindPath(v);
Node *node = pf.GetBestNode();
if (attempt == 0 && !path_found) continue; // Try again with restricted search area.
if (!path_found || node == nullptr) return GetRandomFollowUpTileTrackdir(v, v->tile, trackdir).second;

/* Make the ship move around aimlessly. This prevents repeated pathfinder calls and clearly indicates that the ship is lost. */
if (!path_found) return CreateRandomPath(v, path_cache, SHIP_LOST_PATH_LENGTH);

/* Return only the path within the current water region if an intermediate destination was returned. If not, cache the entire path
* to the final destination tile. The low-level pathfinder might actually prefer a different docking tile in a nearby region. Without
* caching the full path the ship can get stuck in a loop. */
const WaterRegionPatchDesc end_water_patch = GetWaterRegionPatchInfo(node->GetTile());
const WaterRegionPatchDesc start_water_patch = GetWaterRegionPatchInfo(tile);
assert(start_water_patch == high_level_path.front());
assert(GetWaterRegionPatchInfo(tile) == high_level_path.front() || GetWaterRegionPatchInfo(v->tile) == high_level_path.front());
const WaterRegionPatchDesc start_water_patch = high_level_path.front();
while (node->m_parent) {
const WaterRegionPatchDesc node_water_patch = GetWaterRegionPatchInfo(node->GetTile());

Expand All @@ -262,10 +268,18 @@ class CYapfFollowShipT
}
node = node->m_parent;
}
assert(node->GetTile() == v->tile);

/* If the ship has to turn around, return INVALID_TRACKDIR to trigger this. */
best_origin_dir = node->GetTrackdir();
if ((TrackdirToTrackdirBits(best_origin_dir) & origin_dirs) == TRACKDIR_BIT_NONE) {
path_cache.clear();
return INVALID_TRACKDIR;
}

/* A empty path means we are already at the destination. The pathfinder shouldn't have been called at all.
* Return a random reachable trackdir to hopefully nudge the ship out of this strange situation. */
if (path_cache.empty()) return GetRandomFollowUpTileTrackdir(v, v->tile, trackdir).second;
if (path_cache.empty()) return CreateRandomPath(v, path_cache, 1);

/* Take out the last trackdir as the result. */
const Trackdir result = path_cache.front();
Expand All @@ -284,52 +298,30 @@ class CYapfFollowShipT
* Check whether a ship should reverse to reach its destination.
* Called when leaving depot.
* @param v Ship.
* @param tile Current position.
* @param td1 Forward direction.
* @param td2 Reverse direction.
* @param trackdir [out] the best of all possible reversed trackdirs.
* @return true if the reverse direction is better.
*/
static bool CheckShipReverse(const Ship *v, TileIndex tile, Trackdir td1, Trackdir td2, Trackdir *trackdir)
static bool CheckShipReverse(const Ship *v, Trackdir *trackdir)
{
const std::vector<WaterRegionPatchDesc> high_level_path = YapfShipFindWaterRegionPath(v, tile, NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1);
if (high_level_path.empty()) {
if (trackdir) *trackdir = INVALID_TRACKDIR;
return false;
}
bool path_found = false;
ShipPathCache dummy_cache;
Trackdir best_origin_dir = INVALID_TRACKDIR;

/* Create pathfinder instance. */
Tpf pf(MAX_SHIP_PF_NODES);
/* Set origin and destination nodes. */
if (trackdir == nullptr) {
pf.SetOrigin(tile, TrackdirToTrackdirBits(td1) | TrackdirToTrackdirBits(td2));
} else {
DiagDirection entry = ReverseDiagDir(VehicleExitDir(v->direction, v->state));
TrackdirBits rtds = DiagdirReachesTrackdirs(entry) & TrackStatusToTrackdirBits(GetTileTrackStatus(tile, TRANSPORT_WATER, 0, entry));
pf.SetOrigin(tile, rtds);
}
pf.SetDestination(v);
if (high_level_path.size() > 1) pf.SetIntermediateDestination(high_level_path.back());
pf.RestrictSearch(high_level_path);

/* Find best path. */
if (!pf.FindPath(v)) return false;

Node *pNode = pf.GetBestNode();
if (pNode == nullptr) return false;

/* Path was found, walk through the path back to the origin. */
while (pNode->m_parent != nullptr) {
pNode = pNode->m_parent;
}

Trackdir best_trackdir = pNode->GetTrackdir();
if (trackdir != nullptr) {
*trackdir = best_trackdir;
/* The normal case, typically called when ships leave a dock. */
const Trackdir reverse_dir = ReverseTrackdir(v->GetVehicleTrackdir());
const TrackdirBits origin_dirs = TrackdirToTrackdirBits(v->GetVehicleTrackdir());
const TrackdirBits reverse_origin_dirs = TrackdirToTrackdirBits(reverse_dir);
(void)ChooseShipTrack(v, v->tile, origin_dirs, reverse_origin_dirs, path_found, dummy_cache, best_origin_dir);
return path_found && best_origin_dir == reverse_dir;
} else {
assert(best_trackdir == td1 || best_trackdir == td2);
/* This gets called when a ship suddenly can't move forward, e.g. due to terraforming. */
const DiagDirection entry = ReverseDiagDir(VehicleExitDir(v->direction, v->state));
const TrackdirBits reverse_origin_dirs = DiagdirReachesTrackdirs(entry) & TrackStatusToTrackdirBits(GetTileTrackStatus(v->tile, TRANSPORT_WATER, 0, entry));
(void)ChooseShipTrack(v, v->tile, TRACKDIR_BIT_NONE, reverse_origin_dirs, path_found, dummy_cache, best_origin_dir);
*trackdir = path_found ? best_origin_dir : GetRandomTrackdir(reverse_origin_dirs);
return true;
}
return best_trackdir != td1;
}
};

Expand Down Expand Up @@ -437,14 +429,14 @@ struct CYapfShip : CYapfT<CYapfShip_TypesT<CYapfShip, CFollowTrackWater, CShipNo
/** Ship controller helper - path finder invoker. */
Track YapfShipChooseTrack(const Ship *v, TileIndex tile, bool &path_found, ShipPathCache &path_cache)
{
Trackdir td_ret = CYapfShip::ChooseShipTrack(v, tile, path_found, path_cache);
Trackdir best_origin_dir = INVALID_TRACKDIR;
const TrackdirBits origin_dirs = TrackdirToTrackdirBits(v->GetVehicleTrackdir());
const TrackdirBits reverse_origin_dirs = TrackdirToTrackdirBits(ReverseTrackdir(v->GetVehicleTrackdir()));
const Trackdir td_ret = CYapfShip::ChooseShipTrack(v, tile, origin_dirs, reverse_origin_dirs, path_found, path_cache, best_origin_dir);
return (td_ret != INVALID_TRACKDIR) ? TrackdirToTrack(td_ret) : INVALID_TRACK;
}

bool YapfShipCheckReverse(const Ship *v, Trackdir *trackdir)
{
Trackdir td = v->GetVehicleTrackdir();
Trackdir td_rev = ReverseTrackdir(td);
TileIndex tile = v->tile;
return CYapfShip::CheckShipReverse(v, tile, td, td_rev, trackdir);
return CYapfShip::CheckShipReverse(v, trackdir);
}
18 changes: 10 additions & 8 deletions src/pathfinder/yapf/yapf_ship_regions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ class CYapfDestinationRegionT

protected:
Key m_dest;
Key m_reverse_dest;

public:
void SetDestination(const WaterRegionPatchDesc &water_region_patch)
void SetDestination(const WaterRegionPatchDesc &water_region_patch, const WaterRegionPatchDesc &reverse_water_region_patch)
{
m_dest.Set(water_region_patch);
m_reverse_dest.Set(reverse_water_region_patch);
}

protected:
Expand All @@ -145,7 +147,7 @@ class CYapfDestinationRegionT
public:
inline bool PfDetectDestination(Node &n) const
{
return n.m_key == m_dest;
return n.m_key == m_dest || n.m_key == m_reverse_dest;
}

inline bool PfCalcEstimate(Node &n)
Expand Down Expand Up @@ -191,11 +193,12 @@ class CYapfFollowRegionT
static std::vector<WaterRegionPatchDesc> FindWaterRegionPath(const Ship *v, TileIndex start_tile, int max_returned_path_length)
{
const WaterRegionPatchDesc start_water_region_patch = GetWaterRegionPatchInfo(start_tile);
const WaterRegionPatchDesc reverse_start_water_region_patch = GetWaterRegionPatchInfo(v->tile);

/* We reserve 4 nodes (patches) per water region. The vast majority of water regions have 1 or 2 regions so this should be a pretty
* safe limit. We cap the limit at 65536 which is at a region size of 16x16 is equivalent to one node per region for a 4096x4096 map. */
Tpf pf(std::min(static_cast<int>(Map::Size() * NODES_PER_REGION) / WATER_REGION_NUMBER_OF_TILES, MAX_NUMBER_OF_NODES));
pf.SetDestination(start_water_region_patch);
pf.SetDestination(start_water_region_patch, reverse_start_water_region_patch);

if (v->current_order.IsType(OT_GOTO_STATION)) {
DestinationID station_id = v->current_order.GetDestination();
Expand All @@ -212,15 +215,14 @@ class CYapfFollowRegionT
pf.AddOrigin(GetWaterRegionPatchInfo(tile));
}

/* If origin and destination are the same we simply return that water patch. */
std::vector<WaterRegionPatchDesc> path = { start_water_region_patch };
path.reserve(max_returned_path_length);
if (pf.HasOrigin(start_water_region_patch)) return path;

/* Find best path. */
if (!pf.FindPath(v)) return {}; // Path not found.

Node *node = pf.GetBestNode();
std::vector<WaterRegionPatchDesc> path;
path.reserve(max_returned_path_length);
path.push_back(node->m_key.m_water_region_patch);

for (int i = 0; i < max_returned_path_length - 1; ++i) {
if (node != nullptr) {
node = node->m_parent;
Expand Down

0 comments on commit a757935

Please sign in to comment.