From a7c2f489f63e03943e875ae1101d0f62b65f93ad Mon Sep 17 00:00:00 2001 From: Kuhnovic <68320206+Kuhnovic@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:06:14 +0100 Subject: [PATCH] Fix #5713: FindClosestShipDepot only considers depots that are actually reachable (#11768) (cherry picked from commit 8a4a99b7e879454f04e239eecc2301908e6d4eb0) --- src/pathfinder/water_regions.cpp | 14 +++++- src/pathfinder/water_regions.h | 2 + src/pathfinder/yapf/yapf_ship_regions.cpp | 4 +- src/ship_cmd.cpp | 61 ++++++++++++++++++----- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/pathfinder/water_regions.cpp b/src/pathfinder/water_regions.cpp index decfc01814..814499c7fa 100644 --- a/src/pathfinder/water_regions.cpp +++ b/src/pathfinder/water_regions.cpp @@ -27,6 +27,7 @@ constexpr TWaterRegionPatchLabel FIRST_REGION_LABEL = 1; constexpr TWaterRegionPatchLabel INVALID_WATER_REGION_PATCH = 0; static_assert(sizeof(TWaterRegionTraversabilityBits) * 8 == WATER_REGION_EDGE_LENGTH); +static_assert(sizeof(TWaterRegionPatchLabel) == sizeof(byte)); // Important for the hash calculation. static inline TrackBits GetWaterTracks(TileIndex tile) { return TrackStatusToTrackBits(GetTileTrackStatus(tile, TRANSPORT_WATER, 0)); } static inline bool IsAqueductTile(TileIndex tile) { return IsBridgeTile(tile) && GetTunnelBridgeTransportType(tile) == TRANSPORT_WATER; } @@ -318,14 +319,23 @@ WaterRegionReference GetUpdatedWaterRegion(TileIndex tile) } /** - * Returns the index of the water region - * @param water_region The Water region to return the index for + * Returns the index of the water region. + * @param water_region The water region to return the index for. */ TWaterRegionIndex GetWaterRegionIndex(const WaterRegionDesc &water_region) { return GetWaterRegionIndex(water_region.x, water_region.y); } +/** + * Calculates a number that uniquely identifies the provided water region patch. + * @param water_region_patch The Water region to calculate the hash for. + */ +uint32_t CalculateWaterRegionPatchHash(const WaterRegionPatchDesc &water_region_patch) +{ + return water_region_patch.label | GetWaterRegionIndex(water_region_patch) << 8; +} + /** * Returns the center tile of a particular water region. * @param water_region The water region to find the center tile for. diff --git a/src/pathfinder/water_regions.h b/src/pathfinder/water_regions.h index 044c9c4f76..b8bddf6e7e 100644 --- a/src/pathfinder/water_regions.h +++ b/src/pathfinder/water_regions.h @@ -58,6 +58,8 @@ struct WaterRegionDesc TWaterRegionIndex GetWaterRegionIndex(const WaterRegionDesc &water_region); +uint32_t CalculateWaterRegionPatchHash(const WaterRegionPatchDesc &water_region_patch); + TileIndex GetWaterRegionCenterTile(const WaterRegionDesc &water_region); WaterRegionDesc GetWaterRegionInfo(TileIndex tile); diff --git a/src/pathfinder/yapf/yapf_ship_regions.cpp b/src/pathfinder/yapf/yapf_ship_regions.cpp index 33fad3bbbe..eacdff2a84 100644 --- a/src/pathfinder/yapf/yapf_ship_regions.cpp +++ b/src/pathfinder/yapf/yapf_ship_regions.cpp @@ -25,14 +25,12 @@ constexpr uint32_t MAX_NUMBER_OF_NODES = 65536; struct CYapfRegionPatchNodeKey { WaterRegionPatchDesc m_water_region_patch; - static_assert(sizeof(TWaterRegionPatchLabel) == sizeof(byte)); // Important for the hash calculation. - inline void Set(const WaterRegionPatchDesc &water_region_patch) { m_water_region_patch = water_region_patch; } - inline int CalcHash() const { return m_water_region_patch.label | GetWaterRegionIndex(m_water_region_patch) << 8; } + inline uint32_t CalcHash() const { return CalculateWaterRegionPatchHash(m_water_region_patch); } inline bool operator==(const CYapfRegionPatchNodeKey &other) const { return CalcHash() == other.CalcHash(); } }; diff --git a/src/ship_cmd.cpp b/src/ship_cmd.cpp index fff2825f43..b957329317 100644 --- a/src/ship_cmd.cpp +++ b/src/ship_cmd.cpp @@ -18,6 +18,7 @@ #include "station_base.h" #include "newgrf_engine.h" #include "pathfinder/yapf/yapf.h" +#include "pathfinder/yapf/yapf_ship_regions.h" #include "newgrf_sound.h" #include "spritecache.h" #include "strings_func.h" @@ -38,11 +39,16 @@ #include "industry_map.h" #include "core/checksum_func.hpp" #include "articulated_vehicles.h" +#include "core/ring_buffer.hpp" +#include "3rdparty/robin_hood/robin_hood.h" #include "table/strings.h" #include "safeguards.h" +/** Max distance in tiles (as the crow flies) to search for depots when user clicks "go to depot". */ +constexpr int MAX_SHIP_DEPOT_SEARCH_DISTANCE = 80; + /** Directions to search towards given track bits and the ship's enter direction. */ const DiagDirection _ship_search_directions[6][4] = { { DIAGDIR_NE, INVALID_DIAGDIR, DIAGDIR_SW, INVALID_DIAGDIR }, @@ -155,21 +161,50 @@ void Ship::GetImage(Direction direction, EngineImageType image_type, VehicleSpri static const Depot *FindClosestShipDepot(const Vehicle *v, uint max_distance) { - /* Find the closest depot */ - const Depot *best_depot = nullptr; - /* If we don't have a maximum distance, i.e. distance = 0, - * we want to find any depot so the best distance of no - * depot must be more than any correct distance. On the - * other hand if we have set a maximum distance, any depot - * further away than max_distance can safely be ignored. */ - uint best_dist = max_distance == 0 ? UINT_MAX : max_distance + 1; + const uint max_region_distance = (max_distance / WATER_REGION_EDGE_LENGTH) + 1; + + static robin_hood::unordered_flat_set visited_patch_hashes; + static ring_buffer patches_to_search; + visited_patch_hashes.clear(); + patches_to_search.clear(); + + /* Step 1: find a set of reachable Water Region Patches using BFS. */ + const WaterRegionPatchDesc start_patch = GetWaterRegionPatchInfo(v->tile); + patches_to_search.push_back(start_patch); + visited_patch_hashes.insert(CalculateWaterRegionPatchHash(start_patch)); + + while (!patches_to_search.empty()) { + /* Remove first patch from the queue and make it the current patch. */ + const WaterRegionPatchDesc current_node = patches_to_search.front(); + patches_to_search.pop_front(); + + /* Add neighbors of the current patch to the search queue. */ + TVisitWaterRegionPatchCallBack visitFunc = [&](const WaterRegionPatchDesc &water_region_patch) { + /* Note that we check the max distance per axis, not the total distance. */ + if (Delta(water_region_patch.x, start_patch.x) > max_region_distance || + Delta(water_region_patch.y, start_patch.y) > max_region_distance) return; + + const uint32_t hash = CalculateWaterRegionPatchHash(water_region_patch); + auto res = visited_patch_hashes.insert(hash); + if (res.second) { + patches_to_search.push_back(water_region_patch); + } + }; + VisitWaterRegionPatchNeighbors(current_node, visitFunc); + } + + /* Step 2: Find the closest depot within the reachable Water Region Patches. */ + const uint max_distance_sq = max_distance * max_distance; + const Depot *best_depot = nullptr; + uint best_dist_sq = std::numeric_limits::max(); for (const Depot *depot : Depot::Iterate()) { - TileIndex tile = depot->xy; + const TileIndex tile = depot->xy; if (IsShipDepotTile(tile) && IsInfraTileUsageAllowed(VEH_SHIP, v->owner, tile)) { - uint dist = DistanceManhattan(tile, v->tile); - if (dist < best_dist) { - best_dist = dist; + const uint dist_sq = DistanceSquare(tile, v->tile); + if (dist_sq < best_dist_sq && dist_sq <= max_distance_sq && + visited_patch_hashes.count(CalculateWaterRegionPatchHash(GetWaterRegionPatchInfo(tile))) > 0) { + best_dist_sq = dist_sq; best_depot = depot; } } @@ -1190,7 +1225,7 @@ CommandCost CmdBuildShip(TileIndex tile, DoCommandFlag flags, const Engine *e, V ClosestDepot Ship::FindClosestDepot() { - const Depot *depot = FindClosestShipDepot(this, 0); + const Depot *depot = FindClosestShipDepot(this, MAX_SHIP_DEPOT_SEARCH_DISTANCE); if (depot == nullptr) return ClosestDepot(); return ClosestDepot(depot->xy, depot->index);