diff --git a/src/pathfinder/water_regions.cpp b/src/pathfinder/water_regions.cpp index 60e1c9b723..88fb21493f 100644 --- a/src/pathfinder/water_regions.cpp +++ b/src/pathfinder/water_regions.cpp @@ -430,3 +430,24 @@ void PrintWaterRegionDebugInfo(TileIndex tile) { GetUpdatedWaterRegion(tile).PrintDebugInfo(); } + +/** + * Tests the provided callback function on all tiles of the water patch of the region + * and returns the first tile that passes the callback test. + * @param callback The test function that will be called for the water patch. + * @param water_region_patch Water patch within the water region to test the callback. + * @return the first tile which passed the callback test, or INVALID_TILE if the callback failed. + */ +TileIndex GetTileInWaterRegionPatch(const WaterRegionPatchDesc &water_region_patch, TestTileIndexCallBack &callback) +{ + const WaterRegion region = GetUpdatedWaterRegion(water_region_patch.x, water_region_patch.y); + + /* Check if the region has a tile which passes the callback test. */ + for (const TileIndex tile : region) { + if (region.GetLabel(tile) != water_region_patch.label || !callback(tile)) continue; + + return tile; + } + + return INVALID_TILE; +} diff --git a/src/pathfinder/water_regions.h b/src/pathfinder/water_regions.h index 4e35f7090c..bb457bc183 100644 --- a/src/pathfinder/water_regions.h +++ b/src/pathfinder/water_regions.h @@ -64,4 +64,7 @@ void AllocateWaterRegions(); void PrintWaterRegionDebugInfo(TileIndex tile); +using TestTileIndexCallBack = std::function; +TileIndex GetTileInWaterRegionPatch(const WaterRegionPatchDesc &water_region_patch, TestTileIndexCallBack &callback); + #endif /* WATER_REGIONS_H */ diff --git a/src/pathfinder/yapf/yapf.h b/src/pathfinder/yapf/yapf.h index 186986ce57..88f38334f0 100644 --- a/src/pathfinder/yapf/yapf.h +++ b/src/pathfinder/yapf/yapf.h @@ -34,6 +34,16 @@ Track YapfShipChooseTrack(const Ship *v, TileIndex tile, bool &path_found, ShipP */ bool YapfShipCheckReverse(const Ship *v, Trackdir *trackdir); +/** + * Used when user sends ship to the nearest depot or if ship needs servicing using YAPF. + * @param v ship that needs to go to some depot + * @param max_penalty max distance (in pathfinder penalty) from the current ship position + * (used also as optimization - the pathfinder can stop path finding if max_penalty + * was reached and no depot was seen) + * @return the data about the depot + */ +FindDepotData YapfShipFindNearestDepot(const Ship *v, int max_penalty); + /** * Finds the best path for given road vehicle using YAPF. * @param v the RV that needs to find a path diff --git a/src/pathfinder/yapf/yapf_ship.cpp b/src/pathfinder/yapf/yapf_ship.cpp index a47df5abb3..06307b54bb 100644 --- a/src/pathfinder/yapf/yapf_ship.cpp +++ b/src/pathfinder/yapf/yapf_ship.cpp @@ -36,6 +36,7 @@ protected: TileIndex dest_tile; TrackdirBits dest_trackdirs; StationID dest_station; + bool any_ship_depot = false; bool has_intermediate_dest = false; TileIndex intermediate_dest_tile; @@ -55,6 +56,11 @@ public: } } + void SetAnyShipDepotDestination() + { + this->any_ship_depot = true; + } + void SetIntermediateDestination(const WaterRegionPatchDesc &water_region_patch) { this->has_intermediate_dest = true; @@ -69,10 +75,16 @@ protected: return *static_cast(this); } + TestTileIndexCallBack detect_ship_depot = [&](const TileIndex tile) + { + return IsShipDepotTile(tile) && GetShipDepotPart(tile) == DEPOT_PART_NORTH && IsTileOwner(tile, Yapf().GetVehicle()->owner); + }; + public: /** Called by YAPF to detect if node ends in the desired destination. */ inline bool PfDetectDestination(Node &n) { + if (this->any_ship_depot) return this->detect_ship_depot(n.key.tile); return this->PfDetectDestinationTile(n.segment_last_tile, n.segment_last_td); } @@ -89,6 +101,11 @@ public: return tile == this->dest_tile && ((this->dest_trackdirs & TrackdirToTrackdirBits(trackdir)) != TRACKDIR_BIT_NONE); } + inline TileIndex GetShipDepotDestination(const WaterRegionPatchDesc &water_region_patch) + { + return GetTileInWaterRegionPatch(water_region_patch, this->detect_ship_depot); + } + /** * Called by YAPF to calculate cost estimate. Calculates distance to the destination * adds it to the actual cost from origin and stores the sum to the Node::estimate. @@ -99,7 +116,7 @@ public: static const int dg_dir_to_x_offs[] = { -1, 0, 1, 0 }; static const int dg_dir_to_y_offs[] = { 0, 1, 0, -1 }; - if (this->PfDetectDestination(n)) { + if (this->any_ship_depot || this->PfDetectDestination(n)) { n.estimate = n.cost; return true; } @@ -158,7 +175,7 @@ public: } /** Restricts the search by creating corridor or water regions through which the ship is allowed to travel. */ - inline void RestrictSearch(const std::vector &path) + inline void RestrictSearch(const std::span &path) { this->water_region_corridor.clear(); for (const WaterRegionPatchDesc &path_entry : path) this->water_region_corridor.push_back(path_entry); @@ -211,16 +228,20 @@ public: return result; } - static Trackdir ChooseShipTrack(const Ship *v, TileIndex tile, TrackdirBits forward_dirs, TrackdirBits reverse_dirs, + static Trackdir ChooseShipTrack(const Ship *v, TileIndex &tile, TrackdirBits forward_dirs, TrackdirBits reverse_dirs, int max_penalty, bool &path_found, ShipPathCache &path_cache, Trackdir &best_origin_dir) { - const std::vector high_level_path = YapfShipFindWaterRegionPath(v, tile, NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1); + std::vector 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, path_cache, SHIP_LOST_PATH_LENGTH); } + const bool find_closest_depot = tile == INVALID_TILE; + if (find_closest_depot) tile = v->tile; + const bool automatic_servicing = find_closest_depot && max_penalty != 0; + /* Try one time without restricting the search area, which generally results in better and more natural looking paths. * However the pathfinder can hit the node limit in certain situations such as long aqueducts or maze-like terrain. * If that happens we run the pathfinder again, but restricted only to the regions provided by the region pathfinder. */ @@ -229,13 +250,28 @@ public: /* Set origin and destination nodes */ pf.SetOrigin(v->tile, forward_dirs | reverse_dirs); - pf.SetDestination(v); - const bool is_intermediate_destination = static_cast(high_level_path.size()) >= NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1; - if (is_intermediate_destination) pf.SetIntermediateDestination(high_level_path.back()); + if (find_closest_depot) { + pf.SetAnyShipDepotDestination(); + } else { + pf.SetDestination(v); + } + pf.SetMaxCost(max_penalty); + + const std::span high_level_path_span(high_level_path.data(), std::min(high_level_path.size(), NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1)); + const bool is_intermediate_destination = static_cast(high_level_path_span.size()) >= NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1; + if (is_intermediate_destination) { + if (automatic_servicing) { + /* Automatic servicing requires a valid path cost from start to end. + * However, when an intermediate destination is set, the resulting cost + * cannot be used to determine if it falls within the maximum allowed penalty. */ + return INVALID_TRACKDIR; + } + pf.SetIntermediateDestination(high_level_path_span.back()); + } /* Restrict the search area to prevent the low level pathfinder from expanding too many nodes. This can happen * when the terrain is very "maze-like" or when the high level path "teleports" via a very long aqueduct. */ - if (attempt > 0) pf.RestrictSearch(high_level_path); + if (attempt > 0) pf.RestrictSearch(high_level_path_span); /* Find best path. */ path_found = pf.FindPath(v); @@ -245,6 +281,12 @@ public: /* 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 early when only searching for the closest depot tile. */ + if (find_closest_depot) { + tile = is_intermediate_destination ? pf.GetShipDepotDestination(high_level_path.back()) : node->GetTile(); + return INVALID_TRACKDIR; + } + /* 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. */ @@ -254,7 +296,7 @@ public: while (node->parent) { const WaterRegionPatchDesc node_water_patch = GetWaterRegionPatchInfo(node->GetTile()); - const bool node_water_patch_on_high_level_path = std::ranges::find(high_level_path, node_water_patch) != high_level_path.end(); + const bool node_water_patch_on_high_level_path = std::ranges::find(high_level_path_span, node_water_patch) != high_level_path_span.end(); const bool add_full_path = !is_intermediate_destination && node_water_patch != end_water_patch; /* The cached path must always lead to a region patch that's on the high level path. @@ -303,6 +345,7 @@ public: { bool path_found = false; ShipPathCache dummy_cache; + TileIndex tile = v->tile; Trackdir best_origin_dir = INVALID_TRACKDIR; if (trackdir == nullptr) { @@ -310,17 +353,44 @@ public: const Trackdir reverse_dir = ReverseTrackdir(v->GetVehicleTrackdir()); const TrackdirBits forward_dirs = TrackdirToTrackdirBits(v->GetVehicleTrackdir()); const TrackdirBits reverse_dirs = TrackdirToTrackdirBits(reverse_dir); - (void)ChooseShipTrack(v, v->tile, forward_dirs, reverse_dirs, path_found, dummy_cache, best_origin_dir); + (void)ChooseShipTrack(v, tile, forward_dirs, reverse_dirs, 0, path_found, dummy_cache, best_origin_dir); return path_found && best_origin_dir == reverse_dir; } else { /* 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_dirs = DiagdirReachesTrackdirs(entry) & TrackStatusToTrackdirBits(GetTileTrackStatus(v->tile, TRANSPORT_WATER, 0, entry)); - (void)ChooseShipTrack(v, v->tile, TRACKDIR_BIT_NONE, reverse_dirs, path_found, dummy_cache, best_origin_dir); + (void)ChooseShipTrack(v, tile, TRACKDIR_BIT_NONE, reverse_dirs, 0, path_found, dummy_cache, best_origin_dir); *trackdir = path_found && best_origin_dir != INVALID_TRACKDIR ? best_origin_dir : GetRandomTrackdir(reverse_dirs); return true; } } + + /** + * Find the best depot for a ship. + * @param v Ship + * @param max_penalty maximum pathfinder cost. + * @return FindDepotData with the best depot tile, cost and whether to reverse. + */ + static inline FindDepotData FindNearestDepot(const Ship *v, int max_penalty) + { + FindDepotData depot; + + bool path_found = false; + ShipPathCache dummy_cache; + TileIndex tile = INVALID_TILE; + Trackdir best_origin_dir = INVALID_TRACKDIR; + const bool search_both_ways = max_penalty == 0; + const Trackdir forward_dir = v->GetVehicleTrackdir(); + const Trackdir reverse_dir = ReverseTrackdir(forward_dir); + const TrackdirBits forward_dirs = TrackdirToTrackdirBits(forward_dir); + const TrackdirBits reverse_dirs = search_both_ways ? TrackdirToTrackdirBits(reverse_dir) : TRACKDIR_BIT_NONE; + (void)ChooseShipTrack(v, tile, forward_dirs, reverse_dirs, max_penalty, path_found, dummy_cache, best_origin_dir); + if (path_found) { + assert(tile != INVALID_TILE); + depot.tile = tile; + } + return depot; + } }; /** Cost Provider module of YAPF for ships. */ @@ -333,6 +403,11 @@ public: typedef typename Types::NodeList::Item Node; ///< this will be our node type. typedef typename Node::Key Key; ///< key to hash tables. +protected: + int max_cost; + + CYapfCostShipT() : max_cost(0) {} + /** to access inherited path finder */ Tpf &Yapf() { @@ -340,6 +415,11 @@ public: } public: + inline void SetMaxCost(int cost) + { + this->max_cost = cost; + } + inline int CurveCost(Trackdir td1, Trackdir td2) { assert(IsValidTrackdir(td1)); @@ -384,6 +464,10 @@ public: uint8_t speed_frac = (GetEffectiveWaterClass(n.GetTile()) == WATER_CLASS_SEA) ? svi->ocean_speed_frac : svi->canal_speed_frac; if (speed_frac > 0) c += YAPF_TILE_LENGTH * (1 + tf->tiles_skipped) * speed_frac / (256 - speed_frac); + /* Finish if we already exceeded the maximum path cost (i.e. when + * searching for the nearest depot). */ + if (this->max_cost > 0 && (n.parent->cost + c) > this->max_cost) return false; + /* Apply it. */ n.cost = n.parent->cost + c; return true; @@ -422,7 +506,7 @@ Track YapfShipChooseTrack(const Ship *v, TileIndex tile, bool &path_found, ShipP { Trackdir best_origin_dir = INVALID_TRACKDIR; const TrackdirBits origin_dirs = TrackdirToTrackdirBits(v->GetVehicleTrackdir()); - const Trackdir td_ret = CYapfShip::ChooseShipTrack(v, tile, origin_dirs, TRACKDIR_BIT_NONE, path_found, path_cache, best_origin_dir); + const Trackdir td_ret = CYapfShip::ChooseShipTrack(v, tile, origin_dirs, TRACKDIR_BIT_NONE, 0, path_found, path_cache, best_origin_dir); return (td_ret != INVALID_TRACKDIR) ? TrackdirToTrack(td_ret) : INVALID_TRACK; } @@ -430,3 +514,8 @@ bool YapfShipCheckReverse(const Ship *v, Trackdir *trackdir) { return CYapfShip::CheckShipReverse(v, trackdir); } + +FindDepotData YapfShipFindNearestDepot(const Ship *v, int max_penalty) +{ + return CYapfShip::FindNearestDepot(v, max_penalty); +} diff --git a/src/pathfinder/yapf/yapf_ship_regions.cpp b/src/pathfinder/yapf/yapf_ship_regions.cpp index 0a1a831a05..1991321475 100644 --- a/src/pathfinder/yapf/yapf_ship_regions.cpp +++ b/src/pathfinder/yapf/yapf_ship_regions.cpp @@ -119,6 +119,7 @@ public: protected: Key dest; + bool any_ship_depot = false; public: void SetDestination(const WaterRegionPatchDesc &water_region_patch) @@ -126,18 +127,32 @@ public: this->dest.Set(water_region_patch); } + void SetAnyShipDepotDestination() + { + this->any_ship_depot = true; + } + protected: + TestTileIndexCallBack detect_ship_depot = [&](const TileIndex tile) + { + return IsShipDepotTile(tile) && GetShipDepotPart(tile) == DEPOT_PART_NORTH && IsTileOwner(tile, Yapf().GetVehicle()->owner); + }; + Tpf &Yapf() { return *static_cast(this); } public: - inline bool PfDetectDestination(Node &n) const + inline bool PfDetectDestination(Node &n) { + if (this->any_ship_depot) { + return GetTileInWaterRegionPatch(n.key.water_region_patch, this->detect_ship_depot) != INVALID_TILE; + } + return n.key == this->dest; } inline bool PfCalcEstimate(Node &n) { - if (this->PfDetectDestination(n)) { + if (this->any_ship_depot || this->PfDetectDestination(n)) { n.estimate = n.cost; return true; } @@ -218,6 +233,31 @@ public: assert(!path.empty()); return path; } + + static std::vector FindShipDepotRegionPath(const Ship *v) + { + const WaterRegionPatchDesc 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(Map::Size() * NODES_PER_REGION) / WATER_REGION_NUMBER_OF_TILES, MAX_NUMBER_OF_NODES)); + pf.AddOrigin(start_water_region_patch); + pf.SetAnyShipDepotDestination(); + + /* Find best path. */ + if (!pf.FindPath(v)) return {}; // Path not found. + + std::vector path; + Node *node = pf.GetBestNode(); + while (node != nullptr) { + path.push_back(node->key.water_region_patch); + node = node->parent; + } + + assert(!path.empty()); + std::ranges::reverse(path); + return path; + } }; /** Cost Provider of YAPF for water regions. */ @@ -296,5 +336,8 @@ struct CYapfRegionWater : CYapfT YapfShipFindWaterRegionPath(const Ship *v, TileIndex start_tile, int max_returned_path_length) { + const bool find_closest_depot = start_tile == INVALID_TILE; + + if (find_closest_depot) return CYapfRegionWater::FindShipDepotRegionPath(v); return CYapfRegionWater::FindWaterRegionPath(v, start_tile, max_returned_path_length); } diff --git a/src/pathfinder/yapf/yapf_ship_regions.h b/src/pathfinder/yapf/yapf_ship_regions.h index dc488754b4..318c867769 100644 --- a/src/pathfinder/yapf/yapf_ship_regions.h +++ b/src/pathfinder/yapf/yapf_ship_regions.h @@ -16,5 +16,6 @@ struct Ship; std::vector YapfShipFindWaterRegionPath(const Ship *v, TileIndex start_tile, int max_returned_path_length); +std::vector YapfFindShipDepotRegionPath(const Ship *v); #endif /* YAPF_SHIP_REGIONS_H */ diff --git a/src/ship_cmd.cpp b/src/ship_cmd.cpp index 956d730252..7a67ad0569 100644 --- a/src/ship_cmd.cpp +++ b/src/ship_cmd.cpp @@ -17,7 +17,6 @@ #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" @@ -39,13 +38,8 @@ #include "table/strings.h" -#include - #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; - /** * Determine the effective #WaterClass for a ship travelling on a tile. * @param tile Tile of interest @@ -150,55 +144,13 @@ void Ship::GetImage(Direction direction, EngineImageType image_type, VehicleSpri static const Depot *FindClosestShipDepot(const Vehicle *v, uint max_distance) { - const int max_region_distance = (max_distance / WATER_REGION_EDGE_LENGTH) + 1; + const TileIndex tile = v->tile; + if (IsShipDepotTile(tile) && IsTileOwner(tile, v->owner)) return Depot::GetByTile(tile); - static std::unordered_set visited_patch_hashes; - static std::deque patches_to_search; - visited_patch_hashes.clear(); - patches_to_search.clear(); + FindDepotData sfdd = YapfShipFindNearestDepot(Ship::From(v), max_distance); - /* 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 neighbours of the current patch to the search queue. */ - VisitWaterRegionPatchCallback visit_func = [&](const WaterRegionPatchDesc &water_region_patch) { - /* Note that we check the max distance per axis, not the total distance. */ - if (std::abs(water_region_patch.x - start_patch.x) > max_region_distance || - std::abs(water_region_patch.y - start_patch.y) > max_region_distance) return; - - const int hash = CalculateWaterRegionPatchHash(water_region_patch); - if (visited_patch_hashes.count(hash) == 0) { - visited_patch_hashes.insert(hash); - patches_to_search.push_back(water_region_patch); - } - }; - - VisitWaterRegionPatchNeighbours(current_node, visit_func); - } - - /* Step 2: Find the closest depot within the reachable Water Region Patches. */ - const Depot *best_depot = nullptr; - uint best_dist_sq = std::numeric_limits::max(); - for (const Depot *depot : Depot::Iterate()) { - const TileIndex tile = depot->xy; - if (IsShipDepotTile(tile) && IsTileOwner(tile, v->owner)) { - const uint dist_sq = DistanceSquare(tile, v->tile); - if (dist_sq < best_dist_sq && dist_sq <= max_distance * max_distance && - visited_patch_hashes.count(CalculateWaterRegionPatchHash(GetWaterRegionPatchInfo(tile))) > 0) { - best_dist_sq = dist_sq; - best_depot = depot; - } - } - } - - return best_depot; + if (sfdd.tile == INVALID_TILE) return nullptr; + return Depot::GetByTile(sfdd.tile); } static void CheckIfShipNeedsService(Vehicle *v) @@ -209,7 +161,7 @@ static void CheckIfShipNeedsService(Vehicle *v) return; } - uint max_distance = _settings_game.pf.yapf.maximum_go_to_depot_penalty / YAPF_TILE_LENGTH; + uint max_distance = _settings_game.pf.yapf.maximum_go_to_depot_penalty; const Depot *depot = FindClosestShipDepot(v, max_distance); @@ -956,7 +908,7 @@ CommandCost CmdBuildShip(DoCommandFlags flags, TileIndex tile, const Engine *e, ClosestDepot Ship::FindClosestDepot() { - const Depot *depot = FindClosestShipDepot(this, MAX_SHIP_DEPOT_SEARCH_DISTANCE); + const Depot *depot = FindClosestShipDepot(this, 0); if (depot == nullptr) return ClosestDepot(); return ClosestDepot(depot->xy, depot->index);