From 7a065ee3032023207dda42c1ffd3a5f0c1c89cc0 Mon Sep 17 00:00:00 2001 From: Koen Bussemaker Date: Sun, 5 Mar 2023 13:23:21 +0100 Subject: [PATCH] Feature: Region-based pathfinder for ships --- src/depot_gui.cpp | 4 +- src/genworld.cpp | 3 + src/misc_gui.cpp | 4 + src/pathfinder/CMakeLists.txt | 2 + src/pathfinder/water_regions.cpp | 463 ++++++++++++++++++++++ src/pathfinder/water_regions.h | 53 +++ src/pathfinder/water_regions_sl.txt | 0 src/pathfinder/yapf/CMakeLists.txt | 2 + src/pathfinder/yapf/yapf_base.hpp | 27 ++ src/pathfinder/yapf/yapf_ship.cpp | 207 +++++++--- src/pathfinder/yapf/yapf_ship_regions.cpp | 360 +++++++++++++++++ src/pathfinder/yapf/yapf_ship_regions.h | 45 +++ src/saveload/CMakeLists.txt | 1 + src/saveload/afterload.cpp | 6 + src/saveload/saveload.cpp | 2 + src/saveload/saveload.h | 1 + src/saveload/water_regions_sl.cpp | 57 +++ src/ship_cmd.cpp | 5 + src/track_func.h | 1 + src/track_type.h | 2 + src/tunnelbridge_cmd.cpp | 3 + src/viewport.cpp | 118 +++++- src/viewport_func.h | 5 + src/water_cmd.cpp | 25 +- src/waypoint_cmd.cpp | 2 + 25 files changed, 1330 insertions(+), 68 deletions(-) create mode 100644 src/pathfinder/water_regions.cpp create mode 100644 src/pathfinder/water_regions.h create mode 100644 src/pathfinder/water_regions_sl.txt create mode 100644 src/pathfinder/yapf/yapf_ship_regions.cpp create mode 100644 src/pathfinder/yapf/yapf_ship_regions.h create mode 100644 src/saveload/water_regions_sl.cpp diff --git a/src/depot_gui.cpp b/src/depot_gui.cpp index 7f454946ce008..1e6bc456c285a 100644 --- a/src/depot_gui.cpp +++ b/src/depot_gui.cpp @@ -908,7 +908,9 @@ struct DepotWindow : Window { { if (_ctrl_pressed) { /* Share-clone, do not open new viewport, and keep tool active */ - Command::Post(STR_ERROR_CAN_T_BUY_TRAIN + v->type, this->window_number, v->index, true); + for (int i = 0; i < 200; ++i) { // TODO KB for performance testing, remove again + Command::Post(STR_ERROR_CAN_T_BUY_TRAIN + v->type, this->window_number, v->index, true); + } } else { /* Copy-clone, open viewport for new vehicle, and deselect the tool (assume player wants to change things on new vehicle) */ if (Command::Post(STR_ERROR_CAN_T_BUY_TRAIN + v->type, CcCloneVehicle, this->window_number, v->index, false)) { diff --git a/src/genworld.cpp b/src/genworld.cpp index 4ae44b98eb063..9b670b3d55a6e 100644 --- a/src/genworld.cpp +++ b/src/genworld.cpp @@ -34,6 +34,7 @@ #include "string_func.h" #include "thread.h" #include "tgp.h" +#include "pathfinder/water_regions.h" // TODO KB for ship pathfinder reset #include "safeguards.h" @@ -173,6 +174,8 @@ static void _GenerateWorld() } } + InitializeWaterRegions(); // TODO remove? + BasePersistentStorageArray::SwitchMode(PSM_LEAVE_GAMELOOP); ResetObjectToPlace(); diff --git a/src/misc_gui.cpp b/src/misc_gui.cpp index 896708b77a593..1c5924d5c98a7 100644 --- a/src/misc_gui.cpp +++ b/src/misc_gui.cpp @@ -28,6 +28,7 @@ #include "viewport_func.h" #include "landscape_cmd.h" #include "rev.h" +#include "pathfinder/water_regions.h" #include "widgets/misc_widget.h" @@ -128,6 +129,9 @@ class LandInfoWindow : public Window { Debug(misc, LANDINFOD_LEVEL, "m7 = {:#x}", tile.m7()); Debug(misc, LANDINFOD_LEVEL, "m8 = {:#x}", tile.m8()); #undef LANDINFOD_LEVEL + + // TODO debug code, remove again + DEBUG_UpdateWaterRegion(tile); } void OnInit() override diff --git a/src/pathfinder/CMakeLists.txt b/src/pathfinder/CMakeLists.txt index 0616371622dbe..adf896895df05 100644 --- a/src/pathfinder/CMakeLists.txt +++ b/src/pathfinder/CMakeLists.txt @@ -5,4 +5,6 @@ add_files( follow_track.hpp pathfinder_func.h pathfinder_type.h + water_regions.h + water_regions.cpp ) diff --git a/src/pathfinder/water_regions.cpp b/src/pathfinder/water_regions.cpp new file mode 100644 index 0000000000000..5233a531562e8 --- /dev/null +++ b/src/pathfinder/water_regions.cpp @@ -0,0 +1,463 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file water_regions.h Handles dividing the water in the map into regions to assist pathfinding. */ + +#include "stdafx.h" +#include "map_func.h" +#include "water_regions.h" +#include "map_func.h" +#include "tilearea_type.h" +#include "track_func.h" +#include "transport_type.h" +#include "landscape.h" +#include "tunnelbridge_map.h" +#include "follow_track.hpp" +#include "ship.h" +#include "viewport_func.h" // For debug drawing +#include "debug.h" + +#include +#include +#include +#include + +inline TrackBits GetWaterTracks(TileIndex tile) +{ + return TrackStatusToTrackBits(GetTileTrackStatus(tile, TRANSPORT_WATER, 0)); +} + +bool IsAqueductTile(TileIndex tile) +{ + return IsBridgeTile(tile) && GetTunnelBridgeTransportType(tile) == TRANSPORT_WATER; +} + +using TTraversabilityBits = uint16; +static constexpr TWaterRegionLabel FIRST_REGION_LABEL = 1; + +static_assert(sizeof(TTraversabilityBits) * 8 == WATER_REGION_SIZE); +static_assert(std::numeric_limits::max() <= WATER_REGION_SIZE * WATER_REGION_SIZE - 1); + +/** + * Represents the indivual water patches within a certain square region of the map. These individual patches are identifier + * using a Connected Component Labeling (CCL) algorithm. The data stores within this class applies to this individual region + * alone, there is no knowledge about any of the surrounding regions or the rest of the map. This makes it easy to invalidate + * and update a water region if any changes are made to it, such as construction or terraforming. + */ +class WaterRegion +{ +private: + std::array edge_traversability_bits{}; + bool has_cross_region_aqueducts = false; + TWaterRegionLabel number_of_labels; // 0 = no water, 1 = one single patch of water, etc... + const OrthogonalTileArea tile_area; + std::array labels; + bool initialized = false; + +public: + WaterRegion(int region_x, int region_y) + : tile_area(TileXY(region_x * WATER_REGION_SIZE, region_y * WATER_REGION_SIZE), WATER_REGION_SIZE, WATER_REGION_SIZE) + {} + + OrthogonalTileIterator begin() const { return this->tile_area.begin(); } + OrthogonalTileIterator end() const { return this->tile_area.end(); } + + bool IsInitialized() const { return this->initialized; } + + void Invalidate() { this->initialized = false; } + + /** + * Returns a set of bits indicating whether an edge tile on a particular side is traversable or not. These + * values can be used to determine whether a ship can enter/leave the region through a particular edge tile. + * @see GetLocalIndex() for a description of the coordinate system used. + * @param side Which side of the region we want to know the edge traversability of. + * @returns A value holding the edge traversability bits. + */ + TTraversabilityBits GetEdgeTraversabilityBits(DiagDirection side) const { return edge_traversability_bits[side]; } + + /** + * @returns The amount of different labels present within the water region. A value of + * 0 means there is no water present in the water region at all. + */ + int NumberOfLabels() const { return this->number_of_labels; } + + /** + * @returns Whether the water region contains aqueducts that cross the region boundaries. + */ + bool HasCrossRegionAqueducts() const { return this->has_cross_region_aqueducts; } + + /** + * Returns the local index of the tile within the region. The N corner represents 0, + * the x direction is positive in the SW direction, and Y is positive in the SE direction. + * @param tile Tile within the water region. + * @returns The local index. + */ + int GetLocalIndex(TileIndex tile) const + { + assert(this->tile_area.Contains(tile)); + return (TileX(tile) - TileX(this->tile_area.tile)) + WATER_REGION_SIZE * (TileY(tile) - TileY(this->tile_area.tile)); + } + + /** + * Returns the label that was assigned to the tile. + * @param tile The tile of which we want to retrieve the label. + * @returns The label assigned to the tile. + */ + TWaterRegionLabel GetLabel(TileIndex tile) const + { + assert(this->tile_area.Contains(tile)); + return this->labels[GetLocalIndex(tile)]; + } + + /** + * Performs the connected component labeling and other data gathering. + * @see WaterRegion class definition for more information. + */ + void ForceUpdate(bool debug_mode = false) + { + if (debug_mode) DEBUG_clearDrawInstructions(); + this->has_cross_region_aqueducts = false; + + for (const TileIndex tile : this->tile_area) { + this->labels[GetLocalIndex(tile)] = INVALID_WATER_REGION_LABEL; + if (IsAqueductTile(tile)) { + const TileIndex other_aqueduct_end = GetOtherBridgeEnd(tile); + if (!tile_area.Contains(other_aqueduct_end)) this->has_cross_region_aqueducts = true; + } + } + + std::unordered_map temp_labels; + for (const TileIndex tile : this->tile_area) temp_labels[tile] = INVALID_WATER_REGION_LABEL; + TWaterRegionLabel label_counter = 1; + + for (TileIndex start_tile : tile_area) { + static std::vector tiles_to_check; + tiles_to_check.clear(); + tiles_to_check.push_back(start_tile); + + bool increase_label = false; + while (!tiles_to_check.empty()) { + const TileIndex tile = tiles_to_check.back(); + tiles_to_check.pop_back(); + + const TrackdirBits valid_dirs = TrackBitsToTrackdirBits(GetWaterTracks(tile)); + if (!valid_dirs) continue; + + const TWaterRegionLabel tile_label = temp_labels[tile]; + if (tile_label != INVALID_WATER_REGION_LABEL) continue; + + temp_labels[tile] = label_counter; + increase_label = true; + + for (Trackdir dir : SetTrackdirBitIterator(valid_dirs)) { + if (debug_mode) DEBUG_addDrawInstruction(tile, TrackdirToTrack(dir), 1, true); + + CFollowTrackWater ft; + if (ft.Follow(tile, dir) + && this->tile_area.Contains(ft.m_new_tile)) { + tiles_to_check.push_back(ft.m_new_tile); + } + } + } + + if (increase_label) label_counter++; + } + + for (const auto [tile, label] : temp_labels) labels[GetLocalIndex(tile)] = label; + + int highest_label = label_counter - 1; + + this->number_of_labels = highest_label; + this->initialized = true; + + /* Calculate the traversability(whether the tile can be entered / exited) for all edges. Note that + * we always follow the same X and Y scanning direction, this is important for comparisons later on! */ + this->edge_traversability_bits.fill(0); + const int top_x = TileX(tile_area.tile); + const int top_y = TileY(tile_area.tile); + for (int i = 0; i < WATER_REGION_SIZE; ++i) { + if (GetWaterTracks(TileXY(top_x + i, top_y)) & TRACK_BIT_3WAY_NW) SetBit(this->edge_traversability_bits[DIAGDIR_NW], i); // NW edge + if (GetWaterTracks(TileXY(top_x + i, top_y + WATER_REGION_SIZE - 1)) & TRACK_BIT_3WAY_SE) SetBit(this->edge_traversability_bits[DIAGDIR_SE], i); // SE edge + if (GetWaterTracks(TileXY(top_x, top_y + i)) & TRACK_BIT_3WAY_NE) SetBit(this->edge_traversability_bits[DIAGDIR_NE], i); // NE edge + if (GetWaterTracks(TileXY(top_x + WATER_REGION_SIZE - 1, top_y + i)) & TRACK_BIT_3WAY_SW) SetBit(this->edge_traversability_bits[DIAGDIR_SW], i); // SW edge + } + } + + /** + * Updates the labels and other data, but only if the region is not yet initialized. + */ + void UpdateIfNotInitialized(bool debug_mode = false) + { + if (this->initialized && !debug_mode) return; + ForceUpdate(debug_mode); + } +}; + +std::vector _water_regions; + +TileIndex GetTileIndexFromLocalCoordinate(int region_x, int region_y, int local_x, int local_y) +{ + assert(local_x >= 0 && local_y < WATER_REGION_SIZE); + assert(local_y >= 0 && local_y < WATER_REGION_SIZE); + return TileXY(WATER_REGION_SIZE * region_x + local_x, WATER_REGION_SIZE * region_y + local_y); +} + +TileIndex GetEdgeTileCoordinate(int region_x, int region_y, DiagDirection side, int x_or_y) // TODO better name +{ + assert(x_or_y >= 0 && x_or_y < WATER_REGION_SIZE); + switch (side) + { + case DIAGDIR_NE: return GetTileIndexFromLocalCoordinate(region_x, region_y, 0, x_or_y); + case DIAGDIR_SW: return GetTileIndexFromLocalCoordinate(region_x, region_y, WATER_REGION_SIZE - 1, x_or_y); + case DIAGDIR_NW: return GetTileIndexFromLocalCoordinate(region_x, region_y, x_or_y, 0); + case DIAGDIR_SE: return GetTileIndexFromLocalCoordinate(region_x, region_y, x_or_y, WATER_REGION_SIZE - 1); + default: NOT_REACHED(); + } +} + +int GetWaterRegionMapSizeX() +{ + return Map::SizeX() / WATER_REGION_SIZE; +} + +int GetWaterRegionMapSizeY() +{ + return Map::SizeY() / WATER_REGION_SIZE; +} + +int GetWaterRegionX(TWaterRegionIndex region_index) +{ + return region_index % GetWaterRegionMapSizeX(); +} + +int GetWaterRegionY(TWaterRegionIndex region_index) +{ + return region_index / GetWaterRegionMapSizeX(); +} + +int GetWaterRegionX(TileIndex tile) +{ + return TileX(tile) / WATER_REGION_SIZE; +} + +int GetWaterRegionY(TileIndex tile) +{ + return TileY(tile) / WATER_REGION_SIZE; +} + +/** + * Returns the center tile of a particular water region. + * @param region_x X coordinate of the water region. + * @param region_x Y coordinate of the water region. + * @returns The center tile of the water region. + */ +TileIndex GetWaterRegionCenterTile(int region_x, int region_y) +{ + return TileXY(region_x * WATER_REGION_SIZE + (WATER_REGION_SIZE / 2), + region_y * WATER_REGION_SIZE + (WATER_REGION_SIZE / 2)); +} + +TWaterRegionIndex GetWaterRegionIndex(int region_x, int region_y) +{ + // TODO checking asserts + return GetWaterRegionMapSizeX() * region_y + region_x; +} + +TWaterRegionIndex GetWaterRegionIndex(TileIndex tile) +{ + return GetWaterRegionIndex(GetWaterRegionX(tile), GetWaterRegionY(tile)); +} + +WaterRegion& GetUpdatedWaterRegion(uint16 region_x, uint16 region_y) +{ + WaterRegion& result = _water_regions[GetWaterRegionIndex(region_x, region_y)]; + result.UpdateIfNotInitialized(); + return result; +} + +WaterRegion& GetUpdatedWaterRegion(TileIndex tile) +{ + WaterRegion& result = _water_regions[GetWaterRegionIndex(tile)]; + result.UpdateIfNotInitialized(); + return result; +} + +/** + * Retrieves the label assigned to the provided tile. Note that this triggers an update + * of thewater region if the region is not yet initialized or was invalidated. + * @param tile Tile of which we want to retrieve the label. + * @returns The label of the tile. + */ +TWaterRegionLabel GetWaterRegionTileLabel(TileIndex tile) +{ + return GetUpdatedWaterRegion(tile).GetLabel(tile); +} + +/** + * Invalidates the water region that the tile is part of. + * @param tile Tile within the water region that we wish to invalidate. + */ +void InvalidateWaterRegion(TileIndex tile) +{ + int index = GetWaterRegionIndex(tile); + if (index > _water_regions.size()) return; + _water_regions[index].Invalidate(); + for (TileIndex t : _water_regions[index]) { + DEBUG_addDrawRectangleInstruction(t, 1, true); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + } + DEBUG_addDrawRectangleInstruction(tile, 5, true); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +} + +bool DEBUG_IsWaterRegionSimple(TileIndex tile) +{ + const int index = GetWaterRegionIndex(tile); + if (index > _water_regions.size()) return false; + return _water_regions[index].NumberOfLabels() == 1; +} + +void DEBUG_UpdateWaterRegion(TileIndex tile) +{ + GetUpdatedWaterRegion(tile).UpdateIfNotInitialized(true); +} + +/** + * Calls the provided callback function for all accessible water region labels + * accessible from one particular side of the starting region. + * @param region_x X coordinate of the water region to start from + * @param region_y Y coordinate of the water region to start from + * @param region_label Label within the water region to start from + * @param callback The function that will be called for each neighbor that is found + */ +inline void VisitNeighborLabels(DiagDirection side, int region_x, int region_y, TWaterRegionLabel region_label, TAddRegionCallBack& func) +{ + WaterRegion& current_region = GetUpdatedWaterRegion(region_x, region_y); + + const TileIndexDiffC offset = TileIndexDiffCByDiagDir(side); + const int nx = region_x + offset.x; + const int ny = region_y + offset.y; + + if (nx < 0 || ny < 0 || nx >= GetWaterRegionMapSizeX() || ny >= GetWaterRegionMapSizeY()) return; + + const WaterRegion& neighboring_region = GetUpdatedWaterRegion(nx, ny); + const DiagDirection opposite_side = ReverseDiagDir(side); + + /* Indicates via which local x or y coordinates we can cross over into the next region. */ + const TTraversabilityBits traversability_bits = current_region.GetEdgeTraversabilityBits(side) + & neighboring_region.GetEdgeTraversabilityBits(opposite_side); + if (traversability_bits == 0) return; + + if (current_region.NumberOfLabels() == 1 && neighboring_region.NumberOfLabels() == 1) { + func(nx, ny, FIRST_REGION_LABEL); // No further checks needed because we know there is just one label per region + } + else { + /* Multiple labels case. Check each edge tile individually */ + static std::vector unique_labels; + unique_labels.clear(); + for (int x_or_y = 0; x_or_y < WATER_REGION_SIZE; ++x_or_y) { + if (!HasBit(traversability_bits, x_or_y)) continue; + + const TileIndex current_edge_tile = GetEdgeTileCoordinate(region_x, region_y, side, x_or_y); + const TWaterRegionLabel current_label = current_region.GetLabel(current_edge_tile); + if (current_label != region_label) continue; + + const TileIndex neighbor_edge_tile = GetEdgeTileCoordinate(nx, ny, opposite_side, x_or_y); + const TWaterRegionLabel neighbor_label = neighboring_region.GetLabel(neighbor_edge_tile); + if (std::find(unique_labels.begin(), unique_labels.end(), neighbor_label) == unique_labels.end()) unique_labels.push_back(neighbor_label); + } + for (TWaterRegionLabel unique_label : unique_labels) func(nx, ny, unique_label); + } +} + +/** + * Calls the provided callback function on all accessible water region labels in each + * cardinal direction, plus any others that are reachable via aqueducts. + * @param region_x X coordinate of the water region to start from + * @param region_y Y coordinate of the water region to start from + * @param region_label Label within the water region to start from + * @param callback The function that will be called for each neighbor that is found + */ +void VisitWaterRegionNeighbors(int region_x, int region_y, TWaterRegionLabel region_label, TAddRegionCallBack& callback) +{ + WaterRegion& current_region = GetUpdatedWaterRegion(region_x, region_y); + + for (DiagDirection side = DIAGDIR_BEGIN; side < DIAGDIR_END; side++) { + VisitNeighborLabels(side, region_x, region_y, region_label, callback); + } + + /* Deal with aqueducts that span across multiple regions. */ + if (current_region.HasCrossRegionAqueducts()) { + for (TileIndex tile : current_region) { + if (GetWaterRegionTileLabel(tile) == region_label && IsAqueductTile(tile)) { + const TileIndex other_end_tile = GetOtherBridgeEnd(tile); + if (GetWaterRegionIndex(tile) != GetWaterRegionIndex(other_end_tile)) { + callback(GetWaterRegionX(other_end_tile), GetWaterRegionY(other_end_tile), GetWaterRegionTileLabel(other_end_tile)); + } + } + } + } +} + +void MarkSubSquare(int x, int y, int label, int color, bool override) // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +{ +#ifndef NDEBUG + for (int dx = 0; dx < WATER_REGION_SIZE; ++dx) { + for (int dy = 0; dy < WATER_REGION_SIZE; ++dy) { + int tx = x * WATER_REGION_SIZE + dx; + int ty = y * WATER_REGION_SIZE + dy; + TileIndex tile = TileXY(tx, ty); + + if (label < 0 || GetWaterRegionTileLabel(tile) == label) + DEBUG_addDrawRectangleInstruction(tile, color, override); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + } + } +#endif +} + +void MarkSubSquare(TWaterRegionIndex region_index, int color, bool override) +{ + MarkSubSquare(GetWaterRegionX(region_index), GetWaterRegionY(region_index), -1, color, override); +} + +std::vector GetWaterRegionSaveLoadInfo() +{ + std::vector result; + for (WaterRegion& region : _water_regions) result.push_back(WaterRegionSaveLoadInfo{ region.IsInitialized()}); + return result; +} + +void LoadWaterRegions(const std::vector& save_load_info) +{ + DEBUG_clearDrawInstructions(); + _water_regions.clear(); + _water_regions.reserve(save_load_info.size()); + TWaterRegionIndex index = 0; + for (const auto& loaded_region_info : save_load_info) { + WaterRegion water_region{ GetWaterRegionX(index), GetWaterRegionY(index) }; + if (loaded_region_info.initialized) water_region.UpdateIfNotInitialized(); + if (!loaded_region_info.initialized) MarkSubSquare(GetWaterRegionX(index), GetWaterRegionY(index), -1, 1); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + _water_regions.push_back(std::move(water_region)); + index++; + } +} + +/** + * Initializes all water regions. All water tiles will be scanned and indepent regions will be identified. + */ +void InitializeWaterRegions() +{ + DEBUG_clearDrawInstructions(); + + _water_regions.clear(); + _water_regions.reserve(GetWaterRegionMapSizeX() * GetWaterRegionMapSizeY()); + + for (int y = 0; y < GetWaterRegionMapSizeY(); y++) { + for (int x = 0; x < GetWaterRegionMapSizeX(); x++) { + _water_regions.push_back(WaterRegion{ x, y }); + _water_regions.back().UpdateIfNotInitialized(); + } + } +} diff --git a/src/pathfinder/water_regions.h b/src/pathfinder/water_regions.h new file mode 100644 index 0000000000000..6ba010c3be6bf --- /dev/null +++ b/src/pathfinder/water_regions.h @@ -0,0 +1,53 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file water_regions.h Handles dividing the water in the map into regions to assist pathfinding. */ + +#ifndef WATER_REGIONS_H +#define WATER_REGIONS_H + +#include "tile_type.h" + +#include +#include + +using TWaterRegionLabel = uint8; +using TWaterRegionIndex = uint; +using TAddRegionCallBack = std::function; + +constexpr TWaterRegionIndex INVALID_WATER_REGION = static_cast(-1); +constexpr TWaterRegionLabel INVALID_WATER_REGION_LABEL = 0; +constexpr int WATER_REGION_SIZE = 16; + +void InvalidateWaterRegion(TileIndex tile); + +bool DEBUG_IsWaterRegionSimple(TileIndex tile); +void DEBUG_UpdateWaterRegion(TileIndex tile); + +void VisitWaterRegionNeighbors(int region_x, int region_y, TWaterRegionLabel region_label, TAddRegionCallBack& callback); + +TileIndex GetWaterRegionCenterTile(int region_x, int region_y); +TWaterRegionIndex GetWaterRegionIndex(int region_x, int region_y); +TWaterRegionIndex GetWaterRegionIndex(TileIndex tile); +int GetWaterRegionX(TileIndex tile); +int GetWaterRegionY(TileIndex tile); +TWaterRegionLabel GetWaterRegionTileLabel(TileIndex tile); + +void InitializeWaterRegions(); + +void MarkSubSquare(int x, int y, int label, int color, bool override = false); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +void MarkSubSquare(TWaterRegionIndex region_index, int color, bool override = false); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + +struct WaterRegionSaveLoadInfo +{ + bool initialized; +}; + +std::vector GetWaterRegionSaveLoadInfo(); +void LoadWaterRegions(const std::vector& save_load_info); + +#endif /* WATER_REGIONS_H */ diff --git a/src/pathfinder/water_regions_sl.txt b/src/pathfinder/water_regions_sl.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pathfinder/yapf/CMakeLists.txt b/src/pathfinder/yapf/CMakeLists.txt index 170c1ad61dd74..6717233352b60 100644 --- a/src/pathfinder/yapf/CMakeLists.txt +++ b/src/pathfinder/yapf/CMakeLists.txt @@ -16,5 +16,7 @@ add_files( yapf_rail.cpp yapf_road.cpp yapf_ship.cpp + yapf_ship_regions.h + yapf_ship_regions.cpp yapf_type.hpp ) diff --git a/src/pathfinder/yapf/yapf_base.hpp b/src/pathfinder/yapf/yapf_base.hpp index 8c45efa1e3fe2..97ebc583f3fda 100644 --- a/src/pathfinder/yapf/yapf_base.hpp +++ b/src/pathfinder/yapf/yapf_base.hpp @@ -10,8 +10,11 @@ #ifndef YAPF_BASE_HPP #define YAPF_BASE_HPP +#define ENABLE_YAPF_DEBUG_DRAWING 1 + #include "../../debug.h" #include "../../settings_type.h" +#include "viewport_func.h" /** * CYapfBaseT - A-star type path finder base class. @@ -43,6 +46,15 @@ * declaration. There are some examples. For another example look at * test_yapf.h (part or unittest project). */ + +template +struct HasGetTile : std::false_type { +}; + +template +struct HasGetTile : std::true_type { +}; + template class CYapfBaseT { public: @@ -110,6 +122,11 @@ class CYapfBaseT { */ inline bool FindPath(const VehicleType *v) { +#ifdef ENABLE_YAPF_DEBUG_DRAWING + //DEBUG_clearDrawInstructions(); +#endif + + m_veh = v; Yapf().PfSetStartupNodes(); @@ -131,6 +148,10 @@ class CYapfBaseT { if (m_max_search_nodes == 0 || m_nodes.ClosedCount() < m_max_search_nodes) { m_nodes.PopOpenNode(n->GetKey()); m_nodes.InsertClosedNode(*n); +#ifdef ENABLE_YAPF_DEBUG_DRAWING + if constexpr(HasGetTile::value) DEBUG_addDrawInstruction(n->GetTile(), TrackdirToTrack(n->GetTrackdir()), 3, true);// 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + +#endif } else { bDestFound = false; break; @@ -176,6 +197,9 @@ class CYapfBaseT { /** Add new node (created by CreateNewNode and filled with data) into open list */ inline void AddStartupNode(Node &n) { +#ifdef ENABLE_YAPF_DEBUG_DRAWING + if constexpr (HasGetTile::value) DEBUG_addDrawInstruction(n.GetTile(), TrackdirToTrack(n.GetTrackdir()), 3);// 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +#endif Yapf().PfNodeCacheFetch(n); /* insert the new node only if it is not there */ if (m_nodes.FindOpenNode(n.m_key) == nullptr) { @@ -293,6 +317,9 @@ class CYapfBaseT { /* the new node is really new * add it to the open list */ m_nodes.InsertOpenNode(n); +#ifdef ENABLE_YAPF_DEBUG_DRAWING + if constexpr (HasGetTile::value) DEBUG_addDrawInstruction(n.GetTile(), TrackdirToTrack(n.GetTrackdir()), 1);// 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +#endif if (set_intermediate) m_pBestIntermediateNode = &n; } diff --git a/src/pathfinder/yapf/yapf_ship.cpp b/src/pathfinder/yapf/yapf_ship.cpp index 947de5a764806..71f008e5231d6 100644 --- a/src/pathfinder/yapf/yapf_ship.cpp +++ b/src/pathfinder/yapf/yapf_ship.cpp @@ -5,15 +5,19 @@ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ -/** @file yapf_ship.cpp Implementation of YAPF for ships. */ + /** @file yapf_ship.cpp Implementation of YAPF for ships. */ #include "../../stdafx.h" #include "../../ship.h" #include "../../industry.h" #include "../../vehicle_func.h" +#include + #include "yapf.hpp" #include "yapf_node_ship.hpp" +#include "yapf_ship_regions.h" +#include "../water_regions.h" #include "../../safeguards.h" @@ -31,20 +35,33 @@ class CYapfDestinationTileWaterT TrackdirBits m_destTrackdirs; StationID m_destStation; + bool m_has_intermediate_dest = false; + TileIndex m_intermediate_dest_tile; + WaterRegionDescriptor m_intermediate_dest_region; + public: - void SetDestination(const Ship *v) + void SetDestination(const Ship* v) { if (v->current_order.IsType(OT_GOTO_STATION)) { - m_destStation = v->current_order.GetDestination(); - m_destTile = CalcClosestStationTile(m_destStation, v->tile, STATION_DOCK); + m_destStation = v->current_order.GetDestination(); + m_destTile = CalcClosestStationTile(m_destStation, v->tile, STATION_DOCK); m_destTrackdirs = INVALID_TRACKDIR_BIT; - } else { - m_destStation = INVALID_STATION; - m_destTile = v->dest_tile; + } + else { + m_destStation = INVALID_STATION; + m_destTile = v->dest_tile; m_destTrackdirs = TrackStatusToTrackdirBits(GetTileTrackStatus(v->dest_tile, TRANSPORT_WATER, 0)); } } + void SetIntermediateDestination(WaterRegionDescriptor water_region) + { + m_has_intermediate_dest = true; + m_intermediate_dest_tile = GetWaterRegionCenterTile(water_region.x, water_region.y); + m_intermediate_dest_region = water_region; + DEBUG_addDrawRectangleInstruction(m_intermediate_dest_tile, 0, true); + } + protected: /** to access inherited path finder */ inline Tpf& Yapf() @@ -61,6 +78,11 @@ class CYapfDestinationTileWaterT inline bool PfDetectDestinationTile(TileIndex tile, Trackdir trackdir) { + if (m_has_intermediate_dest) + return GetWaterRegionX(tile) == m_intermediate_dest_region.x + && GetWaterRegionY(tile) == m_intermediate_dest_region.y + && GetWaterRegionTileLabel(tile) == m_intermediate_dest_region.label; + if (m_destStation != INVALID_STATION) { return IsDockingTile(tile) && IsShipDestinationTile(tile, m_destStation); } @@ -74,9 +96,11 @@ class CYapfDestinationTileWaterT */ inline bool PfCalcEstimate(Node& n) { - 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 (PfDetectDestination(n)) { + const TileIndex destination_tile = m_has_intermediate_dest ? m_intermediate_dest_tile : m_destTile; + + 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 (PfDetectDestination(n)) { // TODO change this to use the destination_tile above if possible? n.m_estimate = n.m_cost; return true; } @@ -85,8 +109,8 @@ class CYapfDestinationTileWaterT DiagDirection exitdir = TrackdirToExitdir(n.m_segment_last_td); int x1 = 2 * TileX(tile) + dg_dir_to_x_offs[(int)exitdir]; int y1 = 2 * TileY(tile) + dg_dir_to_y_offs[(int)exitdir]; - int x2 = 2 * TileX(m_destTile); - int y2 = 2 * TileY(m_destTile); + int x2 = 2 * TileX(destination_tile); + int y2 = 2 * TileY(destination_tile); int dx = abs(x1 - x2); int dy = abs(y1 - y2); int dmin = std::min(dx, dy); @@ -113,33 +137,65 @@ class CYapfFollowShipT /** to access inherited path finder */ inline Tpf& Yapf() { - return *static_cast(this); + return *static_cast(this); } + std::unordered_set m_water_region_corridor; + public: /** * Called by YAPF to move from the given node to the next tile. For each * reachable trackdir on the new tile creates new node, initializes it * and adds it to the open list by calling Yapf().AddNewNode(n) */ - inline void PfFollowNode(Node &old_node) + inline void PfFollowNode(Node& old_node) { TrackFollower F(Yapf().GetVehicle()); if (F.Follow(old_node.m_key.m_tile, old_node.m_key.m_td)) { - Yapf().AddMultipleNodes(&old_node, F); + if (m_water_region_corridor.empty() || m_water_region_corridor.find(GetWaterRegionIndex(F.m_new_tile)) != m_water_region_corridor.end()) { + Yapf().AddMultipleNodes(&old_node, F); + } } } + /* A B A + * C B C + * Create a corridor through which the ship is allowed to travel. Note that the 4-neighbor water region search + * might choose the "wrong" intermediate node B when traveling from A to C. This can lead to strange behavior + * at the tile level. We avoid this by adding the B tiles from both examples when such a situation is detected. */ + inline void RestrictSearch(const std::vector& path) + { + constexpr int MAX_TILES_IN_CORRIDOR = WATER_REGION_SIZE * WATER_REGION_SIZE * (2 * (NUMBER_OR_WATER_REGIONS_LOOKAHEAD + 1) - 1); + assert(MAX_TILES_IN_CORRIDOR < _settings_game.pf.yapf.max_search_nodes); + + m_water_region_corridor.clear(); + for (const auto& path_entry : path) m_water_region_corridor.insert(GetWaterRegionIndex(path_entry.x, path_entry.y)); + for (size_t i = 2; i < path.size(); ++i) { + const auto& current_region = path[i]; + const auto& grandparent_region = path[i - 2U]; + if (std::abs(current_region.x - grandparent_region.x) == 1 && std::abs(current_region.y - grandparent_region.y) == 1) { + m_water_region_corridor.insert(GetWaterRegionIndex(current_region.x, grandparent_region.y)); + m_water_region_corridor.insert(GetWaterRegionIndex(grandparent_region.x, current_region.y)); + } + } + + for (const auto& region_index : m_water_region_corridor) MarkSubSquare(region_index, 2, true); + for (const auto& region : path) MarkSubSquare(GetWaterRegionIndex(region.x, region.y), 4, true); + MarkSubSquare(GetWaterRegionIndex(path.back().x, path.back().y), 5, true); + } + /** return debug report character to identify the transportation type */ inline char TransportTypeChar() const { return 'w'; } - static Trackdir ChooseShipTrack(const Ship *v, TileIndex tile, DiagDirection enterdir, TrackBits tracks, bool &path_found, ShipPathCache &path_cache) + // TODO KB If the ship is completely surrounded then no high level path is search and the ships doesn't report "path not found"! + + static Trackdir ChooseShipTrack(const Ship* v, TileIndex tile, DiagDirection enterdir, TrackBits tracks, bool& path_found, ShipPathCache& path_cache) { /* handle special case - when next tile is destination tile */ - if (tile == v->dest_tile) { + if (tile == v->dest_tile) { /* convert tracks to trackdirs */ TrackdirBits trackdirs = TrackBitsToTrackdirBits(tracks); /* limit to trackdirs reachable from enterdir */ @@ -150,6 +206,13 @@ class CYapfFollowShipT return (HasTrackdir(trackdirs, veh_dir)) ? veh_dir : (Trackdir)FindFirstBit2x64(trackdirs); } + DEBUG_clearDrawInstructions(); + const auto high_level_result = YapfShipFindDestinationRegion(v, tile); + if (!high_level_result.path_found) { + path_found = false; + return INVALID_TRACKDIR; + } + /* move back to the old tile/trackdir (where ship is coming from) */ TileIndex src_tile = TileAddByDiagDir(tile, ReverseDiagDir(enterdir)); Trackdir trackdir = v->GetVehicleTrackdir(); @@ -163,40 +226,51 @@ class CYapfFollowShipT /* set origin and destination nodes */ pf.SetOrigin(src_tile, trackdirs); pf.SetDestination(v); + if (high_level_result.intermediate_destination) pf.SetIntermediateDestination(high_level_result.path.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. */ + pf.RestrictSearch(high_level_result.path); + /* find best path */ path_found = pf.FindPath(v); - - Trackdir next_trackdir = INVALID_TRACKDIR; // this would mean "path not found" - - Node *pNode = pf.GetBestNode(); - if (pNode != nullptr) { - uint steps = 0; - for (Node *n = pNode; n->m_parent != nullptr; n = n->m_parent) steps++; - uint skip = 0; - if (path_found) skip = YAPF_SHIP_PATH_CACHE_LENGTH / 2; - - /* walk through the path back to the origin */ - Node *pPrevNode = nullptr; - while (pNode->m_parent != nullptr) { - steps--; - /* Skip tiles at end of path near destination. */ - if (skip > 0) skip--; - if (skip == 0 && steps > 0 && steps < YAPF_SHIP_PATH_CACHE_LENGTH) { - path_cache.push_front(pNode->GetTrackdir()); - } - pPrevNode = pNode; - pNode = pNode->m_parent; + Node* node = pf.GetBestNode(); + if (!node) 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. */ + const WaterRegionDescriptor end_region = GetWaterRegionDescriptor(node->GetTile()); + std::deque> draw_stuff; + const WaterRegionDescriptor start_region = GetWaterRegionDescriptor(tile); + while (node->m_parent) { + const WaterRegionDescriptor node_region = GetWaterRegionDescriptor(node->GetTile()); + if (node_region == start_region || (!high_level_result.intermediate_destination && node_region != end_region)) { + path_cache.push_front(node->GetTrackdir()); + draw_stuff.push_front({ node->GetTile(), node->GetTrackdir() }); } - /* return trackdir from the best next node (direct child of origin) */ - Node &best_next_node = *pPrevNode; - assert(best_next_node.GetTile() == tile); - next_trackdir = best_next_node.GetTrackdir(); - /* remove last element for the special case when tile == dest_tile */ - if (path_found && !path_cache.empty()) path_cache.pop_back(); + node = node->m_parent; + } + assert(!path_cache.empty()); // TODO make this more robust + + /* Take out the last trackdir as the result */ + const Trackdir result = path_cache.front(); + path_cache.pop_front(); + + /* Clear path cache when in last water region. This is to allow ships to spread over different docking tiles dynamically. */ + if (start_region == end_region) { + path_cache.clear(); + draw_stuff.clear(); + } + + for (const auto [t, td] : draw_stuff) { + DEBUG_addDrawInstruction(t, TrackdirToTrack(td), 0, true);// 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent } - return next_trackdir; + return result; } + + /** * Check whether a ship should reverse to reach its destination. * Called when leaving depot. @@ -207,23 +281,34 @@ class CYapfFollowShipT * @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, TileIndex tile, Trackdir td1, Trackdir td2, Trackdir* trackdir) { + DEBUG_clearDrawInstructions(); + const auto high_level_result = YapfShipFindDestinationRegion(v, tile); + if (!high_level_result.path_found) { + if (trackdir) *trackdir = INVALID_TRACKDIR; + return false; + } + /* create pathfinder instance */ Tpf pf; /* set origin and destination nodes */ if (trackdir == nullptr) { pf.SetOrigin(tile, TrackdirToTrackdirBits(td1) | TrackdirToTrackdirBits(td2)); - } else { + } + 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_result.intermediate_destination) pf.SetIntermediateDestination(high_level_result.path.back()); + pf.RestrictSearch(high_level_result.path); + /* find best path */ if (!pf.FindPath(v)) return false; - Node *pNode = pf.GetBestNode(); + Node* pNode = pf.GetBestNode(); if (pNode == nullptr) return false; /* path was found @@ -235,7 +320,8 @@ class CYapfFollowShipT Trackdir best_trackdir = pNode->GetTrackdir(); if (trackdir != nullptr) { *trackdir = best_trackdir; - } else { + } + else { assert(best_trackdir == td1 || best_trackdir == td2); } return best_trackdir != td1; @@ -256,7 +342,7 @@ class CYapfCostShipT /** to access inherited path finder */ Tpf& Yapf() { - return *static_cast(this); + return *static_cast(this); } public: @@ -268,16 +354,17 @@ class CYapfCostShipT if (HasTrackdir(TrackdirCrossesTrackdirs(td1), td2)) { /* 90-deg curve penalty */ return Yapf().PfGetSettings().ship_curve90_penalty; - } else if (td2 != NextTrackdir(td1)) { + } + else if (td2 != NextTrackdir(td1)) { /* 45-deg curve penalty */ return Yapf().PfGetSettings().ship_curve45_penalty; } return 0; } - static Vehicle *CountShipProc(Vehicle *v, void *data) + static Vehicle* CountShipProc(Vehicle* v, void* data) { - uint *count = (uint *)data; + uint* count = (uint*)data; /* Ignore other vehicles (aircraft) and ships inside depot. */ if (v->type == VEH_SHIP && (v->vehstatus & VS_HIDDEN) == 0) (*count)++; @@ -289,7 +376,7 @@ class CYapfCostShipT * Calculates only the cost of given node, adds it to the parent node cost * and stores the result into Node::m_cost member */ - inline bool PfCalcCost(Node &n, const TrackFollower *tf) + inline bool PfCalcCost(Node& n, const TrackFollower* tf) { /* base tile cost depending on distance */ int c = IsDiagonalTrackdir(n.GetTrackdir()) ? YAPF_TILE_LENGTH : YAPF_TILE_CORNER_LENGTH; @@ -307,7 +394,7 @@ class CYapfCostShipT c += YAPF_TILE_LENGTH * tf->m_tiles_skipped; /* Ocean/canal speed penalty. */ - const ShipVehicleInfo *svi = ShipVehInfo(Yapf().GetVehicle()->engine_type); + const ShipVehicleInfo* svi = ShipVehInfo(Yapf().GetVehicle()->engine_type); byte 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->m_tiles_skipped) * speed_frac / (256 - speed_frac); @@ -344,15 +431,15 @@ struct CYapfShip_TypesT }; /* YAPF type 1 - uses TileIndex/Trackdir as Node key */ -struct CYapfShip1 : CYapfT > {}; +struct CYapfShip1 : CYapfT > {}; /* YAPF type 2 - uses TileIndex/DiagDirection as Node key */ -struct CYapfShip2 : CYapfT > {}; +struct CYapfShip2 : CYapfT > {}; /** Ship controller helper - path finder invoker */ -Track YapfShipChooseTrack(const Ship *v, TileIndex tile, DiagDirection enterdir, TrackBits tracks, bool &path_found, ShipPathCache &path_cache) +Track YapfShipChooseTrack(const Ship* v, TileIndex tile, DiagDirection enterdir, TrackBits tracks, bool& path_found, ShipPathCache& path_cache) { /* default is YAPF type 2 */ - typedef Trackdir (*PfnChooseShipTrack)(const Ship*, TileIndex, DiagDirection, TrackBits, bool &path_found, ShipPathCache &path_cache); + typedef Trackdir(*PfnChooseShipTrack)(const Ship*, TileIndex, DiagDirection, TrackBits, bool& path_found, ShipPathCache& path_cache); PfnChooseShipTrack pfnChooseShipTrack = CYapfShip2::ChooseShipTrack; // default: ExitDir /* check if non-default YAPF type needed */ @@ -364,7 +451,7 @@ Track YapfShipChooseTrack(const Ship *v, TileIndex tile, DiagDirection enterdir, return (td_ret != INVALID_TRACKDIR) ? TrackdirToTrack(td_ret) : INVALID_TRACK; } -bool YapfShipCheckReverse(const Ship *v, Trackdir *trackdir) +bool YapfShipCheckReverse(const Ship* v, Trackdir* trackdir) { Trackdir td = v->GetVehicleTrackdir(); Trackdir td_rev = ReverseTrackdir(td); diff --git a/src/pathfinder/yapf/yapf_ship_regions.cpp b/src/pathfinder/yapf/yapf_ship_regions.cpp new file mode 100644 index 0000000000000..8556399aa5880 --- /dev/null +++ b/src/pathfinder/yapf/yapf_ship_regions.cpp @@ -0,0 +1,360 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file yapf_ship.cpp Implementation of YAPF for water regions, which are used for finding intermediate ship destinations. */ + +#include "../../stdafx.h" +#include "../../ship.h" + +#include "yapf.hpp" +#include "yapf_ship_regions.h" +#include "../water_regions.h" + +#include "../../safeguards.h" + +constexpr int DIRECT_NEIGHBOR_COST = 100; +constexpr int INDIRECT_NEIGHBOR_COST = 140; + +/** Yapf Node Key that evaluates hash from region pointer. */ +struct CYapfNodeKeyRegion { + + int m_x; + int m_y; + TWaterRegionLabel m_label; + + inline void Set(int x, int y, TWaterRegionLabel label) + { + m_x = x; + m_y = y; + m_label = label; + } + + inline int CalcHash() const { return m_x | m_y << MAX_MAP_SIZE_BITS | m_label << MAX_MAP_SIZE_BITS * 2; }; + inline bool operator == (const CYapfNodeKeyRegion& other) const { return CalcHash() == other.CalcHash(); } +}; + +uint ManhattanDistance(const CYapfNodeKeyRegion& a, const CYapfNodeKeyRegion& b) +{ + return (std::abs(a.m_x - b.m_x) + std::abs(a.m_y - b.m_y)) * DIRECT_NEIGHBOR_COST; +} + +/** Yapf Node for regions */ +/** Yapf Node base */ +template +struct CYapfRegionNodeT { + typedef Tkey_ Key; + typedef CYapfRegionNodeT Node; + + Tkey_ m_key; + Node *m_hash_next; + Node *m_parent; + int m_cost; + int m_estimate; + + inline void Set(Node* parent, int x, int y, int label) + { + m_key.Set(x, y, label); + m_hash_next = nullptr; + m_parent = parent; + m_cost = 0; + m_estimate = 0; + } + + inline void Set(Node* parent, const Key& key) + { + Set(parent, key.m_x, key.m_y, key.m_label); + } + + /* Returns the parent of the parent node if it can be accessed by "cutting the corner", otherwise the parent is return */ + Node* GetClosestParentOrGrandparent(bool& grandparent_is_best) const + { + grandparent_is_best = false; + Node* grandparent = m_parent ? m_parent->m_parent : nullptr; + if (m_parent && grandparent) { + if (std::abs(m_key.m_x - grandparent->m_key.m_x) == 1 && std::abs(m_key.m_y - grandparent->m_key.m_y) == 1) { + grandparent_is_best = true; + return grandparent; + } + } + return m_parent; + } + + inline Node* GetHashNext() { return m_hash_next; } + inline void SetHashNext(Node* pNext) { m_hash_next = pNext; } + inline const Tkey_& GetKey() const { return m_key; } + inline int GetCost() { return m_cost; } + inline int GetCostEstimate() { return m_estimate; } + inline bool operator < (const Node& other) const { return m_estimate < other.m_estimate; } +}; + +/** YAPF origin for regions */ +template +class CYapfOriginRegionT +{ +public: + typedef typename Types::Tpf Tpf; ///< the pathfinder class (derived from THIS class) + typedef typename Types::NodeList::Titem Node; ///< this will be our node type + typedef typename Node::Key Key; ///< key to hash tables + +protected: + /// to access inherited path finder + inline Tpf& Yapf() { return *static_cast(this); } + +private: + std::vector m_origin_keys; // TODO maybe use a map? + +public: + void AddOrigin(int x, int y, TWaterRegionLabel label) + { + if (!HasOrigin(x, y, label)) m_origin_keys.push_back(CYapfNodeKeyRegion{ x, y, label }); + } + + bool HasOrigin(int x, int y, TWaterRegionLabel label) + { + return std::find(m_origin_keys.begin(), m_origin_keys.end(), CYapfNodeKeyRegion{ x, y, label }) != m_origin_keys.end(); + } + + /// Called when YAPF needs to place origin nodes into open list + void PfSetStartupNodes() + { + for (const CYapfNodeKeyRegion& origin_key : m_origin_keys) + { + Node& node = Yapf().CreateNewNode(); + node.Set(nullptr, origin_key); + Yapf().AddStartupNode(node); + } + } +}; + +/** YAPF destination provider for regions*/ +template +class CYapfDestinationRegionT +{ +public: + typedef typename Types::Tpf Tpf; ///< the pathfinder class (derived from THIS class) + typedef typename Types::NodeList::Titem Node; ///< this will be our node type + typedef typename Node::Key Key; ///< key to hash tables + +protected: + Key m_dest; + +public: + void SetDestination(int x, int y, int label) + { + m_dest.Set(x, y, label); + } + +protected: + /// to access inherited path finder + Tpf& Yapf() { return *static_cast(this); } + +public: + /// Called by YAPF to detect if node ends in the desired destination + inline bool PfDetectDestination(Node& n) const + { + return n.m_key == m_dest; + + } + + /** 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::m_estimate */ + inline bool PfCalcEstimate(Node& n) + { + if (PfDetectDestination(n)) { + n.m_estimate = n.m_cost; + return true; + } + + // OCTILE DISTANCE + //const int dx = std::abs(n.m_key.m_x - m_dest.m_x); + //const int dy = std::abs(n.m_key.m_y - m_dest.m_y); + //const int heuristic = DIRECT_NEIGHBOR_COST * (dx + dy) - (2 * DIRECT_NEIGHBOR_COST - INDIRECT_NEIGHBOR_COST) * std::min(dx, dy); + //n.m_estimate = n.m_cost + heuristic; + + n.m_estimate = n.m_cost + ManhattanDistance(n.m_key, m_dest); + + return true; + } +}; + +/** Node Follower module of YAPF for region pathfinding */ +template +class CYapfFollowRegionT +{ +public: + typedef typename Types::Tpf Tpf; ///< the pathfinder class (derived from THIS class) + typedef typename Types::TrackFollower TrackFollower; + typedef typename Types::NodeList::Titem Node; ///< this will be our node type + typedef typename Node::Key Key; ///< key to hash tables + +protected: + /// to access inherited path finder + inline Tpf& Yapf() { return *static_cast(this); } + +public: + /** Called by YAPF to move from the given node to the next tile. For each + * reachable region on the new region creates new node, initializes it + * and adds it to the open list by calling Yapf().AddNewNode(n) */ + inline void PfFollowNode(Node& old_node) + { + TAddRegionCallBack visitFunc = [&](int region_x, int region_y, TWaterRegionLabel label) + { + Node& node = Yapf().CreateNewNode(); + node.Set(&old_node, region_x, region_y, label); + Yapf().AddNewNode(node, TrackFollower{}); + + //MarkSubSquare(node.m_key.m_x, node.m_key.m_y, node.m_key.m_label, 2); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + }; + VisitWaterRegionNeighbors(old_node.m_key.m_x, old_node.m_key.m_y, old_node.m_key.m_label, visitFunc); + } + + /// return debug report character to identify the transportation type + inline char TransportTypeChar() const { return '^'; } + + static HighLevelPathResult FindHighLevelPath(const Ship* v, TileIndex start_tile) + { + const int start_region_x = GetWaterRegionX(start_tile); + const int start_region_y = GetWaterRegionY(start_tile); + const TWaterRegionLabel start_region_label = GetWaterRegionTileLabel(start_tile); + + Tpf pf; + pf.SetDestination(start_region_x, start_region_y, start_region_label); + + if (v->current_order.IsType(OT_GOTO_STATION)) { + DestinationID station_id = v->current_order.GetDestination(); + const BaseStation* station = BaseStation::Get(station_id); + TileArea tile_area; + station->GetTileArea(&tile_area, STATION_DOCK); + for (const auto& tile : tile_area) { + if (IsDockingTile(tile) && IsShipDestinationTile(tile, station_id)) { + pf.AddOrigin(GetWaterRegionX(tile), GetWaterRegionY(tile), GetWaterRegionTileLabel(tile)); + } + } + } + else { + // TODO investigate why the current tile must be added, and not the src_tile that's uses for regular pathfinding + TileIndex tile = v->dest_tile; + pf.AddOrigin(GetWaterRegionX(tile), GetWaterRegionY(tile), GetWaterRegionTileLabel(tile)); + } + + /* If origin and destination are the same we simply return that region, but we mark it as "not an intermediate destination" */ + // TODO why do we even return anything at all? + if (pf.HasOrigin(start_region_x, start_region_y, start_region_label)) { + std::vector path = { WaterRegionDescriptor{start_region_x, start_region_y, start_region_label} }; + return HighLevelPathResult{ true, false /* not intermediate */, std::move(path)}; + } + + /* find best path */ + if (!pf.FindPath(v)) return { false }; // Path not found + + std::vector path; + + WaterRegionDescriptor destination_region{ 0,0,0 }; + + Node* node = pf.GetBestNode(); + path.push_back(WaterRegionDescriptor{ node->m_key.m_x, node->m_key.m_y, node->m_key.m_label }); // TODO explain that the start node must be part of the path + + bool is_intermediate_destination = true; + for (int i = 0; i < NUMBER_OR_WATER_REGIONS_LOOKAHEAD; ++i) { + if (node) { + if (node && !node->m_parent) is_intermediate_destination = false; + node = node->m_parent; + if (node) path.push_back(WaterRegionDescriptor{ node->m_key.m_x, node->m_key.m_y, node->m_key.m_label }); + } + } + + // Draw the rest of the path black + if (is_intermediate_destination) { + while (node) + { + node = node->m_parent; + if (node) MarkSubSquare(node->m_key.m_x, node->m_key.m_y, node->m_key.m_label, 3, true); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + } + } + + assert(!path.empty()); + for (const auto& region : path) { + MarkSubSquare(region.x, region.y, region.label, 1, true); + } + MarkSubSquare(path.back().x, path.back().y, path.back().label, 5, true); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + return HighLevelPathResult{ true, is_intermediate_destination, std::move(path)}; + } +}; +/** Cost Provider module of YAPF for regions */ +template +class CYapfCostRegionT +{ +public: + typedef typename Types::Tpf Tpf; ///< the pathfinder class (derived from THIS class) + typedef typename Types::TrackFollower TrackFollower; + typedef typename Types::NodeList::Titem Node; ///< this will be our node type + typedef typename Node::Key Key; ///< key to hash tables + +protected: + /// to access inherited path finder + Tpf& Yapf() { return *static_cast(this); } + +public: + /** Called by YAPF to calculate the cost from the origin to the given node. + * Calculates only the cost of given node, adds it to the parent node cost + * and stores the result into Node::m_cost member */ + inline bool PfCalcCost(Node& n, const TrackFollower* tf) + { + n.m_cost = n.m_parent->m_cost + ManhattanDistance(n.m_key, n.m_parent->m_key); + + /* Incentivise zigzagging by adding a slight penalty when the search continues in the same direction. */ + Node* grandparent = n.m_parent->m_parent; + if (grandparent) { + const int dx_parent = n.m_key.m_x - n.m_parent->m_key.m_x; + const int dy_parent = n.m_key.m_y - n.m_parent->m_key.m_y; + const int dx_grandparent = n.m_parent->m_key.m_x - n.m_parent->m_parent->m_key.m_x; + const int dy_grandparent = n.m_parent->m_key.m_y - n.m_parent->m_parent->m_key.m_y; + if (dx_parent == dx_grandparent || dy_parent == dy_grandparent) n.m_cost += 1; + } + + return true; + } +}; + +//Region does not need follower +struct NullFollower : public CFollowTrackWater +{ +}; + +/** Config struct of YAPF for route planning. + * Defines all 6 base YAPF modules as classes providing services for CYapfBaseT. + */ +template +struct CYapfRegion_TypesT +{ + /** Types - shortcut for this struct type */ + typedef CYapfRegion_TypesT Types; + + /** Tpf - pathfinder type */ + typedef Tpf_ Tpf; + /** track follower helper class */ + typedef NullFollower TrackFollower; + /** node list type */ + typedef Tnode_list NodeList; + typedef Ship VehicleType; + /** pathfinder components (modules) */ + typedef CYapfBaseT PfBase; // base pathfinder class + typedef CYapfFollowRegionT PfFollow; // node follower + typedef CYapfOriginRegionT PfOrigin; // origin provider + typedef CYapfDestinationRegionT PfDestination; // destination/distance provider + typedef CYapfSegmentCostCacheNoneT PfCache; // segment cost cache provider + typedef CYapfCostRegionT PfCost; // cost provider +}; + +typedef CNodeList_HashTableT, 12, 12> CRegionNodeListWater; + +struct CYapfRegionWater : CYapfT > {}; + +HighLevelPathResult YapfShipFindDestinationRegion(const Ship* v, TileIndex start_tile) +{ + return CYapfRegionWater::FindHighLevelPath(v, start_tile); +} diff --git a/src/pathfinder/yapf/yapf_ship_regions.h b/src/pathfinder/yapf/yapf_ship_regions.h new file mode 100644 index 0000000000000..12bb55bfcd7d6 --- /dev/null +++ b/src/pathfinder/yapf/yapf_ship_regions.h @@ -0,0 +1,45 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + + /** @file yapf_ship.cpp Implementation of YAPF for water regions, which are used for finding intermediate ship destinations. */ + +#ifndef YAPF_SHIP_REGIONS_H +#define YAPF_SHIP_REGIONS_H + +#include "../../stdafx.h" +#include "../../tile_type.h" +#include "../water_regions.h" + +#include + +constexpr int NUMBER_OR_WATER_REGIONS_LOOKAHEAD = 4; + +struct WaterRegionDescriptor +{ + int x; + int y; + TWaterRegionLabel label; + + bool operator==(const WaterRegionDescriptor& other) const { return x == other.x && y == other.y && label == other.label; } + bool operator!=(const WaterRegionDescriptor& other) const { return !(*this == other); } +}; + +struct HighLevelPathResult +{ + const bool path_found = false; + const bool intermediate_destination = false; + const std::vector path; +}; + +inline WaterRegionDescriptor GetWaterRegionDescriptor(TileIndex tile) +{ + return WaterRegionDescriptor{ GetWaterRegionX(tile), GetWaterRegionY(tile), GetWaterRegionTileLabel(tile) }; +} + +HighLevelPathResult YapfShipFindDestinationRegion(const Ship* v, TileIndex start_tile); + +#endif /* YAPF_SHIP_REGIONS_H */ diff --git a/src/saveload/CMakeLists.txt b/src/saveload/CMakeLists.txt index 3eea5b67419a8..ff75d659d468c 100644 --- a/src/saveload/CMakeLists.txt +++ b/src/saveload/CMakeLists.txt @@ -44,4 +44,5 @@ add_files( town_sl.cpp vehicle_sl.cpp waypoint_sl.cpp + water_regions_sl.cpp ) diff --git a/src/saveload/afterload.cpp b/src/saveload/afterload.cpp index 6ebf4ccff1fd5..41b1323b30e48 100644 --- a/src/saveload/afterload.cpp +++ b/src/saveload/afterload.cpp @@ -58,6 +58,7 @@ #include "../disaster_vehicle.h" #include "../ship.h" #include "../water.h" +#include "../pathfinder/water_regions.h" // TODO KB for ship pathfinder reset #include "saveload_internal.h" @@ -3239,6 +3240,11 @@ bool AfterLoadGame() } } + if (IsSavegameVersionBefore(SLV_WATER_REGIONS)) { + Debug(net, 0, "OLD SAVEGAME WITHOUT WATER REGIONS - initializing..."); + InitializeWaterRegions(); + } + return true; } diff --git a/src/saveload/saveload.cpp b/src/saveload/saveload.cpp index 604c6292e108b..576214aee5852 100644 --- a/src/saveload/saveload.cpp +++ b/src/saveload/saveload.cpp @@ -252,6 +252,7 @@ static const std::vector &ChunkHandlers() extern const ChunkHandlerTable _airport_chunk_handlers; extern const ChunkHandlerTable _object_chunk_handlers; extern const ChunkHandlerTable _persistent_storage_chunk_handlers; + extern const ChunkHandlerTable _water_region_chunk_handlers; /** List of all chunks in a savegame. */ static const ChunkHandlerTable _chunk_handler_tables[] = { @@ -289,6 +290,7 @@ static const std::vector &ChunkHandlers() _airport_chunk_handlers, _object_chunk_handlers, _persistent_storage_chunk_handlers, + _water_region_chunk_handlers, }; static std::vector _chunk_handlers; diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 61d4047b152dc..ae73feef4dc29 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -345,6 +345,7 @@ enum SaveLoadVersion : uint16 { SLV_MULTITRACK_LEVEL_CROSSINGS, ///< 302 PR#9931 v13.0 Multi-track level crossings. SLV_NEWGRF_ROAD_STOPS, ///< 303 PR#10144 NewGRF road stops. SLV_LINKGRAPH_EDGES, ///< 304 PR#10314 Explicitly store link graph edges destination, PR#10471 int64 instead of uint64 league rating + SLV_WATER_REGIONS, ///< 305 PR#10543 Water Regions for ship pathfinder. SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/saveload/water_regions_sl.cpp b/src/saveload/water_regions_sl.cpp new file mode 100644 index 0000000000000..b50198368ee60 --- /dev/null +++ b/src/saveload/water_regions_sl.cpp @@ -0,0 +1,57 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file water_regions_sl.cpp Code handling saving and loading of water regions */ + +#include "../stdafx.h" + +#include "saveload.h" +#include "pathfinder/water_regions.h" + +#include "../safeguards.h" + +static const SaveLoad _water_region_desc[] = { + SLE_VAR(WaterRegionSaveLoadInfo, initialized, SLE_BOOL), +}; + +// TODO is an empty Compat table ok? +SaveLoadCompatTable _water_region_sl_compat{}; + +struct WRGNChunkHandler : ChunkHandler { + WRGNChunkHandler() : ChunkHandler('WRGN', CH_TABLE) {} + + void Save() const override + { + SlTableHeader(_water_region_desc); + + int index = 0; + for (WaterRegionSaveLoadInfo& region : GetWaterRegionSaveLoadInfo()) { + SlSetArrayIndex(index++); + SlObject(®ion, _water_region_desc); + } + } + + void Load() const override + { + const std::vector slt = SlCompatTableHeader(_water_region_desc, _water_region_sl_compat); + + int index; + + std::vector loaded_info; + while ((index = SlIterateArray()) != -1) { + WaterRegionSaveLoadInfo region_info; + SlObject(®ion_info, slt); + loaded_info.push_back(std::move(region_info)); + } + + LoadWaterRegions(loaded_info); + } +}; + +static const WRGNChunkHandler WRGN; +static const ChunkHandlerRef water_region_chunk_handlers[] = { WRGN }; +extern const ChunkHandlerTable _water_region_chunk_handlers(water_region_chunk_handlers); diff --git a/src/ship_cmd.cpp b/src/ship_cmd.cpp index 3dba2259c1fe0..1008db230b31e 100644 --- a/src/ship_cmd.cpp +++ b/src/ship_cmd.cpp @@ -35,6 +35,7 @@ #include "industry.h" #include "industry_map.h" #include "ship_cmd.h" +#include "viewport_func.h" #include "table/strings.h" @@ -832,7 +833,11 @@ bool Ship::Tick() if (!(this->vehstatus & VS_STOPPED)) this->running_ticks++; + + ShipController(this); + DEBUG_addDrawRectangleInstruction(this->tile, 0, true); // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent + DEBUG_addDrawInstruction(this->tile, TrackdirToTrack(this->GetVehicleTrackdir()), 0, true);// 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent return true; } diff --git a/src/track_func.h b/src/track_func.h index 646745064600d..ab70a8cdfd97c 100644 --- a/src/track_func.h +++ b/src/track_func.h @@ -16,6 +16,7 @@ #include "slope_func.h" using SetTrackBitIterator = SetBitIterator; +using SetTrackdirBitIterator = SetBitIterator; /** * Checks if a Track is valid. diff --git a/src/track_type.h b/src/track_type.h index 70278c58d950d..cdab70ff2d562 100644 --- a/src/track_type.h +++ b/src/track_type.h @@ -89,6 +89,8 @@ enum Trackdir : byte { INVALID_TRACKDIR = 0xFF, ///< Flag for an invalid trackdir }; +/** Allow incrementing of Trackdir variables */ +DECLARE_POSTFIX_INCREMENT(Trackdir) /** Define basic enum properties */ template <> struct EnumPropsT : MakeEnumPropsT {}; diff --git a/src/tunnelbridge_cmd.cpp b/src/tunnelbridge_cmd.cpp index e9f4fb7169d8c..3a6af9f03e569 100644 --- a/src/tunnelbridge_cmd.cpp +++ b/src/tunnelbridge_cmd.cpp @@ -20,6 +20,7 @@ #include "ship.h" #include "roadveh.h" #include "pathfinder/yapf/yapf_cache.h" +#include "pathfinder/water_regions.h" #include "newgrf_sound.h" #include "autoslope.h" #include "tunnelbridge_map.h" @@ -561,6 +562,8 @@ CommandCost CmdBuildBridge(DoCommandFlag flags, TileIndex tile_end, TileIndex ti MakeAqueductBridgeRamp(tile_end, owner, ReverseDiagDir(dir)); CheckForDockingTile(tile_start); CheckForDockingTile(tile_end); + InvalidateWaterRegion(tile_start); + InvalidateWaterRegion(tile_end); break; default: diff --git a/src/viewport.cpp b/src/viewport.cpp index dd5e469085537..b69832f3a0d38 100644 --- a/src/viewport.cpp +++ b/src/viewport.cpp @@ -64,6 +64,8 @@ #include "core/backup_type.hpp" #include "landscape.h" #include "viewport_func.h" +#include "map_func.h" +#include "water.h" #include "station_base.h" #include "waypoint_base.h" #include "town.h" @@ -99,9 +101,26 @@ #include "table/string_colours.h" #include "safeguards.h" +#include +#include Point _tile_fract_coords; +struct DEBUG_DrawInstruction +{ + TileIndex tile; + TrackBits tracks; + std::unordered_map color_hints; +}; + +struct DEBUG_DrawRectangleInstruction +{ + TileIndex tile; + int color_hint; +}; + +std::unordered_map _DEBUG_drawInstructions; +std::unordered_map _DEBUG_drawRectangleInstructions; ViewportSignKdtree _viewport_sign_kdtree(&Kdtree_ViewportSignXYFunc); static int _viewport_sign_maxwidth = 0; @@ -958,7 +977,7 @@ static const HighLightStyle _autorail_type[6][2] = { * @param *ti TileInfo Tile that is being drawn * @param autorail_type Offset into _AutorailTilehSprite[][] */ -static void DrawAutorailSelection(const TileInfo *ti, uint autorail_type) +static void DrawAutorailSelection(const TileInfo *ti, uint autorail_type, PaletteID override_palette = 666666) { SpriteID image; PaletteID pal; @@ -985,6 +1004,8 @@ static void DrawAutorailSelection(const TileInfo *ti, uint autorail_type) pal = PALETTE_SEL_TILE_RED; } + if (override_palette != 666666) pal = override_palette; + DrawSelectionSprite(image, _thd.make_square_red ? PALETTE_SEL_TILE_RED : pal, ti, 7, foundation_part); } @@ -1081,6 +1102,7 @@ static void HighlightTownLocalAuthorityTiles(const TileInfo *ti) } + /** * Checks if the specified tile is selected and if so draws selection using correct selectionstyle. * @param *ti TileInfo Tile that is being drawn @@ -1175,8 +1197,18 @@ static int GetViewportY(Point tile) /** * Add the landscape to the viewport, i.e. all ground tiles and buildings. */ +//std::unordered_map _debugDrawStuff; static void ViewportAddLandscape() { + + + static int counter = 0; + counter++; + + + + + assert(_vd.dpi.top <= _vd.dpi.top + _vd.dpi.height); assert(_vd.dpi.left <= _vd.dpi.left + _vd.dpi.width); @@ -1266,10 +1298,11 @@ static void ViewportAddLandscape() /* Would a higher bridge on a more southern tile be visible? * If yes, we need to loop over more rows to possibly find one. */ if (min_visible_height < potential_bridge_height + MAX_TILE_EXTENT_TOP) last_row = false; - } else { - /* Outside of map. If we are on the north border of the map, there may still be a bridge visible, - * so we need to loop over more rows to possibly find one. */ - if ((tilecoord.x <= 0 || tilecoord.y <= 0) && min_visible_height < potential_bridge_height + MAX_TILE_EXTENT_TOP) last_row = false; + } + else { + /* Outside of map. If we are on the north border of the map, there may still be a bridge visible, + * so we need to loop over more rows to possibly find one. */ + if ((tilecoord.x <= 0 || tilecoord.y <= 0) && min_visible_height < potential_bridge_height + MAX_TILE_EXTENT_TOP) last_row = false; } if (tile_visible) { @@ -1281,10 +1314,52 @@ static void ViewportAddLandscape() _vd.last_foundation_child[1] = nullptr; _tile_type_procs[tile_type]->draw_tile_proc(&_cur_ti); - if (_cur_ti.tile != INVALID_TILE) DrawTileSelection(&_cur_ti); + + bool anyCorner = false; + if (_cur_ti.tile != INVALID_TILE) { + DrawTileSelection(&_cur_ti); + + + const std::unordered_map trackbit_highlightstyle_map = { + {TRACK_BIT_X, HT_DIR_X}, + {TRACK_BIT_Y, HT_DIR_Y}, + {TRACK_BIT_UPPER, HT_DIR_HU}, + {TRACK_BIT_LOWER, HT_DIR_HL}, + {TRACK_BIT_RIGHT, HT_DIR_VR}, + {TRACK_BIT_LEFT, HT_DIR_VL} }; + + const auto it = _DEBUG_drawInstructions.find(_cur_ti.tile); + const auto it_rect = _DEBUG_drawRectangleInstructions.find(_cur_ti.tile); + + if (it != _DEBUG_drawInstructions.end()) + { + for (Track t : SetTrackBitIterator(it->second.tracks)) + { + PaletteID pal = PAL_NONE; + if (it->second.color_hints[t] == 1) pal = PALETTE_SEL_TILE_RED; + if (it->second.color_hints[t] == 2) pal = PALETTE_SEL_TILE_BLUE; + if (it->second.color_hints[t] == 3) pal = PALETTE_ALL_BLACK; + if (it->second.color_hints[t] == 4) pal = PALETTE_TO_TRANSPARENT; + if (it->second.color_hints[t] == 5) pal = PALETTE_TILE_RED_PULSATING; + DrawAutorailSelection(&_cur_ti, _autorail_type[trackbit_highlightstyle_map.at(TrackToTrackBits(t))][0], pal); + } + } + if (it_rect != _DEBUG_drawRectangleInstructions.end()) + { + PaletteID pal = PAL_NONE; + if (it_rect->second.color_hint == 1) pal = PALETTE_SEL_TILE_RED; + if (it_rect->second.color_hint == 2) pal = PALETTE_SEL_TILE_BLUE; + if (it_rect->second.color_hint == 3) pal = PALETTE_ALL_BLACK; + if (it_rect->second.color_hint == 4) pal = PALETTE_TO_TRANSPARENT; + if (it_rect->second.color_hint == 5) pal = PALETTE_TILE_RED_PULSATING; + DrawTileSelectionRect(&_cur_ti, pal); + } + } } } } + + } /** @@ -3561,3 +3636,34 @@ void SetViewportCatchmentTown(const Town *t, bool sel) } if (_viewport_highlight_town != nullptr) SetWindowDirty(WC_TOWN_VIEW, _viewport_highlight_town->index); } + + +void DEBUG_addDrawInstruction(TileIndex tile, Track track, int color_hint, bool override) // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +{ +#ifndef NDEBUG + if (HasBit(_DEBUG_drawInstructions[tile].tracks, track) && !override) return; + + _DEBUG_drawInstructions.at(tile).tracks |= TrackToTrackBits(track); + _DEBUG_drawInstructions.at(tile).color_hints[track] = color_hint; + MarkTileDirtyByTile(tile); +#endif +} + +void DEBUG_addDrawRectangleInstruction(TileIndex tile, int color_hint, bool override) // 0 = normal, 1 = red, 2 = blue, 3 = black, 4 = transparent +{ +#ifndef NDEBUG + if (_DEBUG_drawRectangleInstructions.count(tile) > 0 && !override) return; + + _DEBUG_drawRectangleInstructions[tile] = {tile, color_hint}; + MarkTileDirtyByTile(tile); +#endif +} + +void DEBUG_clearDrawInstructions() +{ +#ifndef NDEBUG + _DEBUG_drawInstructions.clear(); + _DEBUG_drawRectangleInstructions.clear(); + MarkWholeScreenDirty(); +#endif +} diff --git a/src/viewport_func.h b/src/viewport_func.h index 20bf5837c18fb..5f2cc9653285f 100644 --- a/src/viewport_func.h +++ b/src/viewport_func.h @@ -15,6 +15,7 @@ #include "window_type.h" #include "tile_map.h" #include "station_type.h" +#include "track_type.h" static const int TILE_HEIGHT_STEP = 50; ///< One Z unit tile height difference is displayed as 50m. @@ -101,4 +102,8 @@ void SetViewportCatchmentStation(const Station *st, bool sel); void SetViewportCatchmentTown(const Town *t, bool sel); void MarkCatchmentTilesDirty(); +void DEBUG_addDrawInstruction(TileIndex tile, Track track, int color_hint, bool override = false); +void DEBUG_addDrawRectangleInstruction(TileIndex tile, int color_hint, bool override = false); +void DEBUG_clearDrawInstructions(); + #endif /* VIEWPORT_FUNC_H */ diff --git a/src/water_cmd.cpp b/src/water_cmd.cpp index 759afbdc4f1b8..848556aebd60b 100644 --- a/src/water_cmd.cpp +++ b/src/water_cmd.cpp @@ -39,6 +39,7 @@ #include "industry.h" #include "water_cmd.h" #include "landscape_cmd.h" +#include "pathfinder/water_regions.h" #include "table/strings.h" @@ -133,6 +134,9 @@ CommandCost CmdBuildShipDepot(DoCommandFlag flags, TileIndex tile, Axis axis) } if (flags & DC_EXEC) { + InvalidateWaterRegion(tile); + InvalidateWaterRegion(tile2); + Depot *depot = new Depot(tile); depot->build_date = _date; @@ -243,6 +247,7 @@ void MakeWaterKeepingClass(TileIndex tile, Owner o) /* Zero map array and terminate animation */ DoClearSquare(tile); + InvalidateWaterRegion(tile); /* Maybe change to water */ switch (wc) { @@ -340,6 +345,10 @@ static CommandCost DoBuildLock(TileIndex tile, DiagDirection dir, DoCommandFlag } if (flags & DC_EXEC) { + InvalidateWaterRegion(tile); + InvalidateWaterRegion(tile + delta); + InvalidateWaterRegion(tile - delta); + /* Update company infrastructure counts. */ Company *c = Company::GetIfValid(_current_company); if (c != nullptr) { @@ -481,6 +490,8 @@ CommandCost CmdBuildCanal(DoCommandFlag flags, TileIndex tile, TileIndex start_t if (!water) cost.AddCost(ret); if (flags & DC_EXEC) { + InvalidateWaterRegion(current_tile); + switch (wc) { case WATER_CLASS_RIVER: MakeRiver(current_tile, Random()); @@ -523,8 +534,11 @@ CommandCost CmdBuildCanal(DoCommandFlag flags, TileIndex tile, TileIndex start_t } } + static CommandCost ClearTile_Water(TileIndex tile, DoCommandFlag flags) { + if (flags & DC_EXEC) InvalidateWaterRegion(tile); + switch (GetWaterTileType(tile)) { case WATER_TILE_CLEAR: { if (flags & DC_NO_WATER) return_cmd_error(STR_ERROR_CAN_T_BUILD_ON_WATER); @@ -748,7 +762,10 @@ static void DrawWaterEdges(bool canal, uint offset, TileIndex tile) /** Draw a plain sea water tile with no edges */ static void DrawSeaWater(TileIndex tile) { - DrawGroundSprite(SPR_FLAT_WATER_TILE, PAL_NONE); + // TODO debug code, remove again + PaletteID pal = PAL_NONE; + if (DEBUG_IsWaterRegionSimple(tile)) pal = PALETTE_TO_TRANSPARENT; + DrawGroundSprite(SPR_FLAT_WATER_TILE, pal); } /** draw a canal styled water tile with dikes around */ @@ -925,6 +942,10 @@ static void DrawTile_Water(TileInfo *ti) DrawWaterDepot(ti); break; } + + // debug drawing + TileIndex tile = ti->tile; + if (TileX(tile) % WATER_REGION_SIZE == 0 || TileY(tile) % WATER_REGION_SIZE == 0) DrawGroundSprite(SPR_DOT, PAL_NONE); } void DrawShipDepotSprite(int x, int y, Axis axis, DepotPart part) @@ -1155,6 +1176,8 @@ void DoFloodTile(TileIndex target) } if (flooded) { + InvalidateWaterRegion(target); + /* Mark surrounding canal tiles dirty too to avoid glitches */ MarkCanalsAndRiversAroundDirty(target); diff --git a/src/waypoint_cmd.cpp b/src/waypoint_cmd.cpp index b04a570d2b4d7..041ac393e8403 100644 --- a/src/waypoint_cmd.cpp +++ b/src/waypoint_cmd.cpp @@ -15,6 +15,7 @@ #include "town.h" #include "waypoint_base.h" #include "pathfinder/yapf/yapf_cache.h" +#include "pathfinder/water_regions.h" #include "strings_func.h" #include "viewport_func.h" #include "viewport_kdtree.h" @@ -340,6 +341,7 @@ CommandCost CmdBuildBuoy(DoCommandFlag flags, TileIndex tile) if (wp->town == nullptr) MakeDefaultName(wp); MakeBuoy(tile, wp->index, GetWaterClass(tile)); + InvalidateWaterRegion(tile); CheckForDockingTile(tile); MarkTileDirtyByTile(tile);