diff --git a/src/heightmap.cpp b/src/heightmap.cpp index 462d8d15ea..a520704763 100644 --- a/src/heightmap.cpp +++ b/src/heightmap.cpp @@ -9,6 +9,7 @@ #include "stdafx.h" #include "heightmap.h" +#include "landscape.h" #include "clear_map.h" #include "strings_func.h" #include "void_map.h" @@ -523,6 +524,10 @@ bool LoadHeightmap(DetailedFileType dft, std::string_view filename) GrayscaleToMapHeights(x, y, map); FixSlopes(); + + /* If all map borders are water, we will draw infinite water. */ + _settings_game.game_creation.water_borders = (CheckWaterBorders(false) ? BORDERFLAGS_ALL : BorderFlag{}); + MarkWholeScreenDirty(); return true; diff --git a/src/landscape.cpp b/src/landscape.cpp index 5db1c9177a..ac7ecd44bd 100644 --- a/src/landscape.cpp +++ b/src/landscape.cpp @@ -635,6 +635,41 @@ void ClearSnowLine() _snow_line = nullptr; } +/** + * Check if all tiles on the map edge should be considered water borders. + * @param allow_non_flat_void Should we allow non-flat void tiles? (if map edge raised, then flattened to sea level) + * @return true If the edge of the map is flat and height 0, allowing for infinite water borders. + */ +bool CheckWaterBorders(bool allow_non_flat_void) +{ + auto check_tile = [allow_non_flat_void](uint x, uint y, Slope inner_edge) -> bool { + auto [slope, h] = GetTilePixelSlopeOutsideMap(x, y); + + /* The edge tile is flat. */ + if ((slope == SLOPE_FLAT) && (h == 0)) return true; + if (allow_non_flat_void && h == 0 && (slope & inner_edge) == 0 && IsTileType(TileXY(x, y), MP_VOID)) return true; + return false; + }; + + /* Check the map corners. */ + if (!check_tile(0, 0, SLOPE_S)) return false; + if (!check_tile(0, Map::SizeY(), SLOPE_W)) return false; + if (!check_tile(Map::SizeX(), 0, SLOPE_E)) return false; + if (!check_tile(Map::SizeX(), Map::SizeY(), SLOPE_N)) return false; + + /* Check the map edges.*/ + for (uint x = 0; x <= Map::SizeX(); x++) { + if (!check_tile(x, 0, SLOPE_SE)) return false; + if (!check_tile(x, Map::SizeY(), SLOPE_NW)) return false; + } + for (uint y = 1; y < Map::SizeY(); y++) { + if (!check_tile(0, y, SLOPE_SW)) return false; + if (!check_tile(Map::SizeX(), y, SLOPE_NE)) return false; + } + + return true; +} + /** * Clear a piece of landscape * @param flags of operation to conduct diff --git a/src/landscape.h b/src/landscape.h index db2a9781e4..a329d847af 100644 --- a/src/landscape.h +++ b/src/landscape.h @@ -33,6 +33,8 @@ uint8_t HighestSnowLine(); uint8_t LowestSnowLine(); void ClearSnowLine(); +bool CheckWaterBorders(bool allow_non_flat_void); + int GetSlopeZInCorner(Slope tileh, Corner corner); std::tuple GetFoundationSlope(TileIndex tile); diff --git a/src/saveload/afterload.cpp b/src/saveload/afterload.cpp index da1386915f..45eeaf26c9 100644 --- a/src/saveload/afterload.cpp +++ b/src/saveload/afterload.cpp @@ -1024,6 +1024,27 @@ bool AfterLoadGame() _settings_game.construction.freeform_edges = false; } + /* Handle infinite water borders. */ + if (IsSavegameVersionBefore(SLV_INFINITE_WATER_BORDERS)) { + if (CheckWaterBorders(true)) { + /* We passed the water borders check, yay! */ + _settings_game.game_creation.water_borders = BORDERFLAGS_ALL; + + /* Flatten void tiles, which may have been affected by terraforming near the map edge. */ + for (uint x = 0; x <= Map::SizeX(); x++) { + SetTileHeightOutsideMap(x, 0, 0); + SetTileHeightOutsideMap(x, Map::SizeY(), 0); + } + for (uint y = 1; y < Map::SizeY(); y++) { + SetTileHeightOutsideMap(0, y, 0); + SetTileHeightOutsideMap(Map::SizeX(), y, 0); + } + } else { + /* Not all edges are ocean. */ + _settings_game.game_creation.water_borders = BorderFlag{}; + } + } + /* From version 9.0, we update the max passengers of a town (was sometimes negative * before that. */ if (IsSavegameVersionBefore(SLV_9)) { diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index 7ab7c06742..0db5bd6700 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -404,6 +404,7 @@ enum SaveLoadVersion : uint16_t { SLV_ORDERS_OWNED_BY_ORDERLIST, ///< 354 PR#13948 Orders stored in OrderList, pool removed. SLV_FACE_STYLES, ///< 355 PR#14319 Addition of face styles, replacing gender and ethnicity. + SLV_INFINITE_WATER_BORDERS, ///< 356 PR#13289 Draw infinite water when all borders are water. SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/table/settings/world_settings.ini b/src/table/settings/world_settings.ini index a4c97cf854..ba682b59cf 100644 --- a/src/table/settings/world_settings.ini +++ b/src/table/settings/world_settings.ini @@ -264,6 +264,7 @@ cat = SC_BASIC var = game_creation.water_borders type = SLE_UINT8 from = SLV_111 +flags = SettingFlag::NewgameOnly def = 15 min = 0 max = 16 diff --git a/src/terraform_cmd.cpp b/src/terraform_cmd.cpp index 2c1c637b41..428a23b196 100644 --- a/src/terraform_cmd.cpp +++ b/src/terraform_cmd.cpp @@ -111,16 +111,25 @@ static std::tuple TerraformTileHeight(TerraformerState * */ if (height == TerraformGetHeightOfTile(ts, tile)) return { CMD_ERROR, INVALID_TILE }; - /* Check "too close to edge of map". Only possible when freeform-edges is off. */ - uint x = TileX(tile); - uint y = TileY(tile); - if (!_settings_game.construction.freeform_edges && ((x <= 1) || (y <= 1) || (x >= Map::MaxX() - 1) || (y >= Map::MaxY() - 1))) { - /* - * Determine a sensible error tile - */ - if (x == 1) x = 0; - if (y == 1) y = 0; - return { CommandCost(STR_ERROR_TOO_CLOSE_TO_EDGE_OF_MAP), TileXY(x, y) }; + /* If the map has infinite water borders, don't allow terraforming the outer ring of tiles to avoid blocking ships in a confusing way. */ + if (_settings_game.game_creation.water_borders == BORDERFLAGS_ALL) { + uint x = TileX(tile); + uint y = TileY(tile); + + auto check_tile = [&](uint x_min, uint y_min, uint x_max, uint y_max) -> bool { + return ((x <= x_min) || (y <= y_min) || (x >= Map::MaxX() - x_max) || (y >= Map::MaxY() - y_max)); + }; + + /* If free-form edges is off, distances are a bit different. */ + if (!_settings_game.construction.freeform_edges && check_tile(1, 1, 1, 1)) { + /* Determine a sensible error tile. */ + if (x == 1) x = 0; + if (y == 1) y = 0; + return { CommandCost(STR_ERROR_TOO_CLOSE_TO_EDGE_OF_MAP), TileXY(x, y) }; + } + + /* Freeform edges are enabled. */ + if (check_tile(2, 2, 1, 1)) return { CommandCost(STR_ERROR_TOO_CLOSE_TO_EDGE_OF_MAP), TileXY(x, y) }; } /* Mark incident tiles that are involved in the terraforming. */ @@ -209,7 +218,11 @@ std::tuple CmdTerraformLand(DoCommandFlags flags, assert(t < Map::Size()); /* MP_VOID tiles can be terraformed but as tunnels and bridges * cannot go under / over these tiles they don't need checking. */ - if (IsTileType(t, MP_VOID)) continue; + if (IsTileType(t, MP_VOID)) { + /* All water borders are drawn with infinite water, so don't allow terraforming off the map edge. */ + if (_settings_game.game_creation.water_borders == BORDERFLAGS_ALL) return { CommandCost(STR_ERROR_TOO_CLOSE_TO_EDGE_OF_MAP), 0, t }; + continue; + } /* Find new heights of tile corners */ int z_N = TerraformGetHeightOfTile(&ts, t + TileDiffXY(0, 0)); diff --git a/src/tile_map.h b/src/tile_map.h index 5f900e8ddc..bc0bdd34f4 100644 --- a/src/tile_map.h +++ b/src/tile_map.h @@ -61,6 +61,18 @@ inline void SetTileHeight(Tile tile, uint height) tile.height() = height; } +/** + * Sets the height of a tile, also for tiles outside the map (virtual "black" tiles). + * + * @param tile The tile to change the height + * @param height The new height value of the tile + * @pre height <= MAX_TILE_HEIGHT + */ +inline void SetTileHeightOutsideMap(int x, int y, uint height) +{ + SetTileHeight(TileXY(Clamp(x, 0, Map::MaxX()), Clamp(y, 0, Map::MaxY())), height); +} + /** * Returns the height of a tile in pixels. * diff --git a/src/void_cmd.cpp b/src/void_cmd.cpp index 5334824997..cf496747fe 100644 --- a/src/void_cmd.cpp +++ b/src/void_cmd.cpp @@ -21,7 +21,12 @@ static void DrawTile_Void(TileInfo *ti) { - DrawGroundSprite(SPR_FLAT_BARE_LAND + SlopeToSpriteOffset(ti->tileh), PALETTE_ALL_BLACK); + /* If all borders are water, draw infinite water off the edges of the map. */ + if (_settings_game.game_creation.water_borders == BORDERFLAGS_ALL) { + DrawGroundSprite(SPR_FLAT_WATER_TILE + SlopeToSpriteOffset(ti->tileh), PAL_NONE); + } else { + DrawGroundSprite(SPR_FLAT_BARE_LAND + SlopeToSpriteOffset(ti->tileh), PALETTE_ALL_BLACK); + } }