From 509cbedf0bdbf0f9ff60e134d6d9441108b0892f Mon Sep 17 00:00:00 2001 From: Tyler Trahan Date: Tue, 18 Nov 2025 07:38:55 -0500 Subject: [PATCH] Feature: Rivers can end in wetlands if unable to reach sea (#14784) --- src/landscape.cpp | 143 +++++++++++++++++++++++++++++++++----------- src/settings_type.h | 11 ++++ src/tree_cmd.cpp | 17 ++---- src/tree_cmd.h | 2 + 4 files changed, 127 insertions(+), 46 deletions(-) diff --git a/src/landscape.cpp b/src/landscape.cpp index 983c3e4f56..b12059bef4 100644 --- a/src/landscape.cpp +++ b/src/landscape.cpp @@ -29,6 +29,7 @@ #include "animated_tile_func.h" #include "core/random_func.hpp" #include "object_base.h" +#include "tree_cmd.h" #include "company_func.h" #include "company_gui.h" #include "saveload/saveload.h" @@ -1045,24 +1046,117 @@ static bool FindSpring(TileIndex tile) } /** - * Make a connected lake; fill all tiles in the circular tile search that are connected. - * @param tile The tile to consider for lake making. + * Is this a valid tile for the water feature at the end of a river? + * @param tile The tile to check. + * @param height The height of the rest of the water feature, which must match. + * @return True iff this is a valid tile to be part of the river terminus. + */ +static bool IsValidRiverTerminusTile(TileIndex tile, uint height) +{ + if (!IsValidTile(tile) || TileHeight(tile) != height || !IsTileFlat(tile)) return false; + if (_settings_game.game_creation.landscape == LandscapeType::Tropic && GetTropicZone(tile) == TROPICZONE_DESERT) return false; + + return true; +} + +/** + * Make a lake centred on the given tile, of a random diameter. + * @param lake_centre The middle tile of the lake. * @param height_lake The height of the lake. */ -static void MakeLake(TileIndex tile, uint height_lake) +static void MakeLake(TileIndex lake_centre, uint height_lake) { - if (!IsValidTile(tile) || TileHeight(tile) != height_lake || !IsTileFlat(tile)) return; - if (_settings_game.game_creation.landscape == LandscapeType::Tropic && GetTropicZone(tile) == TROPICZONE_DESERT) return; + MakeRiverAndModifyDesertZoneAround(lake_centre); + uint diameter = RandomRange(8) + 3; - for (DiagDirection d = DIAGDIR_BEGIN; d < DIAGDIR_END; d++) { - TileIndex t = tile + TileOffsByDiagDir(d); - if (IsWaterTile(t)) { - MakeRiverAndModifyDesertZoneAround(tile); - return; + /* Run the loop twice, so artefacts from going circular in one direction get (mostly) hidden. */ + for (uint loops = 0; loops < 2; ++loops) { + for (TileIndex tile : SpiralTileSequence(lake_centre, diameter)) { + if (!IsValidRiverTerminusTile(tile, height_lake)) continue; + for (DiagDirection d = DIAGDIR_BEGIN; d < DIAGDIR_END; d++) { + TileIndex t = tile + TileOffsByDiagDir(d); + if (IsWaterTile(t)) { + MakeRiverAndModifyDesertZoneAround(tile); + return; + } + } } } } +/** + * Make wetlands around the given tile. + * @param centre The starting tile. + * @param height The height of the wetlands. + * @param river_length The length of the river. + */ +static void MakeWetlands(TileIndex centre, uint height, uint river_length) +{ + MakeRiverAndModifyDesertZoneAround(centre); + + uint diameter = std::max((river_length), 16u); + + /* Some wetlands have trees planted among the water tiles. */ + bool has_trees = Chance16(1, 2); + + /* Create the main wetland area. */ + for (TileIndex tile : SpiralTileSequence(centre, diameter)) { + if (!IsValidRiverTerminusTile(tile, height)) continue; + + /* Don't make a perfect square, but a circle with a noisy border. */ + uint radius = diameter / 2; + if ((DistanceSquare(tile, centre) > radius * radius) && Chance16(3, 4)) continue; + + if (Chance16(1, 3)) { + /* This tile is water. */ + MakeRiverAndModifyDesertZoneAround(tile); + } else if (IsTileType(tile, MP_CLEAR)) { + /* This tile is ground, which we always make rough. */ + SetClearGroundDensity(tile, CLEAR_ROUGH, 3); + /* Maybe place trees? */ + if (has_trees && _settings_game.game_creation.tree_placer != TP_NONE) { + PlaceTree(tile, Random(), true); + } + } + } +} + +/** + * Try to end a river at a tile which is not the sea. + * @param tile The tile to try ending the river at. + * @param begin The starting tile of the river. + * @return Whether we succesfully ended the river on the given tile. + */ +static bool TryMakeRiverTerminus(TileIndex tile, TileIndex begin) +{ + if (!IsValidTile(tile)) return false; + + /* We don't want to end the river at the entry of the valley. */ + if (tile == begin) return false; + + /* We don't want the river to end in the desert. */ + if (_settings_game.game_creation.landscape == LandscapeType::Tropic && GetTropicZone(tile) == TROPICZONE_DESERT) return false; + + /* Only end on flat slopes. */ + int height_lake; + if (!IsTileFlat(tile, &height_lake)) return false; + + /* Only build at the height of the river. */ + int height_begin = TileHeight(begin); + if (height_lake != height_begin) return false; + + /* Checks successful, time to build. + * Chance of water feature is split evenly between a lake, a wetland with trees, and a wetland with grass. */ + if (Chance16(1, 3)) { + MakeLake(tile, height_lake); + } else { + MakeWetlands(tile, height_lake, DistanceManhattan(tile, begin)); + } + + /* This is the new end of the river. */ + return true; +} + /** * Widen a river by expanding into adjacent tiles via circular tile search. * @param tile The tile to try expanding the river into. @@ -1284,31 +1378,12 @@ static std::tuple FlowRiver(TileIndex spring, TileIndex begin, uint } else if (queue.size() > 32) { /* Maybe we can make a lake. Find the Nth of the considered tiles. */ TileIndex lake_centre = queue[RandomRange(static_cast(queue.size()))]; - int height_lake; - - if (IsValidTile(lake_centre) && - /* We don't want the lake at the entry of the valley. */ - lake_centre != begin && - /* We don't want lakes in the desert. */ - (_settings_game.game_creation.landscape != LandscapeType::Tropic || GetTropicZone(lake_centre) != TROPICZONE_DESERT) && - /* A river, or lake, can only be built on flat slopes. */ - IsTileFlat(lake_centre, &height_lake) && - /* We want the lake to be built at the height of the river. */ - height_lake == height_begin && - /* We only want a lake if the river is long enough. */ - DistanceManhattan(spring, lake_centre) > min_river_length) { - end = lake_centre; - MakeRiverAndModifyDesertZoneAround(lake_centre); - uint diameter = RandomRange(8) + 3; - - /* Run the loop twice, so artefacts from going circular in one direction get (mostly) hidden. */ - for (uint loops = 0; loops < 2; ++loops) { - for (auto tile : SpiralTileSequence(lake_centre, diameter)) { - MakeLake(tile, height_lake); - } + if (DistanceManhattan(spring, lake_centre) > min_river_length) { + if (TryMakeRiverTerminus(lake_centre, begin)) { + /* If successful, this becomes the new end of the river. */ + end = lake_centre; + found = true; } - - found = true; } } diff --git a/src/settings_type.h b/src/settings_type.h index 6babceeaab..eeef3aac22 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -99,6 +99,17 @@ enum RightClickClose : uint8_t { RCC_YES_EXCEPT_STICKY, }; +/** + * List of tree placer algorithm. + * + * This enumeration defines all possible tree placer algorithm in the game. + */ +enum TreePlacer : uint8_t { + TP_NONE, ///< No tree placer algorithm + TP_ORIGINAL, ///< The original algorithm + TP_IMPROVED, ///< A 'improved' algorithm +}; + /** Possible values for "place_houses" setting. */ enum PlaceHouses : uint8_t { PH_FORBIDDEN = 0, diff --git a/src/tree_cmd.cpp b/src/tree_cmd.cpp index 8633fe3d24..a5187011de 100644 --- a/src/tree_cmd.cpp +++ b/src/tree_cmd.cpp @@ -33,17 +33,6 @@ #include "safeguards.h" -/** - * List of tree placer algorithm. - * - * This enumeration defines all possible tree placer algorithm in the game. - */ -enum TreePlacer : uint8_t { - TP_NONE, ///< No tree placer algorithm - TP_ORIGINAL, ///< The original algorithm - TP_IMPROVED, ///< A 'improved' algorithm -}; - /** Where to place trees while in-game? */ enum ExtraTreePlacement : uint8_t { ETP_NO_SPREAD, ///< Grow trees on tiles that have them but don't spread to new ones @@ -167,8 +156,9 @@ static TreeType GetRandomTreeType(TileIndex tile, uint seed) * * @param tile The tile to make a tree-tile from * @param r The randomness value from a Random() value + * @param keep_density Whether to keep the existing ground density of the tile. */ -static void PlaceTree(TileIndex tile, uint32_t r) +void PlaceTree(TileIndex tile, uint32_t r, bool keep_density) { TreeType tree = GetRandomTreeType(tile, GB(r, 24, 8)); @@ -176,6 +166,9 @@ static void PlaceTree(TileIndex tile, uint32_t r) PlantTreesOnTile(tile, tree, GB(r, 22, 2), static_cast(std::min(GB(r, 16, 3), 6))); MarkTileDirtyByTile(tile); + /* Maybe keep the existing ground density.*/ + if (keep_density) return; + /* Rerandomize ground, if neither snow nor shore */ TreeGround ground = GetTreeGround(tile); if (ground != TREE_GROUND_SNOW_DESERT && ground != TREE_GROUND_ROUGH_SNOW && ground != TREE_GROUND_SHORE) { diff --git a/src/tree_cmd.h b/src/tree_cmd.h index 65e1a310ad..8a78683f87 100644 --- a/src/tree_cmd.h +++ b/src/tree_cmd.h @@ -12,6 +12,8 @@ #include "command_type.h" +void PlaceTree(TileIndex tile, uint32_t r, bool keep_density = false); + CommandCost CmdPlantTree(DoCommandFlags flags, TileIndex tile, TileIndex start_tile, uint8_t tree_to_plant, bool diagonal); DEF_CMD_TRAIT(CMD_PLANT_TREE, CmdPlantTree, CommandFlag::Auto, CommandType::LandscapeConstruction)