mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-15 11:03:00 +01:00
Merge pull request #23509 from AaronVanGeffen/mapgen-refactor
Refactor map generator and move it into its own namespace
This commit is contained in:
@@ -9,11 +9,13 @@
|
||||
- Change: [#23413] The max number of park entrance objects has been raised to 255.
|
||||
- Fix: [#1122] Trains spawned on a cable lift hill will fall down and crash (original bug).
|
||||
- Fix: [#22742, #22793] In game console does not handle format tokens properly.
|
||||
- Fix: [#23135] Map generator tree placement has noticable patterns.
|
||||
- Fix: [#23286] Currency formatted incorrectly in the in game console.
|
||||
- Fix: [#23348] Console set commands don't print output properly.
|
||||
- Fix: [#23376] Peeps with balloons, hats and umbrellas may leave artifacts on screen.
|
||||
- Fix: [#23486] Object selection minimum requirements can be bypassed with close window hotkey.
|
||||
- Fix: [#23496] Newly spawned vehicles are invisible when spawned while the game is paused.
|
||||
- Fix: [#23509] Map generator window reverts to flatland after selecting a heightmap image.
|
||||
|
||||
0.4.17 (2024-12-08)
|
||||
------------------------------------------------------------------------
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
#include <openrct2/sprites.h>
|
||||
#include <openrct2/windows/Intent.h>
|
||||
#include <openrct2/world/Map.h>
|
||||
#include <openrct2/world/MapGen.h>
|
||||
#include <openrct2/world/map_generator/MapGen.h>
|
||||
#include <openrct2/world/map_generator/PngTerrainGenerator.h>
|
||||
|
||||
using namespace OpenRCT2::World;
|
||||
|
||||
namespace OpenRCT2::Ui::Windows
|
||||
{
|
||||
@@ -267,7 +270,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
private:
|
||||
ResizeDirection _resizeDirection{ ResizeDirection::Both };
|
||||
bool _mapWidthAndHeightLinked{ true };
|
||||
MapGenSettings _settings{};
|
||||
MapGenerator::Settings _settings{};
|
||||
bool _randomTerrain = true;
|
||||
bool _heightmapLoaded = false;
|
||||
std::string _heightmapFilename{};
|
||||
@@ -371,17 +374,17 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
void GenerateMap()
|
||||
{
|
||||
if (_settings.algorithm == MapGenAlgorithm::heightmapImage && !_heightmapLoaded)
|
||||
if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage && !_heightmapLoaded)
|
||||
return;
|
||||
|
||||
MapGenSettings mapgenSettings = _settings;
|
||||
MapGenerator::Settings mapgenSettings = _settings;
|
||||
if (_randomTerrain)
|
||||
{
|
||||
mapgenSettings.landTexture = -1;
|
||||
mapgenSettings.edgeTexture = -1;
|
||||
}
|
||||
|
||||
MapGenGenerate(&mapgenSettings);
|
||||
MapGenerator::generate(&mapgenSettings);
|
||||
GfxInvalidateScreen();
|
||||
}
|
||||
|
||||
@@ -391,9 +394,9 @@ namespace OpenRCT2::Ui::Windows
|
||||
{
|
||||
SharedMouseUp(widgetIndex);
|
||||
|
||||
if (_settings.algorithm == MapGenAlgorithm::simplexNoise)
|
||||
if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise)
|
||||
SimplexMouseUp(widgetIndex);
|
||||
else if (_settings.algorithm == MapGenAlgorithm::heightmapImage)
|
||||
else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage)
|
||||
HeightmapMouseUp(widgetIndex);
|
||||
|
||||
switch (widgetIndex)
|
||||
@@ -414,10 +417,10 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
void BaseMouseDown(WidgetIndex widgetIndex, Widget* widget)
|
||||
{
|
||||
if (_settings.algorithm == MapGenAlgorithm::simplexNoise)
|
||||
if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise)
|
||||
SimplexMouseDown(widgetIndex, widget);
|
||||
|
||||
else if (_settings.algorithm == MapGenAlgorithm::heightmapImage)
|
||||
else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage)
|
||||
HeightmapMouseDown(widgetIndex, widget);
|
||||
|
||||
switch (widgetIndex)
|
||||
@@ -481,7 +484,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
switch (widgetIndex)
|
||||
{
|
||||
case WIDX_HEIGHTMAP_SOURCE_DROPDOWN:
|
||||
_settings.algorithm = MapGenAlgorithm(dropdownIndex);
|
||||
_settings.algorithm = MapGenerator::Algorithm(dropdownIndex);
|
||||
Invalidate();
|
||||
break;
|
||||
}
|
||||
@@ -489,9 +492,9 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
void BaseTextInput(WidgetIndex widgetIndex, int32_t value)
|
||||
{
|
||||
if (_settings.algorithm == MapGenAlgorithm::simplexNoise)
|
||||
if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise)
|
||||
SimplexTextInput(widgetIndex, value);
|
||||
else if (_settings.algorithm == MapGenAlgorithm::heightmapImage)
|
||||
else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage)
|
||||
HeightmapTextInput(widgetIndex, value);
|
||||
|
||||
switch (widgetIndex)
|
||||
@@ -522,7 +525,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
SetWidgetPressed(WIDX_MAP_SIZE_LINK, _mapWidthAndHeightLinked);
|
||||
SetWidgetDisabled(WIDX_MAP_SIZE_LINK, _settings.mapSize.x != _settings.mapSize.y);
|
||||
|
||||
bool isHeightMapImage = _settings.algorithm == MapGenAlgorithm::heightmapImage;
|
||||
bool isHeightMapImage = _settings.algorithm == MapGenerator::Algorithm::heightmapImage;
|
||||
SetWidgetDisabled(WIDX_MAP_SIZE_Y, isHeightMapImage);
|
||||
SetWidgetDisabled(WIDX_MAP_SIZE_Y_UP, isHeightMapImage);
|
||||
SetWidgetDisabled(WIDX_MAP_SIZE_Y_DOWN, isHeightMapImage);
|
||||
@@ -551,19 +554,19 @@ namespace OpenRCT2::Ui::Windows
|
||||
auto& sourceWidget = widgets[WIDX_HEIGHTMAP_SOURCE];
|
||||
switch (_settings.algorithm)
|
||||
{
|
||||
case MapGenAlgorithm::blank:
|
||||
case MapGenerator::Algorithm::blank:
|
||||
sourceWidget.text = STR_HEIGHTMAP_FLATLAND;
|
||||
ToggleSimplexWidgets(false);
|
||||
ToggleHeightmapWidgets(false);
|
||||
break;
|
||||
|
||||
case MapGenAlgorithm::simplexNoise:
|
||||
case MapGenerator::Algorithm::simplexNoise:
|
||||
sourceWidget.text = STR_HEIGHTMAP_SIMPLEX_NOISE;
|
||||
ToggleSimplexWidgets(true);
|
||||
ToggleHeightmapWidgets(false);
|
||||
break;
|
||||
|
||||
case MapGenAlgorithm::heightmapImage:
|
||||
case MapGenerator::Algorithm::heightmapImage:
|
||||
sourceWidget.text = STR_HEIGHTMAP_FILE;
|
||||
ToggleSimplexWidgets(false);
|
||||
ToggleHeightmapWidgets(true);
|
||||
@@ -603,10 +606,10 @@ namespace OpenRCT2::Ui::Windows
|
||||
DrawWidgets(dpi);
|
||||
DrawTabImages(dpi);
|
||||
|
||||
if (_settings.algorithm == MapGenAlgorithm::simplexNoise)
|
||||
if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise)
|
||||
SimplexDraw(dpi);
|
||||
|
||||
else if (_settings.algorithm == MapGenAlgorithm::heightmapImage)
|
||||
else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage)
|
||||
HeightmapDraw(dpi);
|
||||
|
||||
const auto enabledColour = colours[1];
|
||||
@@ -753,7 +756,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
SetPressedTab();
|
||||
|
||||
const bool isFlatland = _settings.algorithm == MapGenAlgorithm::blank;
|
||||
const bool isFlatland = _settings.algorithm == MapGenerator::Algorithm::blank;
|
||||
|
||||
SetWidgetDisabled(WIDX_TREE_LAND_RATIO, !_settings.trees);
|
||||
SetWidgetDisabled(WIDX_TREE_LAND_RATIO_UP, !_settings.trees);
|
||||
@@ -801,7 +804,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
STR_RIDE_LENGTH_ENTRY, ft, { textColour });
|
||||
|
||||
// Maximum tree altitude, label and value
|
||||
const bool isFlatland = _settings.algorithm == MapGenAlgorithm::blank;
|
||||
const bool isFlatland = _settings.algorithm == MapGenerator::Algorithm::blank;
|
||||
const auto maxTreeTextColour = _settings.trees && !isFlatland ? enabledColour : disabledColour;
|
||||
|
||||
DrawTextBasic(
|
||||
@@ -1232,10 +1235,10 @@ namespace OpenRCT2::Ui::Windows
|
||||
SetWidgetEnabled(WIDX_WALL_TEXTURE, !_randomTerrain);
|
||||
|
||||
// Max land height option is irrelevant for flatland
|
||||
SetWidgetEnabled(WIDX_HEIGHTMAP_HIGH, _settings.algorithm != MapGenAlgorithm::blank);
|
||||
SetWidgetEnabled(WIDX_HEIGHTMAP_HIGH, _settings.algorithm != MapGenerator::Algorithm::blank);
|
||||
|
||||
// Only offer terrain edge smoothing if we don't use flatland terrain
|
||||
SetWidgetEnabled(WIDX_HEIGHTMAP_SMOOTH_TILE_EDGES, _settings.algorithm != MapGenAlgorithm::blank);
|
||||
SetWidgetEnabled(WIDX_HEIGHTMAP_SMOOTH_TILE_EDGES, _settings.algorithm != MapGenerator::Algorithm::blank);
|
||||
SetPressedTab();
|
||||
}
|
||||
|
||||
@@ -1395,7 +1398,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
void OnClose() override
|
||||
{
|
||||
MapGenUnloadHeightmapImage();
|
||||
MapGenerator::UnloadHeightmapImage();
|
||||
}
|
||||
|
||||
void OnMouseUp(WidgetIndex widgetIndex) override
|
||||
@@ -1454,7 +1457,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
|
||||
void OnPrepareDraw() override
|
||||
{
|
||||
bool isHeightMapImage = _settings.algorithm == MapGenAlgorithm::heightmapImage;
|
||||
bool isHeightMapImage = _settings.algorithm == MapGenerator::Algorithm::heightmapImage;
|
||||
SetWidgetDisabled(WIDX_MAP_GENERATE, isHeightMapImage && !_heightmapLoaded);
|
||||
|
||||
switch (page)
|
||||
@@ -1534,7 +1537,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
{
|
||||
if (result == MODAL_RESULT_OK)
|
||||
{
|
||||
if (!MapGenLoadHeightmapImage(path))
|
||||
if (!MapGenerator::LoadHeightmapImage(path))
|
||||
{
|
||||
// TODO: Display error popup
|
||||
return;
|
||||
@@ -1543,6 +1546,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
// The window needs to be open while using the map
|
||||
_heightmapLoaded = true;
|
||||
_heightmapFilename = fs::u8path(path).filename().string();
|
||||
_settings.algorithm = MapGenerator::Algorithm::heightmapImage;
|
||||
SetPage(WINDOW_MAPGEN_PAGE_BASE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,8 +616,6 @@
|
||||
<ClInclude Include="world\Location.hpp" />
|
||||
<ClInclude Include="world\Map.h" />
|
||||
<ClInclude Include="world\MapAnimation.h" />
|
||||
<ClInclude Include="world\MapGen.h" />
|
||||
<ClInclude Include="world\MapHelpers.h" />
|
||||
<ClInclude Include="world\Park.h" />
|
||||
<ClInclude Include="world\QuarterTile.h" />
|
||||
<ClInclude Include="world\Scenery.h" />
|
||||
@@ -626,6 +624,13 @@
|
||||
<ClInclude Include="world\TileElementsView.h" />
|
||||
<ClInclude Include="world\TileInspector.h" />
|
||||
<ClInclude Include="world\TilePointerIndex.hpp" />
|
||||
<ClInclude Include="world\map_generator\HeightMap.hpp" />
|
||||
<ClInclude Include="world\map_generator\MapGen.h" />
|
||||
<ClInclude Include="world\map_generator\MapHelpers.h" />
|
||||
<ClInclude Include="world\map_generator\PngTerrainGenerator.h" />
|
||||
<ClInclude Include="world\map_generator\SimplexNoise.h" />
|
||||
<ClInclude Include="world\map_generator\SurfaceSelection.h" />
|
||||
<ClInclude Include="world\map_generator\TreePlacement.h" />
|
||||
<ClInclude Include="world\tile_element\BannerElement.h" />
|
||||
<ClInclude Include="world\tile_element\EntranceElement.h" />
|
||||
<ClInclude Include="world\tile_element\LargeSceneryElement.h" />
|
||||
@@ -1115,13 +1120,17 @@
|
||||
<ClCompile Include="world\Footpath.cpp" />
|
||||
<ClCompile Include="world\Map.cpp" />
|
||||
<ClCompile Include="world\MapAnimation.cpp" />
|
||||
<ClCompile Include="world\MapGen.cpp" />
|
||||
<ClCompile Include="world\MapHelpers.cpp" />
|
||||
<ClCompile Include="world\Park.cpp" />
|
||||
<ClCompile Include="world\QuarterTile.cpp" />
|
||||
<ClCompile Include="world\Scenery.cpp" />
|
||||
<ClCompile Include="world\SurfaceData.cpp" />
|
||||
<ClCompile Include="world\TileInspector.cpp" />
|
||||
<ClCompile Include="world\map_generator\MapGen.cpp" />
|
||||
<ClCompile Include="world\map_generator\MapHelpers.cpp" />
|
||||
<ClCompile Include="world\map_generator\PngTerrainGenerator.cpp" />
|
||||
<ClCompile Include="world\map_generator\SimplexNoise.cpp" />
|
||||
<ClCompile Include="world\map_generator\SurfaceSelection.cpp" />
|
||||
<ClCompile Include="world\map_generator\TreePlacement.cpp" />
|
||||
<ClCompile Include="world\tile_element\BannerElement.cpp" />
|
||||
<ClCompile Include="world\tile_element\EntranceElement.cpp" />
|
||||
<ClCompile Include="world\tile_element\LargeSceneryElement.cpp" />
|
||||
|
||||
@@ -1,950 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "MapGen.h"
|
||||
|
||||
#include "../Context.h"
|
||||
#include "../Diagnostic.h"
|
||||
#include "../Game.h"
|
||||
#include "../GameState.h"
|
||||
#include "../core/Guard.hpp"
|
||||
#include "../core/Imaging.h"
|
||||
#include "../core/String.hpp"
|
||||
#include "../localisation/StringIds.h"
|
||||
#include "../object/ObjectEntryManager.h"
|
||||
#include "../object/ObjectList.h"
|
||||
#include "../object/ObjectManager.h"
|
||||
#include "../object/SmallSceneryEntry.h"
|
||||
#include "../object/TerrainEdgeObject.h"
|
||||
#include "../object/TerrainSurfaceObject.h"
|
||||
#include "../platform/Platform.h"
|
||||
#include "../util/Util.h"
|
||||
#include "../world/tile_element/Slope.h"
|
||||
#include "../world/tile_element/SmallSceneryElement.h"
|
||||
#include "../world/tile_element/SurfaceElement.h"
|
||||
#include "../world/tile_element/TileElement.h"
|
||||
#include "Map.h"
|
||||
#include "MapHelpers.h"
|
||||
#include "Scenery.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
using namespace OpenRCT2;
|
||||
|
||||
#pragma region Height map struct
|
||||
|
||||
static struct
|
||||
{
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
std::vector<uint8_t> mono_bitmap;
|
||||
} _heightMapData;
|
||||
|
||||
#pragma endregion Height map struct
|
||||
|
||||
#pragma region Random objects
|
||||
|
||||
static constexpr const char* GrassTrees[] = {
|
||||
// Dark
|
||||
"rct2.scenery_small.tcf", // Caucasian Fir Tree
|
||||
"rct2.scenery_small.trf", // Red Fir Tree
|
||||
"rct2.scenery_small.trf2", // Red Fir Tree
|
||||
"rct2.scenery_small.tsp", // Scots Pine Tree
|
||||
"rct2.scenery_small.tmzp", // Montezuma Pine Tree
|
||||
"rct2.scenery_small.tap", // Aleppo Pine Tree
|
||||
"rct2.scenery_small.tcrp", // Corsican Pine Tree
|
||||
"rct2.scenery_small.tbp", // Black Poplar Tree
|
||||
|
||||
// Light
|
||||
"rct2.scenery_small.tcl", // Cedar of Lebanon Tree
|
||||
"rct2.scenery_small.tel", // European Larch Tree
|
||||
};
|
||||
|
||||
static constexpr const char* DesertTrees[] = {
|
||||
"rct2.scenery_small.tmp", // Monkey-Puzzle Tree
|
||||
"rct2.scenery_small.thl", // Honey Locust Tree
|
||||
"rct2.scenery_small.th1", // Canary Palm Tree
|
||||
"rct2.scenery_small.th2", // Palm Tree
|
||||
"rct2.scenery_small.tpm", // Palm Tree
|
||||
"rct2.scenery_small.tropt1", // Tree
|
||||
"rct2.scenery_small.tbc", // Cactus
|
||||
"rct2.scenery_small.tsc", // Cactus
|
||||
};
|
||||
|
||||
static constexpr const char* SnowTrees[] = {
|
||||
"rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree
|
||||
"rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree
|
||||
"rct2.scenery_small.trf3", // Snow-covered Red Fir Tree
|
||||
"rct2.scenery_small.trfs", // Snow-covered Red Fir Tree
|
||||
};
|
||||
|
||||
#pragma endregion
|
||||
|
||||
// Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock.
|
||||
static constexpr std::string_view BaseTerrain[] = {
|
||||
"rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown",
|
||||
"rct2.terrain_surface.dirt", "rct2.terrain_surface.ice",
|
||||
};
|
||||
|
||||
static void MapGenGenerateBlank(MapGenSettings* settings);
|
||||
static void MapGenGenerateSimplex(MapGenSettings* settings);
|
||||
static void MapGenGenerateFromHeightmapImage(MapGenSettings* settings);
|
||||
|
||||
static void MapGenPlaceTrees(MapGenSettings* settings);
|
||||
static void MapGenAddBeaches(MapGenSettings* settings);
|
||||
|
||||
void MapGenGenerate(MapGenSettings* settings)
|
||||
{
|
||||
// First, generate the height map
|
||||
switch (settings->algorithm)
|
||||
{
|
||||
case MapGenAlgorithm::blank:
|
||||
MapGenGenerateBlank(settings);
|
||||
break;
|
||||
|
||||
case MapGenAlgorithm::simplexNoise:
|
||||
MapGenGenerateSimplex(settings);
|
||||
break;
|
||||
|
||||
case MapGenAlgorithm::heightmapImage:
|
||||
MapGenGenerateFromHeightmapImage(settings);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add beaches?
|
||||
if (settings->beaches)
|
||||
MapGenAddBeaches(settings);
|
||||
|
||||
// Place trees?
|
||||
if (settings->trees)
|
||||
MapGenPlaceTrees(settings);
|
||||
}
|
||||
|
||||
static void MapGenSetWaterLevel(int32_t waterLevel);
|
||||
static void MapGenSmoothHeight(int32_t iterations);
|
||||
static void MapGenSetHeight(MapGenSettings* settings);
|
||||
|
||||
static float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence);
|
||||
static void MapGenSimplex(MapGenSettings* settings);
|
||||
|
||||
static TileCoordsXY _heightSize;
|
||||
static uint8_t* _height;
|
||||
|
||||
static int32_t GetHeight(int32_t x, int32_t y)
|
||||
{
|
||||
if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y)
|
||||
return _height[x + y * _heightSize.x];
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void SetHeight(int32_t x, int32_t y, int32_t height)
|
||||
{
|
||||
if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y)
|
||||
_height[x + y * _heightSize.x] = height;
|
||||
}
|
||||
|
||||
static ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings)
|
||||
{
|
||||
auto& objectManager = OpenRCT2::GetContext()->GetObjectManager();
|
||||
|
||||
const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture);
|
||||
std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : "";
|
||||
|
||||
if (surfaceTexture.empty())
|
||||
{
|
||||
std::vector<std::string_view> availableTerrains;
|
||||
std::copy_if(std::begin(BaseTerrain), std::end(BaseTerrain), std::back_inserter(availableTerrains), [&](auto terrain) {
|
||||
return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr;
|
||||
});
|
||||
|
||||
if (availableTerrains.empty())
|
||||
// Fall back to the first available surface texture that is available in the park
|
||||
surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier();
|
||||
else
|
||||
surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()];
|
||||
}
|
||||
|
||||
auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture));
|
||||
return surfaceTextureId;
|
||||
}
|
||||
|
||||
static ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId)
|
||||
{
|
||||
auto& objectManager = OpenRCT2::GetContext()->GetObjectManager();
|
||||
|
||||
const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture);
|
||||
std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : "";
|
||||
|
||||
if (edgeTexture.empty())
|
||||
{
|
||||
auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId);
|
||||
auto surfaceTexture = surfaceObject->GetIdentifier();
|
||||
|
||||
// Base edge type on surface type
|
||||
if (surfaceTexture == "rct2.terrain_surface.dirt")
|
||||
edgeTexture = "rct2.terrain_edge.wood_red";
|
||||
else if (surfaceTexture == "rct2.terrain_surface.ice")
|
||||
edgeTexture = "rct2.terrain_edge.ice";
|
||||
else
|
||||
edgeTexture = "rct2.terrain_edge.rock";
|
||||
|
||||
// Fall back to the first available edge texture that is available in the park
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr)
|
||||
edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier();
|
||||
}
|
||||
|
||||
auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture));
|
||||
return edgeTextureId;
|
||||
}
|
||||
|
||||
static void MapGenResetSurfaces(MapGenSettings* settings)
|
||||
{
|
||||
MapClearAllElements();
|
||||
MapInit(settings->mapSize);
|
||||
|
||||
const auto surfaceTextureId = MapGenSurfaceTextureId(settings);
|
||||
const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId);
|
||||
|
||||
for (auto y = 1; y < settings->mapSize.y - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < settings->mapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement != nullptr)
|
||||
{
|
||||
surfaceElement->SetSurfaceObjectIndex(surfaceTextureId);
|
||||
surfaceElement->SetEdgeObjectIndex(edgeTextureId);
|
||||
surfaceElement->BaseHeight = settings->heightmapLow;
|
||||
surfaceElement->ClearanceHeight = settings->heightmapLow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void MapGenGenerateBlank(MapGenSettings* settings)
|
||||
{
|
||||
MapGenResetSurfaces(settings);
|
||||
MapGenSetWaterLevel(settings->waterLevel);
|
||||
}
|
||||
|
||||
static void MapGenGenerateSimplex(MapGenSettings* settings)
|
||||
{
|
||||
MapGenResetSurfaces(settings);
|
||||
|
||||
// Create the temporary height map and initialise
|
||||
auto& mapSize = settings->mapSize;
|
||||
_heightSize = { mapSize.x * 2, mapSize.y * 2 };
|
||||
_height = new uint8_t[_heightSize.y * _heightSize.x];
|
||||
std::fill_n(_height, _heightSize.y * _heightSize.x, 0x00);
|
||||
|
||||
MapGenSimplex(settings);
|
||||
MapGenSmoothHeight(2 + (UtilRand() % 6));
|
||||
|
||||
// Set the game map to the height map
|
||||
MapGenSetHeight(settings);
|
||||
delete[] _height;
|
||||
|
||||
if (settings->smoothTileEdges)
|
||||
{
|
||||
// Set the tile slopes so that there are no cliffs
|
||||
while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Add the water
|
||||
MapGenSetWaterLevel(settings->waterLevel);
|
||||
}
|
||||
|
||||
static void MapGenAddBeaches(MapGenSettings* settings)
|
||||
{
|
||||
auto& objectManager = OpenRCT2::GetContext()->GetObjectManager();
|
||||
|
||||
// Figure out what beach texture to use
|
||||
std::vector<std::string_view> availableBeachTextures;
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr)
|
||||
availableBeachTextures.push_back("rct2.terrain_surface.sand");
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr)
|
||||
availableBeachTextures.push_back("rct2.terrain_surface.sand_brown");
|
||||
|
||||
if (availableBeachTextures.empty())
|
||||
return;
|
||||
|
||||
std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()];
|
||||
auto beachTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture));
|
||||
|
||||
// Add sandy beaches
|
||||
const auto& mapSize = settings->mapSize;
|
||||
for (auto y = 1; y < mapSize.y - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < mapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
|
||||
if (surfaceElement != nullptr && surfaceElement->BaseHeight < settings->waterLevel + 6)
|
||||
surfaceElement->SetSurfaceObjectIndex(beachTextureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void MapGenPlaceTree(ObjectEntryIndex type, const CoordsXY& loc)
|
||||
{
|
||||
auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry<SmallSceneryEntry>(type);
|
||||
if (sceneryEntry == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t surfaceZ = TileElementHeight(loc.ToTileCentre());
|
||||
|
||||
auto* sceneryElement = TileElementInsert<SmallSceneryElement>({ loc, surfaceZ }, 0b1111);
|
||||
Guard::Assert(sceneryElement != nullptr);
|
||||
|
||||
sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height);
|
||||
sceneryElement->SetDirection(UtilRand() & 3);
|
||||
sceneryElement->SetEntryIndex(type);
|
||||
sceneryElement->SetAge(0);
|
||||
sceneryElement->SetPrimaryColour(COLOUR_YELLOW);
|
||||
}
|
||||
|
||||
static bool MapGenSurfaceTakesGrassTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps" || id == "rct2.terrain_surface.dirt";
|
||||
}
|
||||
|
||||
static bool MapGenSurfaceTakesSandTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown"
|
||||
|| id == "rct2.terrain_surface.sand_red";
|
||||
}
|
||||
|
||||
static bool MapGenSurfaceTakesSnowTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.ice";
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool TryFindTreeInList(std::string_view id, const T& treeList)
|
||||
{
|
||||
for (size_t j = 0; j < std::size(treeList); j++)
|
||||
{
|
||||
if (treeList[j] == id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on.
|
||||
*/
|
||||
static void MapGenPlaceTrees(MapGenSettings* settings)
|
||||
{
|
||||
std::vector<int32_t> grassTreeIds;
|
||||
std::vector<int32_t> desertTreeIds;
|
||||
std::vector<int32_t> snowTreeIds;
|
||||
|
||||
for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++)
|
||||
{
|
||||
auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry<SmallSceneryEntry>(i);
|
||||
auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i);
|
||||
|
||||
if (sceneryEntry == nullptr)
|
||||
continue;
|
||||
|
||||
if (TryFindTreeInList(entry->GetIdentifier(), GrassTrees))
|
||||
{
|
||||
grassTreeIds.push_back(i);
|
||||
}
|
||||
else if (TryFindTreeInList(entry->GetIdentifier(), DesertTrees))
|
||||
{
|
||||
desertTreeIds.push_back(i);
|
||||
}
|
||||
else if (TryFindTreeInList(entry->GetIdentifier(), SnowTrees))
|
||||
{
|
||||
snowTreeIds.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Place trees
|
||||
float treeToLandRatio = static_cast<float>(settings->treeToLandRatio) / 100.0f;
|
||||
|
||||
auto& gameState = GetGameState();
|
||||
for (int32_t y = 1; y < gameState.MapSize.y - 1; y++)
|
||||
{
|
||||
for (int32_t x = 1; x < gameState.MapSize.x - 1; x++)
|
||||
{
|
||||
auto pos = CoordsXY{ x, y } * kCoordsXYStep;
|
||||
auto* surfaceElement = MapGetSurfaceElementAt(pos);
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
|
||||
// Don't place on water
|
||||
if (surfaceElement->GetWaterHeight() > 0)
|
||||
continue;
|
||||
|
||||
if (settings->minTreeAltitude > surfaceElement->BaseHeight
|
||||
|| settings->maxTreeAltitude < surfaceElement->BaseHeight)
|
||||
continue;
|
||||
|
||||
// On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn
|
||||
// vegetation
|
||||
float oasisScore = 0.0f;
|
||||
ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
||||
const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex());
|
||||
if (MapGenSurfaceTakesSandTrees(surfaceStyleObject))
|
||||
{
|
||||
oasisScore = -0.5f;
|
||||
constexpr auto maxOasisDistance = 4;
|
||||
for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++)
|
||||
{
|
||||
for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++)
|
||||
{
|
||||
// Get map coord, clamped to the edges
|
||||
const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep };
|
||||
auto neighbourPos = pos + offset;
|
||||
neighbourPos.x = std::clamp(neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1));
|
||||
neighbourPos.y = std::clamp(neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1));
|
||||
|
||||
const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos);
|
||||
if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0)
|
||||
{
|
||||
float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY);
|
||||
oasisScore += 0.5f / (maxOasisDistance * distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use tree:land ratio except when near an oasis
|
||||
constexpr static auto randModulo = 0xFFFF;
|
||||
if (static_cast<float>(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore))
|
||||
continue;
|
||||
|
||||
// Use fractal noise to group tiles that are likely to spawn trees together
|
||||
float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f);
|
||||
// Reduces the range to rarely stray further than 0.5 from the mean.
|
||||
float noiseOffset = UtilRandNormalDistributed() * 0.25f;
|
||||
if (noiseValue + oasisScore < noiseOffset)
|
||||
continue;
|
||||
|
||||
if (!grassTreeIds.empty() && MapGenSurfaceTakesGrassTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()];
|
||||
}
|
||||
else if (!desertTreeIds.empty() && MapGenSurfaceTakesSandTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()];
|
||||
}
|
||||
else if (!snowTreeIds.empty() && MapGenSurfaceTakesSnowTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()];
|
||||
}
|
||||
|
||||
if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL)
|
||||
MapGenPlaceTree(treeObjectEntryIndex, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets each tile's water level to the specified water level if underneath that water level.
|
||||
*/
|
||||
static void MapGenSetWaterLevel(int32_t waterLevel)
|
||||
{
|
||||
auto& gameState = GetGameState();
|
||||
for (int32_t y = 1; y < gameState.MapSize.y - 1; y++)
|
||||
{
|
||||
for (int32_t x = 1; x < gameState.MapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement != nullptr && surfaceElement->BaseHeight < waterLevel)
|
||||
surfaceElement->SetWaterHeight(waterLevel * kCoordsZStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooths the height map.
|
||||
*/
|
||||
static void MapGenSmoothHeight(int32_t iterations)
|
||||
{
|
||||
int32_t i, x, y, xx, yy, avg;
|
||||
int32_t arraySize = _heightSize.y * _heightSize.x * sizeof(uint8_t);
|
||||
uint8_t* copyHeight = new uint8_t[arraySize];
|
||||
|
||||
for (i = 0; i < iterations; i++)
|
||||
{
|
||||
std::memcpy(copyHeight, _height, arraySize);
|
||||
for (y = 1; y < _heightSize.y - 1; y++)
|
||||
{
|
||||
for (x = 1; x < _heightSize.x - 1; x++)
|
||||
{
|
||||
avg = 0;
|
||||
for (yy = -1; yy <= 1; yy++)
|
||||
{
|
||||
for (xx = -1; xx <= 1; xx++)
|
||||
{
|
||||
avg += copyHeight[(y + yy) * _heightSize.x + (x + xx)];
|
||||
}
|
||||
}
|
||||
avg /= 9;
|
||||
SetHeight(x, y, avg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete[] copyHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the actual game map tiles to the height map.
|
||||
*/
|
||||
static void MapGenSetHeight(MapGenSettings* settings)
|
||||
{
|
||||
int32_t x, y, heightX, heightY;
|
||||
|
||||
for (y = 1; y < _heightSize.y / 2 - 1; y++)
|
||||
{
|
||||
for (x = 1; x < _heightSize.x / 2 - 1; x++)
|
||||
{
|
||||
heightX = x * 2;
|
||||
heightY = y * 2;
|
||||
|
||||
uint8_t q00 = GetHeight(heightX + 0, heightY + 0);
|
||||
uint8_t q01 = GetHeight(heightX + 0, heightY + 1);
|
||||
uint8_t q10 = GetHeight(heightX + 1, heightY + 0);
|
||||
uint8_t q11 = GetHeight(heightX + 1, heightY + 1);
|
||||
|
||||
uint8_t baseHeight = (q00 + q01 + q10 + q11) / 4;
|
||||
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
surfaceElement->BaseHeight = std::max(2, baseHeight * 2);
|
||||
|
||||
// If base height is below water level, lower it to create more natural shorelines
|
||||
if (surfaceElement->BaseHeight >= 4 && surfaceElement->BaseHeight <= settings->waterLevel)
|
||||
surfaceElement->BaseHeight -= 2;
|
||||
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight;
|
||||
|
||||
uint8_t currentSlope = surfaceElement->GetSlope();
|
||||
|
||||
if (q00 > baseHeight)
|
||||
currentSlope |= kTileSlopeSCornerUp;
|
||||
if (q01 > baseHeight)
|
||||
currentSlope |= kTileSlopeWCornerUp;
|
||||
if (q10 > baseHeight)
|
||||
currentSlope |= kTileSlopeECornerUp;
|
||||
if (q11 > baseHeight)
|
||||
currentSlope |= kTileSlopeNCornerUp;
|
||||
|
||||
surfaceElement->SetSlope(currentSlope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma region Noise
|
||||
|
||||
/**
|
||||
* Simplex Noise Algorithm with Fractional Brownian Motion
|
||||
* Based on:
|
||||
* - https://code.google.com/p/simplexnoise/
|
||||
* - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion
|
||||
*/
|
||||
|
||||
static float Generate(float x, float y);
|
||||
static int32_t FastFloor(float x);
|
||||
static float Grad(int32_t hash, float x, float y);
|
||||
|
||||
static uint8_t perm[512];
|
||||
|
||||
static void NoiseRand()
|
||||
{
|
||||
for (auto& i : perm)
|
||||
{
|
||||
i = UtilRand() & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
static float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence)
|
||||
{
|
||||
float total = 0.0f;
|
||||
float amplitude = persistence;
|
||||
for (int32_t i = 0; i < octaves; i++)
|
||||
{
|
||||
total += Generate(x * frequency, y * frequency) * amplitude;
|
||||
frequency *= lacunarity;
|
||||
amplitude *= persistence;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static float Generate(float x, float y)
|
||||
{
|
||||
const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0)
|
||||
const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0
|
||||
|
||||
float n0, n1, n2; // Noise contributions from the three corners
|
||||
|
||||
// Skew the input space to determine which simplex cell we're in
|
||||
float s = (x + y) * F2; // Hairy factor for 2D
|
||||
float xs = x + s;
|
||||
float ys = y + s;
|
||||
int32_t i = FastFloor(xs);
|
||||
int32_t j = FastFloor(ys);
|
||||
|
||||
float t = static_cast<float>(i + j) * G2;
|
||||
float X0 = i - t; // Unskew the cell origin back to (x,y) space
|
||||
float Y0 = j - t;
|
||||
float x0 = x - X0; // The x,y distances from the cell origin
|
||||
float y0 = y - Y0;
|
||||
|
||||
// For the 2D case, the simplex shape is an equilateral triangle.
|
||||
// Determine which simplex we are in.
|
||||
int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
|
||||
if (x0 > y0)
|
||||
{
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
} // lower triangle, XY order: (0,0)->(1,0)->(1,1)
|
||||
else
|
||||
{
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
} // upper triangle, YX order: (0,0)->(0,1)->(1,1)
|
||||
|
||||
// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
|
||||
// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
|
||||
// c = (3-sqrt(3))/6
|
||||
|
||||
float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
|
||||
float y1 = y0 - j1 + G2;
|
||||
float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords
|
||||
float y2 = y0 - 1.0f + 2.0f * G2;
|
||||
|
||||
// Wrap the integer indices at 256, to avoid indexing perm[] out of bounds
|
||||
int32_t ii = i % 256;
|
||||
int32_t jj = j % 256;
|
||||
|
||||
// Calculate the contribution from the three corners
|
||||
float t0 = 0.5f - x0 * x0 - y0 * y0;
|
||||
if (t0 < 0.0f)
|
||||
{
|
||||
n0 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t0 *= t0;
|
||||
n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0);
|
||||
}
|
||||
|
||||
float t1 = 0.5f - x1 * x1 - y1 * y1;
|
||||
if (t1 < 0.0f)
|
||||
{
|
||||
n1 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t1 *= t1;
|
||||
n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1);
|
||||
}
|
||||
|
||||
float t2 = 0.5f - x2 * x2 - y2 * y2;
|
||||
if (t2 < 0.0f)
|
||||
{
|
||||
n2 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t2 *= t2;
|
||||
n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2);
|
||||
}
|
||||
|
||||
// Add contributions from each corner to get the final noise value.
|
||||
// The result is scaled to return values in the interval [-1,1].
|
||||
return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary!
|
||||
}
|
||||
|
||||
static int32_t FastFloor(float x)
|
||||
{
|
||||
return (x > 0) ? (static_cast<int32_t>(x)) : ((static_cast<int32_t>(x)) - 1);
|
||||
}
|
||||
|
||||
static float Grad(int32_t hash, float x, float y)
|
||||
{
|
||||
int32_t h = hash & 7; // Convert low 3 bits of hash code
|
||||
float u = h < 4 ? x : y; // into 8 simple gradient directions,
|
||||
float v = h < 4 ? y : x; // and compute the dot product with (x,y).
|
||||
return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v);
|
||||
}
|
||||
|
||||
static void MapGenSimplex(MapGenSettings* settings)
|
||||
{
|
||||
float freq = settings->simplex_base_freq / 100.0f * (1.0f / _heightSize.x);
|
||||
int32_t octaves = settings->simplex_octaves;
|
||||
|
||||
int32_t low = settings->heightmapLow / 2;
|
||||
int32_t high = settings->heightmapHigh / 2 - low;
|
||||
|
||||
NoiseRand();
|
||||
for (int32_t y = 0; y < _heightSize.y; y++)
|
||||
{
|
||||
for (int32_t x = 0; x < _heightSize.x; x++)
|
||||
{
|
||||
float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f);
|
||||
float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f;
|
||||
|
||||
SetHeight(x, y, low + static_cast<int32_t>(normalisedNoiseValue * high));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Heightmap
|
||||
|
||||
/**
|
||||
* Return the tile coordinate that matches the given pixel of a heightmap
|
||||
*/
|
||||
static TileCoordsXY MapgenHeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y)
|
||||
{
|
||||
// The height map does not include the empty tiles around the map, so we add 1.
|
||||
return TileCoordsXY(static_cast<int32_t>(y + 1), static_cast<int32_t>(x + 1));
|
||||
}
|
||||
|
||||
bool MapGenLoadHeightmapImage(const utf8* path)
|
||||
{
|
||||
auto format = Imaging::GetImageFormatFromPath(path);
|
||||
if (format == IMAGE_FORMAT::PNG)
|
||||
{
|
||||
// Promote to 32-bit
|
||||
format = IMAGE_FORMAT::PNG_32;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto image = Imaging::ReadFromFile(path, format);
|
||||
auto width = std::min<uint32_t>(image.Width, kMaximumMapSizePractical);
|
||||
auto height = std::min<uint32_t>(image.Height, kMaximumMapSizePractical);
|
||||
if (width != image.Width || height != image.Height)
|
||||
{
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {});
|
||||
}
|
||||
|
||||
// Allocate memory for the height map values, one byte pixel
|
||||
_heightMapData.mono_bitmap.resize(width * height);
|
||||
_heightMapData.width = width;
|
||||
_heightMapData.height = height;
|
||||
|
||||
// Copy average RGB value to mono bitmap
|
||||
constexpr auto numChannels = 4;
|
||||
const auto pitch = image.Stride;
|
||||
const auto pixels = image.Pixels.data();
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
const auto red = pixels[x * numChannels + y * pitch];
|
||||
const auto green = pixels[x * numChannels + y * pitch + 1];
|
||||
const auto blue = pixels[x * numChannels + y * pitch + 2];
|
||||
_heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case IMAGE_FORMAT::BITMAP:
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {});
|
||||
break;
|
||||
case IMAGE_FORMAT::PNG_32:
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {});
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unable to load height map image: %s", e.what());
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the memory used to store the selected height map
|
||||
*/
|
||||
void MapGenUnloadHeightmapImage()
|
||||
{
|
||||
_heightMapData.mono_bitmap.clear();
|
||||
_heightMapData.width = 0;
|
||||
_heightMapData.height = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies box blur to the surface N times
|
||||
*/
|
||||
static void MapGenSmoothHeightmap(std::vector<uint8_t>& src, int32_t strength)
|
||||
{
|
||||
// Create buffer to store one channel
|
||||
std::vector<uint8_t> dest(src.size());
|
||||
|
||||
for (int32_t i = 0; i < strength; i++)
|
||||
{
|
||||
// Calculate box blur value to all pixels of the surface
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
uint32_t heightSum = 0;
|
||||
|
||||
// Loop over neighbour pixels, all of them have the same weight
|
||||
for (int8_t offsetX = -1; offsetX <= 1; offsetX++)
|
||||
{
|
||||
for (int8_t offsetY = -1; offsetY <= 1; offsetY++)
|
||||
{
|
||||
// Clamp x and y so they stay within the image
|
||||
// This assumes the height map is not tiled, and increases the weight of the edges
|
||||
const int32_t readX = std::clamp<int32_t>(x + offsetX, 0, _heightMapData.width - 1);
|
||||
const int32_t readY = std::clamp<int32_t>(y + offsetY, 0, _heightMapData.height - 1);
|
||||
heightSum += src[readX + readY * _heightMapData.width];
|
||||
}
|
||||
}
|
||||
|
||||
// Take average
|
||||
dest[x + y * _heightMapData.width] = heightSum / 9;
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply the blur to the source pixels
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void MapGenGenerateFromHeightmapImage(MapGenSettings* settings)
|
||||
{
|
||||
Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded");
|
||||
Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same");
|
||||
|
||||
// Make a copy of the original height map that we can edit
|
||||
auto dest = _heightMapData.mono_bitmap;
|
||||
|
||||
// Get technical map size, +2 for the black tiles around the map
|
||||
auto maxWidth = static_cast<int32_t>(_heightMapData.width + 2);
|
||||
auto maxHeight = static_cast<int32_t>(_heightMapData.height + 2);
|
||||
MapInit({ maxHeight, maxWidth });
|
||||
|
||||
if (settings->smooth_height_map)
|
||||
{
|
||||
MapGenSmoothHeightmap(dest, settings->smooth_strength);
|
||||
}
|
||||
|
||||
uint8_t maxValue = 255;
|
||||
uint8_t minValue = 0;
|
||||
|
||||
if (settings->normalize_height)
|
||||
{
|
||||
// Get highest and lowest pixel value
|
||||
maxValue = 0;
|
||||
minValue = 0xff;
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
uint8_t value = dest[x + y * _heightMapData.width];
|
||||
maxValue = std::max(maxValue, value);
|
||||
minValue = std::min(minValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (minValue == maxValue)
|
||||
{
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Guard::Assert(maxValue > minValue, "Input range is invalid");
|
||||
Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid");
|
||||
|
||||
const auto surfaceTextureId = MapGenSurfaceTextureId(settings);
|
||||
const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId);
|
||||
|
||||
const uint8_t rangeIn = maxValue - minValue;
|
||||
const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2;
|
||||
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
// The x and y axis are flipped in the world, so this uses y for x and x for y.
|
||||
auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y);
|
||||
auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords);
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
|
||||
// Read value from bitmap, and convert its range
|
||||
uint8_t value = dest[x + y * _heightMapData.width];
|
||||
value = static_cast<uint8_t>(static_cast<float>(value - minValue) / rangeIn * rangeOut)
|
||||
+ (settings->heightmapLow * 2);
|
||||
surfaceElement->BaseHeight = value;
|
||||
|
||||
// Floor to even number
|
||||
surfaceElement->BaseHeight /= 2;
|
||||
surfaceElement->BaseHeight *= 2;
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight;
|
||||
|
||||
// Set textures
|
||||
surfaceElement->SetSurfaceObjectIndex(surfaceTextureId);
|
||||
surfaceElement->SetEdgeObjectIndex(edgeTextureId);
|
||||
|
||||
// Set water level
|
||||
if (surfaceElement->BaseHeight < settings->waterLevel)
|
||||
{
|
||||
surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth tile edges
|
||||
if (settings->smoothTileEdges)
|
||||
{
|
||||
// Keep smoothing the entire map until no tiles are changed anymore
|
||||
while (true)
|
||||
{
|
||||
uint32_t numTilesChanged = 0;
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y);
|
||||
numTilesChanged += TileSmooth(tileCoords);
|
||||
}
|
||||
}
|
||||
|
||||
if (numTilesChanged == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
@@ -1,53 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../core/StringTypes.h"
|
||||
#include "Location.hpp"
|
||||
|
||||
enum class MapGenAlgorithm : uint8_t
|
||||
{
|
||||
blank,
|
||||
simplexNoise,
|
||||
heightmapImage,
|
||||
};
|
||||
|
||||
struct MapGenSettings
|
||||
{
|
||||
// Base
|
||||
MapGenAlgorithm algorithm = MapGenAlgorithm::blank;
|
||||
TileCoordsXY mapSize{ 150, 150 };
|
||||
int32_t waterLevel = 6;
|
||||
int32_t landTexture = 0;
|
||||
int32_t edgeTexture = 0;
|
||||
int32_t heightmapLow = 14;
|
||||
int32_t heightmapHigh = 60;
|
||||
bool smoothTileEdges = true;
|
||||
|
||||
// Features (e.g. tree, rivers, lakes etc.)
|
||||
bool trees = true;
|
||||
int32_t treeToLandRatio = 25;
|
||||
int32_t minTreeAltitude = 10;
|
||||
int32_t maxTreeAltitude = 50;
|
||||
bool beaches = true;
|
||||
|
||||
// Simplex Noise Parameters
|
||||
int32_t simplex_base_freq = 175;
|
||||
int32_t simplex_octaves = 6;
|
||||
|
||||
// Height map settings
|
||||
bool smooth_height_map = true;
|
||||
uint32_t smooth_strength = 1;
|
||||
bool normalize_height = true;
|
||||
};
|
||||
|
||||
void MapGenGenerate(MapGenSettings* settings);
|
||||
bool MapGenLoadHeightmapImage(const utf8* path);
|
||||
void MapGenUnloadHeightmapImage();
|
||||
@@ -1,311 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "MapHelpers.h"
|
||||
|
||||
#include "../world/tile_element/Slope.h"
|
||||
#include "../world/tile_element/SurfaceElement.h"
|
||||
#include "Map.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
static uint8_t GetBaseHeightOrZero(int32_t x, int32_t y)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
return surfaceElement != nullptr ? surfaceElement->BaseHeight : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not perfect, this still leaves some particular tiles unsmoothed.
|
||||
*/
|
||||
int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b)
|
||||
{
|
||||
int32_t x, y, count, doubleCorner, raisedLand = 0;
|
||||
uint8_t highest, cornerHeights[4];
|
||||
for (y = t; y < b; y++)
|
||||
{
|
||||
for (x = l; x < r; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
surfaceElement->SetSlope(kTileSlopeFlat);
|
||||
|
||||
// Raise to edge height - 2
|
||||
highest = surfaceElement->BaseHeight;
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x - 1, y + 0));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 1, y + 0));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 0, y - 1));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 0, y + 1));
|
||||
if (surfaceElement->BaseHeight < highest - 2)
|
||||
{
|
||||
raisedLand = 1;
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2;
|
||||
}
|
||||
|
||||
// Check corners
|
||||
doubleCorner = -1;
|
||||
cornerHeights[0] = GetBaseHeightOrZero(x - 1, y - 1);
|
||||
cornerHeights[1] = GetBaseHeightOrZero(x + 1, y - 1);
|
||||
cornerHeights[2] = GetBaseHeightOrZero(x + 1, y + 1);
|
||||
cornerHeights[3] = GetBaseHeightOrZero(x - 1, y + 1);
|
||||
highest = surfaceElement->BaseHeight;
|
||||
for (std::size_t i = 0; i < std::size(cornerHeights); i++)
|
||||
highest = std::max(highest, cornerHeights[i]);
|
||||
|
||||
if (highest >= surfaceElement->BaseHeight + 4)
|
||||
{
|
||||
count = 0;
|
||||
int32_t canCompensate = 1;
|
||||
for (std::size_t i = 0; i < std::size(cornerHeights); i++)
|
||||
if (cornerHeights[i] == highest)
|
||||
{
|
||||
count++;
|
||||
|
||||
// Check if surrounding corners aren't too high. The current tile
|
||||
// can't compensate for all the height differences anymore if it has
|
||||
// the extra height slope.
|
||||
int32_t highestOnLowestSide;
|
||||
switch (i)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
highestOnLowestSide = std::max(GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y + 1));
|
||||
break;
|
||||
case 1:
|
||||
highestOnLowestSide = std::max(GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y + 1));
|
||||
break;
|
||||
case 2:
|
||||
highestOnLowestSide = std::max(GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y - 1));
|
||||
break;
|
||||
case 3:
|
||||
highestOnLowestSide = std::max(GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y - 1));
|
||||
break;
|
||||
}
|
||||
|
||||
if (highestOnLowestSide > surfaceElement->BaseHeight)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highestOnLowestSide;
|
||||
raisedLand = 1;
|
||||
canCompensate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 1 && canCompensate)
|
||||
{
|
||||
if (surfaceElement->BaseHeight < highest - 4)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 4;
|
||||
raisedLand = 1;
|
||||
}
|
||||
if (cornerHeights[0] == highest && cornerHeights[2] <= cornerHeights[0] - 4)
|
||||
doubleCorner = 0;
|
||||
else if (cornerHeights[1] == highest && cornerHeights[3] <= cornerHeights[1] - 4)
|
||||
doubleCorner = 1;
|
||||
else if (cornerHeights[2] == highest && cornerHeights[0] <= cornerHeights[2] - 4)
|
||||
doubleCorner = 2;
|
||||
else if (cornerHeights[3] == highest && cornerHeights[1] <= cornerHeights[3] - 4)
|
||||
doubleCorner = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (surfaceElement->BaseHeight < highest - 2)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2;
|
||||
raisedLand = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doubleCorner != -1)
|
||||
{
|
||||
uint8_t slope = surfaceElement->GetSlope() | kTileSlopeDiagonalFlag;
|
||||
switch (doubleCorner)
|
||||
{
|
||||
case 0:
|
||||
slope |= kTileSlopeNCornerDown;
|
||||
break;
|
||||
case 1:
|
||||
slope |= kTileSlopeWCornerDown;
|
||||
break;
|
||||
case 2:
|
||||
slope |= kTileSlopeSCornerDown;
|
||||
break;
|
||||
case 3:
|
||||
slope |= kTileSlopeECornerDown;
|
||||
break;
|
||||
}
|
||||
surfaceElement->SetSlope(slope);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t slope = surfaceElement->GetSlope();
|
||||
// Corners
|
||||
auto surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNCornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeWCornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeECornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSCornerUp;
|
||||
|
||||
// Sides
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 0 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNESideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 0 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSWSideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSESideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNWSideUp;
|
||||
|
||||
// Raise
|
||||
if (slope == kTileSlopeRaisedCornersMask)
|
||||
{
|
||||
slope = kTileSlopeFlat;
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight += 2;
|
||||
}
|
||||
surfaceElement->SetSlope(slope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return raisedLand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises the corners based on the height offset of neighbour tiles.
|
||||
* This does not change the base height, unless all corners have been raised.
|
||||
* @returns 0 if no edits were made, 1 otherwise
|
||||
*/
|
||||
int32_t TileSmooth(const TileCoordsXY& tileCoords)
|
||||
{
|
||||
auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords);
|
||||
if (surfaceElement == nullptr)
|
||||
return 0;
|
||||
|
||||
// +-----+-----+-----+
|
||||
// | W | NW | N |
|
||||
// | 2 | 1 | 0 |
|
||||
// +-----+-----+-----+
|
||||
// | SW | _ | NE |
|
||||
// | 4 | | 3 |
|
||||
// +-----+-----+-----+
|
||||
// | S | SE | E |
|
||||
// | 7 | 6 | 5 |
|
||||
// +-----+-----+-----+
|
||||
|
||||
union
|
||||
{
|
||||
int32_t baseheight[8];
|
||||
struct
|
||||
{
|
||||
int32_t N;
|
||||
int32_t NW;
|
||||
int32_t W;
|
||||
int32_t NE;
|
||||
int32_t SW;
|
||||
int32_t E;
|
||||
int32_t SE;
|
||||
int32_t S;
|
||||
};
|
||||
} neighbourHeightOffset = {};
|
||||
|
||||
// Find the neighbour base heights
|
||||
for (int32_t index = 0, y_offset = -1; y_offset <= 1; y_offset++)
|
||||
{
|
||||
for (int32_t x_offset = -1; x_offset <= 1; x_offset++)
|
||||
{
|
||||
// Skip self
|
||||
if (y_offset == 0 && x_offset == 0)
|
||||
continue;
|
||||
|
||||
// Get neighbour height. If the element is not valid (outside of map) assume the same height
|
||||
auto* neighbourSurfaceElement = MapGetSurfaceElementAt(tileCoords + TileCoordsXY{ x_offset, y_offset });
|
||||
neighbourHeightOffset.baseheight[index] = neighbourSurfaceElement != nullptr ? neighbourSurfaceElement->BaseHeight
|
||||
: surfaceElement->BaseHeight;
|
||||
|
||||
// Make the height relative to the current surface element
|
||||
neighbourHeightOffset.baseheight[index] -= surfaceElement->BaseHeight;
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count number from the three tiles that is currently higher
|
||||
int8_t thresholdW = std::clamp(neighbourHeightOffset.SW, 0, 1) + std::clamp(neighbourHeightOffset.W, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.NW, 0, 1);
|
||||
int8_t thresholdN = std::clamp(neighbourHeightOffset.NW, 0, 1) + std::clamp(neighbourHeightOffset.N, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.NE, 0, 1);
|
||||
int8_t thresholdE = std::clamp(neighbourHeightOffset.NE, 0, 1) + std::clamp(neighbourHeightOffset.E, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.SE, 0, 1);
|
||||
int8_t thresholdS = std::clamp(neighbourHeightOffset.SE, 0, 1) + std::clamp(neighbourHeightOffset.S, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.SW, 0, 1);
|
||||
|
||||
uint8_t slope = kTileSlopeFlat;
|
||||
slope |= (thresholdW >= 1) ? SLOPE_W_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdN >= 1) ? SLOPE_N_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdE >= 1) ? SLOPE_E_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdS >= 1) ? SLOPE_S_THRESHOLD_FLAGS : 0;
|
||||
|
||||
// Set diagonal when three corners (one corner down) have been raised, and the middle one can be raised one more
|
||||
if ((slope == kTileSlopeWCornerDown && neighbourHeightOffset.W >= 4)
|
||||
|| (slope == kTileSlopeSCornerDown && neighbourHeightOffset.S >= 4)
|
||||
|| (slope == kTileSlopeECornerDown && neighbourHeightOffset.E >= 4)
|
||||
|| (slope == kTileSlopeNCornerDown && neighbourHeightOffset.N >= 4))
|
||||
{
|
||||
slope |= kTileSlopeDiagonalFlag;
|
||||
}
|
||||
|
||||
// Check if the calculated slope is the same already
|
||||
uint8_t currentSlope = surfaceElement->GetSlope();
|
||||
if (currentSlope == slope)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((slope & kTileSlopeRaisedCornersMask) == kTileSlopeRaisedCornersMask)
|
||||
{
|
||||
// All corners are raised, raise the entire tile instead.
|
||||
surfaceElement->SetSlope(kTileSlopeFlat);
|
||||
surfaceElement->BaseHeight = (surfaceElement->ClearanceHeight += 2);
|
||||
if (surfaceElement->GetWaterHeight() <= surfaceElement->GetBaseZ())
|
||||
{
|
||||
surfaceElement->SetWaterHeight(0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply the slope to this tile
|
||||
surfaceElement->SetSlope(slope);
|
||||
|
||||
// Set correct clearance height
|
||||
if (slope & kTileSlopeDiagonalFlag)
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 4;
|
||||
else if (slope & kTileSlopeRaisedCornersMask)
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
66
src/openrct2/world/map_generator/HeightMap.hpp
Normal file
66
src/openrct2/world/map_generator/HeightMap.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Location.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
class HeightMap
|
||||
{
|
||||
private:
|
||||
std::vector<uint8_t> _height;
|
||||
|
||||
public:
|
||||
const uint16_t width;
|
||||
const uint16_t height;
|
||||
|
||||
HeightMap(int32_t tagetWidth, int32_t targetHeight)
|
||||
: _height(tagetWidth * targetHeight)
|
||||
, width(tagetWidth)
|
||||
, height(targetHeight)
|
||||
{
|
||||
}
|
||||
|
||||
HeightMap(const HeightMap& heightMap) = default;
|
||||
|
||||
uint8_t& operator[](TileCoordsXY pos)
|
||||
{
|
||||
assert(pos.x >= 0 || pos.y >= 0 || pos.x < width || pos.y < height);
|
||||
return _height[pos.y * width + pos.x];
|
||||
}
|
||||
|
||||
const uint8_t& operator[](TileCoordsXY pos) const
|
||||
{
|
||||
assert(pos.x >= 0 || pos.y >= 0 || pos.x < width || pos.y < height);
|
||||
return _height[pos.y * width + pos.x];
|
||||
}
|
||||
|
||||
uint8_t* data()
|
||||
{
|
||||
return _height.data();
|
||||
}
|
||||
|
||||
const uint8_t* data() const
|
||||
{
|
||||
return _height.data();
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return _height.size();
|
||||
}
|
||||
};
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
171
src/openrct2/world/map_generator/MapGen.cpp
Normal file
171
src/openrct2/world/map_generator/MapGen.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "MapGen.h"
|
||||
|
||||
#include "../../Context.h"
|
||||
#include "../../GameState.h"
|
||||
#include "../../object/ObjectManager.h"
|
||||
#include "../../util/Util.h"
|
||||
#include "../tile_element/Slope.h"
|
||||
#include "../tile_element/SurfaceElement.h"
|
||||
#include "HeightMap.hpp"
|
||||
#include "PngTerrainGenerator.h"
|
||||
#include "SimplexNoise.h"
|
||||
#include "SurfaceSelection.h"
|
||||
#include "TreePlacement.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
static void generateBlankMap(Settings* settings);
|
||||
|
||||
static void addBeaches(Settings* settings);
|
||||
|
||||
void generate(Settings* settings)
|
||||
{
|
||||
// First, generate the height map
|
||||
switch (settings->algorithm)
|
||||
{
|
||||
case Algorithm::blank:
|
||||
generateBlankMap(settings);
|
||||
break;
|
||||
|
||||
case Algorithm::simplexNoise:
|
||||
generateSimplexMap(settings);
|
||||
break;
|
||||
|
||||
case Algorithm::heightmapImage:
|
||||
GenerateFromHeightmapImage(settings);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add beaches?
|
||||
if (settings->beaches)
|
||||
addBeaches(settings);
|
||||
|
||||
// Place trees?
|
||||
if (settings->trees)
|
||||
placeTrees(settings);
|
||||
}
|
||||
|
||||
void resetSurfaces(Settings* settings)
|
||||
{
|
||||
MapClearAllElements();
|
||||
MapInit(settings->mapSize);
|
||||
|
||||
const auto surfaceTextureId = generateSurfaceTextureId(settings);
|
||||
const auto edgeTextureId = generateEdgeTextureId(settings, surfaceTextureId);
|
||||
|
||||
for (auto y = 1; y < settings->mapSize.y - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < settings->mapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement != nullptr)
|
||||
{
|
||||
surfaceElement->SetSurfaceObjectIndex(surfaceTextureId);
|
||||
surfaceElement->SetEdgeObjectIndex(edgeTextureId);
|
||||
surfaceElement->BaseHeight = settings->heightmapLow;
|
||||
surfaceElement->ClearanceHeight = settings->heightmapLow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void generateBlankMap(Settings* settings)
|
||||
{
|
||||
resetSurfaces(settings);
|
||||
setWaterLevel(settings->waterLevel);
|
||||
}
|
||||
|
||||
static void addBeaches(Settings* settings)
|
||||
{
|
||||
auto beachTextureId = generateBeachTextureId();
|
||||
if (beachTextureId == OBJECT_ENTRY_INDEX_NULL)
|
||||
return;
|
||||
|
||||
// Add sandy beaches
|
||||
const auto& mapSize = settings->mapSize;
|
||||
for (auto y = 1; y < mapSize.y - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < mapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
|
||||
if (surfaceElement != nullptr && surfaceElement->BaseHeight < settings->waterLevel + 6)
|
||||
surfaceElement->SetSurfaceObjectIndex(beachTextureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets each tile's water level to the specified water level if underneath that water level.
|
||||
*/
|
||||
void setWaterLevel(int32_t waterLevel)
|
||||
{
|
||||
auto& gameState = GetGameState();
|
||||
for (int32_t y = 1; y < gameState.MapSize.y - 1; y++)
|
||||
{
|
||||
for (int32_t x = 1; x < gameState.MapSize.x - 1; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement != nullptr && surfaceElement->BaseHeight < waterLevel)
|
||||
surfaceElement->SetWaterHeight(waterLevel * kCoordsZStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the actual game map tiles to the height map.
|
||||
*/
|
||||
void setMapHeight(Settings* settings, const HeightMap& heightMap)
|
||||
{
|
||||
for (auto y = 1; y < heightMap.height / 2 - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < heightMap.width / 2 - 1; x++)
|
||||
{
|
||||
auto heightX = x * 2;
|
||||
auto heightY = y * 2;
|
||||
|
||||
uint8_t q00 = heightMap[{ heightX + 0, heightY + 0 }];
|
||||
uint8_t q01 = heightMap[{ heightX + 0, heightY + 1 }];
|
||||
uint8_t q10 = heightMap[{ heightX + 1, heightY + 0 }];
|
||||
uint8_t q11 = heightMap[{ heightX + 1, heightY + 1 }];
|
||||
|
||||
uint8_t baseHeight = (q00 + q01 + q10 + q11) / 4;
|
||||
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
surfaceElement->BaseHeight = std::max(2, baseHeight * 2);
|
||||
|
||||
// If base height is below water level, lower it to create more natural shorelines
|
||||
if (surfaceElement->BaseHeight >= 4 && surfaceElement->BaseHeight <= settings->waterLevel)
|
||||
surfaceElement->BaseHeight -= 2;
|
||||
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight;
|
||||
|
||||
uint8_t currentSlope = surfaceElement->GetSlope();
|
||||
|
||||
if (q00 > baseHeight)
|
||||
currentSlope |= kTileSlopeSCornerUp;
|
||||
if (q01 > baseHeight)
|
||||
currentSlope |= kTileSlopeWCornerUp;
|
||||
if (q10 > baseHeight)
|
||||
currentSlope |= kTileSlopeECornerUp;
|
||||
if (q11 > baseHeight)
|
||||
currentSlope |= kTileSlopeNCornerUp;
|
||||
|
||||
surfaceElement->SetSlope(currentSlope);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
59
src/openrct2/world/map_generator/MapGen.h
Normal file
59
src/openrct2/world/map_generator/MapGen.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Location.hpp"
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
enum class Algorithm : uint8_t
|
||||
{
|
||||
blank,
|
||||
simplexNoise,
|
||||
heightmapImage,
|
||||
};
|
||||
|
||||
struct Settings
|
||||
{
|
||||
// Base
|
||||
Algorithm algorithm = Algorithm::blank;
|
||||
TileCoordsXY mapSize{ 150, 150 };
|
||||
int32_t waterLevel = 6;
|
||||
int32_t landTexture = 0;
|
||||
int32_t edgeTexture = 0;
|
||||
int32_t heightmapLow = 14;
|
||||
int32_t heightmapHigh = 60;
|
||||
bool smoothTileEdges = true;
|
||||
|
||||
// Features (e.g. tree, rivers, lakes etc.)
|
||||
bool trees = true;
|
||||
int32_t treeToLandRatio = 25;
|
||||
int32_t minTreeAltitude = 10;
|
||||
int32_t maxTreeAltitude = 50;
|
||||
bool beaches = true;
|
||||
|
||||
// Simplex Noise Parameters
|
||||
int32_t simplex_base_freq = 175;
|
||||
int32_t simplex_octaves = 6;
|
||||
|
||||
// Height map settings
|
||||
bool smooth_height_map = true;
|
||||
uint32_t smooth_strength = 1;
|
||||
bool normalize_height = true;
|
||||
};
|
||||
|
||||
class HeightMap;
|
||||
|
||||
void generate(Settings* settings);
|
||||
void resetSurfaces(Settings* settings);
|
||||
void setWaterLevel(int32_t waterLevel);
|
||||
void setMapHeight(Settings* settings, const HeightMap& heightMap);
|
||||
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
319
src/openrct2/world/map_generator/MapHelpers.cpp
Normal file
319
src/openrct2/world/map_generator/MapHelpers.cpp
Normal file
@@ -0,0 +1,319 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "MapHelpers.h"
|
||||
|
||||
#include "../../world/tile_element/Slope.h"
|
||||
#include "../../world/tile_element/SurfaceElement.h"
|
||||
#include "../Map.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
static uint8_t GetBaseHeightOrZero(int32_t x, int32_t y)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
return surfaceElement != nullptr ? surfaceElement->BaseHeight : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not perfect, this still leaves some particular tiles unsmoothed.
|
||||
*/
|
||||
int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b)
|
||||
{
|
||||
int32_t x, y, count, doubleCorner, raisedLand = 0;
|
||||
uint8_t highest, cornerHeights[4];
|
||||
for (y = t; y < b; y++)
|
||||
{
|
||||
for (x = l; x < r; x++)
|
||||
{
|
||||
auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y });
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
surfaceElement->SetSlope(kTileSlopeFlat);
|
||||
|
||||
// Raise to edge height - 2
|
||||
highest = surfaceElement->BaseHeight;
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x - 1, y + 0));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 1, y + 0));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 0, y - 1));
|
||||
highest = std::max(highest, GetBaseHeightOrZero(x + 0, y + 1));
|
||||
if (surfaceElement->BaseHeight < highest - 2)
|
||||
{
|
||||
raisedLand = 1;
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2;
|
||||
}
|
||||
|
||||
// Check corners
|
||||
doubleCorner = -1;
|
||||
cornerHeights[0] = GetBaseHeightOrZero(x - 1, y - 1);
|
||||
cornerHeights[1] = GetBaseHeightOrZero(x + 1, y - 1);
|
||||
cornerHeights[2] = GetBaseHeightOrZero(x + 1, y + 1);
|
||||
cornerHeights[3] = GetBaseHeightOrZero(x - 1, y + 1);
|
||||
highest = surfaceElement->BaseHeight;
|
||||
for (std::size_t i = 0; i < std::size(cornerHeights); i++)
|
||||
highest = std::max(highest, cornerHeights[i]);
|
||||
|
||||
if (highest >= surfaceElement->BaseHeight + 4)
|
||||
{
|
||||
count = 0;
|
||||
int32_t canCompensate = 1;
|
||||
for (std::size_t i = 0; i < std::size(cornerHeights); i++)
|
||||
if (cornerHeights[i] == highest)
|
||||
{
|
||||
count++;
|
||||
|
||||
// Check if surrounding corners aren't too high. The current tile
|
||||
// can't compensate for all the height differences anymore if it has
|
||||
// the extra height slope.
|
||||
int32_t highestOnLowestSide;
|
||||
switch (i)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
highestOnLowestSide = std::max(
|
||||
GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y + 1));
|
||||
break;
|
||||
case 1:
|
||||
highestOnLowestSide = std::max(
|
||||
GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y + 1));
|
||||
break;
|
||||
case 2:
|
||||
highestOnLowestSide = std::max(
|
||||
GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y - 1));
|
||||
break;
|
||||
case 3:
|
||||
highestOnLowestSide = std::max(
|
||||
GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y - 1));
|
||||
break;
|
||||
}
|
||||
|
||||
if (highestOnLowestSide > surfaceElement->BaseHeight)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highestOnLowestSide;
|
||||
raisedLand = 1;
|
||||
canCompensate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 1 && canCompensate)
|
||||
{
|
||||
if (surfaceElement->BaseHeight < highest - 4)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 4;
|
||||
raisedLand = 1;
|
||||
}
|
||||
if (cornerHeights[0] == highest && cornerHeights[2] <= cornerHeights[0] - 4)
|
||||
doubleCorner = 0;
|
||||
else if (cornerHeights[1] == highest && cornerHeights[3] <= cornerHeights[1] - 4)
|
||||
doubleCorner = 1;
|
||||
else if (cornerHeights[2] == highest && cornerHeights[0] <= cornerHeights[2] - 4)
|
||||
doubleCorner = 2;
|
||||
else if (cornerHeights[3] == highest && cornerHeights[1] <= cornerHeights[3] - 4)
|
||||
doubleCorner = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (surfaceElement->BaseHeight < highest - 2)
|
||||
{
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2;
|
||||
raisedLand = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doubleCorner != -1)
|
||||
{
|
||||
uint8_t slope = surfaceElement->GetSlope() | kTileSlopeDiagonalFlag;
|
||||
switch (doubleCorner)
|
||||
{
|
||||
case 0:
|
||||
slope |= kTileSlopeNCornerDown;
|
||||
break;
|
||||
case 1:
|
||||
slope |= kTileSlopeWCornerDown;
|
||||
break;
|
||||
case 2:
|
||||
slope |= kTileSlopeSCornerDown;
|
||||
break;
|
||||
case 3:
|
||||
slope |= kTileSlopeECornerDown;
|
||||
break;
|
||||
}
|
||||
surfaceElement->SetSlope(slope);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t slope = surfaceElement->GetSlope();
|
||||
// Corners
|
||||
auto surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNCornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeWCornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeECornerUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSCornerUp;
|
||||
|
||||
// Sides
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 0 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNESideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 0 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSWSideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y - 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeSESideUp;
|
||||
|
||||
surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y + 1 });
|
||||
if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight)
|
||||
slope |= kTileSlopeNWSideUp;
|
||||
|
||||
// Raise
|
||||
if (slope == kTileSlopeRaisedCornersMask)
|
||||
{
|
||||
slope = kTileSlopeFlat;
|
||||
surfaceElement->BaseHeight = surfaceElement->ClearanceHeight += 2;
|
||||
}
|
||||
surfaceElement->SetSlope(slope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return raisedLand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises the corners based on the height offset of neighbour tiles.
|
||||
* This does not change the base height, unless all corners have been raised.
|
||||
* @returns 0 if no edits were made, 1 otherwise
|
||||
*/
|
||||
int32_t TileSmooth(const TileCoordsXY& tileCoords)
|
||||
{
|
||||
auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords);
|
||||
if (surfaceElement == nullptr)
|
||||
return 0;
|
||||
|
||||
// +-----+-----+-----+
|
||||
// | W | NW | N |
|
||||
// | 2 | 1 | 0 |
|
||||
// +-----+-----+-----+
|
||||
// | SW | _ | NE |
|
||||
// | 4 | | 3 |
|
||||
// +-----+-----+-----+
|
||||
// | S | SE | E |
|
||||
// | 7 | 6 | 5 |
|
||||
// +-----+-----+-----+
|
||||
|
||||
union
|
||||
{
|
||||
int32_t baseheight[8];
|
||||
struct
|
||||
{
|
||||
int32_t N;
|
||||
int32_t NW;
|
||||
int32_t W;
|
||||
int32_t NE;
|
||||
int32_t SW;
|
||||
int32_t E;
|
||||
int32_t SE;
|
||||
int32_t S;
|
||||
};
|
||||
} neighbourHeightOffset = {};
|
||||
|
||||
// Find the neighbour base heights
|
||||
for (int32_t index = 0, y_offset = -1; y_offset <= 1; y_offset++)
|
||||
{
|
||||
for (int32_t x_offset = -1; x_offset <= 1; x_offset++)
|
||||
{
|
||||
// Skip self
|
||||
if (y_offset == 0 && x_offset == 0)
|
||||
continue;
|
||||
|
||||
// Get neighbour height. If the element is not valid (outside of map) assume the same height
|
||||
auto* neighbourSurfaceElement = MapGetSurfaceElementAt(tileCoords + TileCoordsXY{ x_offset, y_offset });
|
||||
neighbourHeightOffset.baseheight[index] = neighbourSurfaceElement != nullptr
|
||||
? neighbourSurfaceElement->BaseHeight
|
||||
: surfaceElement->BaseHeight;
|
||||
|
||||
// Make the height relative to the current surface element
|
||||
neighbourHeightOffset.baseheight[index] -= surfaceElement->BaseHeight;
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count number from the three tiles that is currently higher
|
||||
int8_t thresholdW = std::clamp(neighbourHeightOffset.SW, 0, 1) + std::clamp(neighbourHeightOffset.W, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.NW, 0, 1);
|
||||
int8_t thresholdN = std::clamp(neighbourHeightOffset.NW, 0, 1) + std::clamp(neighbourHeightOffset.N, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.NE, 0, 1);
|
||||
int8_t thresholdE = std::clamp(neighbourHeightOffset.NE, 0, 1) + std::clamp(neighbourHeightOffset.E, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.SE, 0, 1);
|
||||
int8_t thresholdS = std::clamp(neighbourHeightOffset.SE, 0, 1) + std::clamp(neighbourHeightOffset.S, 0, 1)
|
||||
+ std::clamp(neighbourHeightOffset.SW, 0, 1);
|
||||
|
||||
uint8_t slope = kTileSlopeFlat;
|
||||
slope |= (thresholdW >= 1) ? SLOPE_W_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdN >= 1) ? SLOPE_N_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdE >= 1) ? SLOPE_E_THRESHOLD_FLAGS : 0;
|
||||
slope |= (thresholdS >= 1) ? SLOPE_S_THRESHOLD_FLAGS : 0;
|
||||
|
||||
// Set diagonal when three corners (one corner down) have been raised, and the middle one can be raised one more
|
||||
if ((slope == kTileSlopeWCornerDown && neighbourHeightOffset.W >= 4)
|
||||
|| (slope == kTileSlopeSCornerDown && neighbourHeightOffset.S >= 4)
|
||||
|| (slope == kTileSlopeECornerDown && neighbourHeightOffset.E >= 4)
|
||||
|| (slope == kTileSlopeNCornerDown && neighbourHeightOffset.N >= 4))
|
||||
{
|
||||
slope |= kTileSlopeDiagonalFlag;
|
||||
}
|
||||
|
||||
// Check if the calculated slope is the same already
|
||||
uint8_t currentSlope = surfaceElement->GetSlope();
|
||||
if (currentSlope == slope)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((slope & kTileSlopeRaisedCornersMask) == kTileSlopeRaisedCornersMask)
|
||||
{
|
||||
// All corners are raised, raise the entire tile instead.
|
||||
surfaceElement->SetSlope(kTileSlopeFlat);
|
||||
surfaceElement->BaseHeight = (surfaceElement->ClearanceHeight += 2);
|
||||
if (surfaceElement->GetWaterHeight() <= surfaceElement->GetBaseZ())
|
||||
{
|
||||
surfaceElement->SetWaterHeight(0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply the slope to this tile
|
||||
surfaceElement->SetSlope(slope);
|
||||
|
||||
// Set correct clearance height
|
||||
if (slope & kTileSlopeDiagonalFlag)
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 4;
|
||||
else if (slope & kTileSlopeRaisedCornersMask)
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
26
src/openrct2/world/map_generator/MapHelpers.h
Normal file
26
src/openrct2/world/map_generator/MapHelpers.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Location.hpp"
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
enum
|
||||
{
|
||||
SLOPE_S_THRESHOLD_FLAGS = (1 << 0),
|
||||
SLOPE_W_THRESHOLD_FLAGS = (1 << 1),
|
||||
SLOPE_N_THRESHOLD_FLAGS = (1 << 2),
|
||||
SLOPE_E_THRESHOLD_FLAGS = (1 << 3)
|
||||
};
|
||||
|
||||
int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b);
|
||||
int32_t TileSmooth(const TileCoordsXY& tileCoords);
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
263
src/openrct2/world/map_generator/PngTerrainGenerator.cpp
Normal file
263
src/openrct2/world/map_generator/PngTerrainGenerator.cpp
Normal file
@@ -0,0 +1,263 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "PngTerrainGenerator.h"
|
||||
|
||||
#include "../../Context.h"
|
||||
#include "../../Diagnostic.h"
|
||||
#include "../../core/Imaging.h"
|
||||
#include "../../localisation/Formatter.h"
|
||||
#include "../../localisation/StringIds.h"
|
||||
#include "../Map.h"
|
||||
#include "../tile_element/SurfaceElement.h"
|
||||
#include "MapGen.h"
|
||||
#include "MapHelpers.h"
|
||||
#include "SurfaceSelection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
static struct
|
||||
{
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
std::vector<uint8_t> mono_bitmap;
|
||||
} _heightMapData;
|
||||
|
||||
/**
|
||||
* Return the tile coordinate that matches the given pixel of a heightmap
|
||||
*/
|
||||
static TileCoordsXY HeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y)
|
||||
{
|
||||
// The height map does not include the empty tiles around the map, so we add 1.
|
||||
return TileCoordsXY(static_cast<int32_t>(y + 1), static_cast<int32_t>(x + 1));
|
||||
}
|
||||
|
||||
bool LoadHeightmapImage(const utf8* path)
|
||||
{
|
||||
auto format = Imaging::GetImageFormatFromPath(path);
|
||||
if (format == IMAGE_FORMAT::PNG)
|
||||
{
|
||||
// Promote to 32-bit
|
||||
format = IMAGE_FORMAT::PNG_32;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto image = Imaging::ReadFromFile(path, format);
|
||||
auto width = std::min<uint32_t>(image.Width, kMaximumMapSizePractical);
|
||||
auto height = std::min<uint32_t>(image.Height, kMaximumMapSizePractical);
|
||||
if (width != image.Width || height != image.Height)
|
||||
{
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {});
|
||||
}
|
||||
|
||||
// Allocate memory for the height map values, one byte pixel
|
||||
_heightMapData.mono_bitmap.resize(width * height);
|
||||
_heightMapData.width = width;
|
||||
_heightMapData.height = height;
|
||||
|
||||
// Copy average RGB value to mono bitmap
|
||||
constexpr auto numChannels = 4;
|
||||
const auto pitch = image.Stride;
|
||||
const auto pixels = image.Pixels.data();
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
const auto red = pixels[x * numChannels + y * pitch];
|
||||
const auto green = pixels[x * numChannels + y * pitch + 1];
|
||||
const auto blue = pixels[x * numChannels + y * pitch + 2];
|
||||
_heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case IMAGE_FORMAT::BITMAP:
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {});
|
||||
break;
|
||||
case IMAGE_FORMAT::PNG_32:
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {});
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unable to load height map image: %s", e.what());
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the memory used to store the selected height map
|
||||
*/
|
||||
void UnloadHeightmapImage()
|
||||
{
|
||||
_heightMapData.mono_bitmap.clear();
|
||||
_heightMapData.width = 0;
|
||||
_heightMapData.height = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies box blur to the surface N times
|
||||
*/
|
||||
static void SmoothHeightmap(std::vector<uint8_t>& src, int32_t strength)
|
||||
{
|
||||
// Create buffer to store one channel
|
||||
std::vector<uint8_t> dest(src.size());
|
||||
|
||||
for (int32_t i = 0; i < strength; i++)
|
||||
{
|
||||
// Calculate box blur value to all pixels of the surface
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
uint32_t heightSum = 0;
|
||||
|
||||
// Loop over neighbour pixels, all of them have the same weight
|
||||
for (int8_t offsetX = -1; offsetX <= 1; offsetX++)
|
||||
{
|
||||
for (int8_t offsetY = -1; offsetY <= 1; offsetY++)
|
||||
{
|
||||
// Clamp x and y so they stay within the image
|
||||
// This assumes the height map is not tiled, and increases the weight of the edges
|
||||
const int32_t readX = std::clamp<int32_t>(x + offsetX, 0, _heightMapData.width - 1);
|
||||
const int32_t readY = std::clamp<int32_t>(y + offsetY, 0, _heightMapData.height - 1);
|
||||
heightSum += src[readX + readY * _heightMapData.width];
|
||||
}
|
||||
}
|
||||
|
||||
// Take average
|
||||
dest[x + y * _heightMapData.width] = heightSum / 9;
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply the blur to the source pixels
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateFromHeightmapImage(Settings* settings)
|
||||
{
|
||||
Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded");
|
||||
Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same");
|
||||
|
||||
// Make a copy of the original height map that we can edit
|
||||
auto dest = _heightMapData.mono_bitmap;
|
||||
|
||||
// Get technical map size, +2 for the black tiles around the map
|
||||
auto maxWidth = static_cast<int32_t>(_heightMapData.width + 2);
|
||||
auto maxHeight = static_cast<int32_t>(_heightMapData.height + 2);
|
||||
MapInit({ maxHeight, maxWidth });
|
||||
|
||||
if (settings->smooth_height_map)
|
||||
{
|
||||
SmoothHeightmap(dest, settings->smooth_strength);
|
||||
}
|
||||
|
||||
uint8_t maxValue = 255;
|
||||
uint8_t minValue = 0;
|
||||
|
||||
if (settings->normalize_height)
|
||||
{
|
||||
// Get highest and lowest pixel value
|
||||
maxValue = 0;
|
||||
minValue = 0xff;
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
uint8_t value = dest[x + y * _heightMapData.width];
|
||||
maxValue = std::max(maxValue, value);
|
||||
minValue = std::min(minValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (minValue == maxValue)
|
||||
{
|
||||
ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Guard::Assert(maxValue > minValue, "Input range is invalid");
|
||||
Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid");
|
||||
|
||||
const auto surfaceTextureId = generateSurfaceTextureId(settings);
|
||||
const auto edgeTextureId = generateEdgeTextureId(settings, surfaceTextureId);
|
||||
|
||||
const uint8_t rangeIn = maxValue - minValue;
|
||||
const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2;
|
||||
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
// The x and y axis are flipped in the world, so this uses y for x and x for y.
|
||||
auto tileCoords = HeightmapCoordToTileCoordsXY(x, y);
|
||||
auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords);
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
|
||||
// Read value from bitmap, and convert its range
|
||||
uint8_t value = dest[x + y * _heightMapData.width];
|
||||
value = static_cast<uint8_t>(static_cast<float>(value - minValue) / rangeIn * rangeOut)
|
||||
+ (settings->heightmapLow * 2);
|
||||
surfaceElement->BaseHeight = value;
|
||||
|
||||
// Floor to even number
|
||||
surfaceElement->BaseHeight /= 2;
|
||||
surfaceElement->BaseHeight *= 2;
|
||||
surfaceElement->ClearanceHeight = surfaceElement->BaseHeight;
|
||||
|
||||
// Set textures
|
||||
surfaceElement->SetSurfaceObjectIndex(surfaceTextureId);
|
||||
surfaceElement->SetEdgeObjectIndex(edgeTextureId);
|
||||
|
||||
// Set water level
|
||||
if (surfaceElement->BaseHeight < settings->waterLevel)
|
||||
{
|
||||
surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth tile edges
|
||||
if (settings->smoothTileEdges)
|
||||
{
|
||||
// Keep smoothing the entire map until no tiles are changed anymore
|
||||
while (true)
|
||||
{
|
||||
uint32_t numTilesChanged = 0;
|
||||
for (uint32_t y = 0; y < _heightMapData.height; y++)
|
||||
{
|
||||
for (uint32_t x = 0; x < _heightMapData.width; x++)
|
||||
{
|
||||
auto tileCoords = HeightmapCoordToTileCoordsXY(x, y);
|
||||
numTilesChanged += TileSmooth(tileCoords);
|
||||
}
|
||||
}
|
||||
|
||||
if (numTilesChanged == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
@@ -9,15 +9,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Location.hpp"
|
||||
#include "../../core/StringTypes.h"
|
||||
|
||||
enum
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
SLOPE_S_THRESHOLD_FLAGS = (1 << 0),
|
||||
SLOPE_W_THRESHOLD_FLAGS = (1 << 1),
|
||||
SLOPE_N_THRESHOLD_FLAGS = (1 << 2),
|
||||
SLOPE_E_THRESHOLD_FLAGS = (1 << 3)
|
||||
};
|
||||
struct Settings;
|
||||
|
||||
int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b);
|
||||
int32_t TileSmooth(const TileCoordsXY& tileCoords);
|
||||
bool LoadHeightmapImage(const utf8* path);
|
||||
void UnloadHeightmapImage();
|
||||
void GenerateFromHeightmapImage(Settings* settings);
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
227
src/openrct2/world/map_generator/SimplexNoise.cpp
Normal file
227
src/openrct2/world/map_generator/SimplexNoise.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "SimplexNoise.h"
|
||||
|
||||
#include "../../util/Util.h"
|
||||
#include "HeightMap.hpp"
|
||||
#include "MapGen.h"
|
||||
#include "MapHelpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
/**
|
||||
* Simplex Noise Algorithm with Fractional Brownian Motion
|
||||
* Based on:
|
||||
* - https://code.google.com/p/simplexnoise/
|
||||
* - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion
|
||||
*/
|
||||
|
||||
static float Generate(float x, float y);
|
||||
static int32_t FastFloor(float x);
|
||||
static float Grad(int32_t hash, float x, float y);
|
||||
|
||||
static uint8_t perm[512];
|
||||
|
||||
void NoiseRand()
|
||||
{
|
||||
for (auto& i : perm)
|
||||
{
|
||||
i = UtilRand() & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence)
|
||||
{
|
||||
float total = 0.0f;
|
||||
float amplitude = persistence;
|
||||
for (int32_t i = 0; i < octaves; i++)
|
||||
{
|
||||
total += Generate(x * frequency, y * frequency) * amplitude;
|
||||
frequency *= lacunarity;
|
||||
amplitude *= persistence;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static float Generate(float x, float y)
|
||||
{
|
||||
const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0)
|
||||
const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0
|
||||
|
||||
float n0, n1, n2; // Noise contributions from the three corners
|
||||
|
||||
// Skew the input space to determine which simplex cell we're in
|
||||
float s = (x + y) * F2; // Hairy factor for 2D
|
||||
float xs = x + s;
|
||||
float ys = y + s;
|
||||
int32_t i = FastFloor(xs);
|
||||
int32_t j = FastFloor(ys);
|
||||
|
||||
float t = static_cast<float>(i + j) * G2;
|
||||
float X0 = i - t; // Unskew the cell origin back to (x,y) space
|
||||
float Y0 = j - t;
|
||||
float x0 = x - X0; // The x,y distances from the cell origin
|
||||
float y0 = y - Y0;
|
||||
|
||||
// For the 2D case, the simplex shape is an equilateral triangle.
|
||||
// Determine which simplex we are in.
|
||||
int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
|
||||
if (x0 > y0)
|
||||
{
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
} // lower triangle, XY order: (0,0)->(1,0)->(1,1)
|
||||
else
|
||||
{
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
} // upper triangle, YX order: (0,0)->(0,1)->(1,1)
|
||||
|
||||
// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
|
||||
// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
|
||||
// c = (3-sqrt(3))/6
|
||||
|
||||
float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
|
||||
float y1 = y0 - j1 + G2;
|
||||
float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords
|
||||
float y2 = y0 - 1.0f + 2.0f * G2;
|
||||
|
||||
// Wrap the integer indices at 256, to avoid indexing perm[] out of bounds
|
||||
int32_t ii = i % 256;
|
||||
int32_t jj = j % 256;
|
||||
|
||||
// Calculate the contribution from the three corners
|
||||
float t0 = 0.5f - x0 * x0 - y0 * y0;
|
||||
if (t0 < 0.0f)
|
||||
{
|
||||
n0 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t0 *= t0;
|
||||
n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0);
|
||||
}
|
||||
|
||||
float t1 = 0.5f - x1 * x1 - y1 * y1;
|
||||
if (t1 < 0.0f)
|
||||
{
|
||||
n1 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t1 *= t1;
|
||||
n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1);
|
||||
}
|
||||
|
||||
float t2 = 0.5f - x2 * x2 - y2 * y2;
|
||||
if (t2 < 0.0f)
|
||||
{
|
||||
n2 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
t2 *= t2;
|
||||
n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2);
|
||||
}
|
||||
|
||||
// Add contributions from each corner to get the final noise value.
|
||||
// The result is scaled to return values in the interval [-1,1].
|
||||
return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary!
|
||||
}
|
||||
|
||||
static int32_t FastFloor(float x)
|
||||
{
|
||||
return (x > 0) ? (static_cast<int32_t>(x)) : ((static_cast<int32_t>(x)) - 1);
|
||||
}
|
||||
|
||||
static float Grad(int32_t hash, float x, float y)
|
||||
{
|
||||
int32_t h = hash & 7; // Convert low 3 bits of hash code
|
||||
float u = h < 4 ? x : y; // into 8 simple gradient directions,
|
||||
float v = h < 4 ? y : x; // and compute the dot product with (x,y).
|
||||
return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooths the height map.
|
||||
*/
|
||||
static void smoothHeightMap(int32_t iterations, HeightMap& heightMap)
|
||||
{
|
||||
for (auto i = 0; i < iterations; i++)
|
||||
{
|
||||
auto copyHeight = heightMap;
|
||||
for (auto y = 1; y < heightMap.height - 1; y++)
|
||||
{
|
||||
for (auto x = 1; x < heightMap.width - 1; x++)
|
||||
{
|
||||
auto avg = 0;
|
||||
for (auto yy = -1; yy <= 1; yy++)
|
||||
{
|
||||
for (auto xx = -1; xx <= 1; xx++)
|
||||
{
|
||||
avg += copyHeight[{ y + yy, x + xx }];
|
||||
}
|
||||
}
|
||||
avg /= 9;
|
||||
heightMap[{ x, y }] = avg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void generateSimplexNoise(Settings* settings, HeightMap& heightMap)
|
||||
{
|
||||
float freq = settings->simplex_base_freq / 100.0f * (1.0f / heightMap.width);
|
||||
int32_t octaves = settings->simplex_octaves;
|
||||
|
||||
int32_t low = settings->heightmapLow / 2;
|
||||
int32_t high = settings->heightmapHigh / 2 - low;
|
||||
|
||||
NoiseRand();
|
||||
for (int32_t y = 0; y < heightMap.height; y++)
|
||||
{
|
||||
for (int32_t x = 0; x < heightMap.width; x++)
|
||||
{
|
||||
float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f);
|
||||
float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f;
|
||||
|
||||
heightMap[{ x, y }] = low + static_cast<int32_t>(normalisedNoiseValue * high);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void generateSimplexMap(Settings* settings)
|
||||
{
|
||||
resetSurfaces(settings);
|
||||
|
||||
// Create the temporary height map and initialise
|
||||
auto& mapSize = settings->mapSize;
|
||||
auto heightMap = HeightMap(mapSize.x * 2, mapSize.y * 2);
|
||||
|
||||
generateSimplexNoise(settings, heightMap);
|
||||
smoothHeightMap(2 + (UtilRand() % 6), heightMap);
|
||||
|
||||
// Set the game map to the height map
|
||||
setMapHeight(settings, heightMap);
|
||||
|
||||
if (settings->smoothTileEdges)
|
||||
{
|
||||
// Set the tile slopes so that there are no cliffs
|
||||
while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Add the water
|
||||
setWaterLevel(settings->waterLevel);
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
22
src/openrct2/world/map_generator/SimplexNoise.h
Normal file
22
src/openrct2/world/map_generator/SimplexNoise.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
struct Settings;
|
||||
|
||||
void NoiseRand();
|
||||
float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence);
|
||||
|
||||
void generateSimplexMap(Settings* settings);
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
102
src/openrct2/world/map_generator/SurfaceSelection.cpp
Normal file
102
src/openrct2/world/map_generator/SurfaceSelection.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "SurfaceSelection.h"
|
||||
|
||||
#include "../../Context.h"
|
||||
#include "../../object/ObjectEntryManager.h"
|
||||
#include "../../object/ObjectList.h"
|
||||
#include "../../object/ObjectManager.h"
|
||||
#include "../../object/TerrainEdgeObject.h"
|
||||
#include "../../object/TerrainSurfaceObject.h"
|
||||
#include "../../util/Util.h"
|
||||
#include "MapGen.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
// Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock.
|
||||
static constexpr std::string_view kBaseTerrain[] = {
|
||||
"rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown",
|
||||
"rct2.terrain_surface.dirt", "rct2.terrain_surface.ice",
|
||||
};
|
||||
|
||||
ObjectEntryIndex generateSurfaceTextureId(Settings* settings)
|
||||
{
|
||||
auto& objectManager = GetContext()->GetObjectManager();
|
||||
|
||||
const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture);
|
||||
std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : "";
|
||||
|
||||
if (surfaceTexture.empty())
|
||||
{
|
||||
std::vector<std::string_view> availableTerrains;
|
||||
std::copy_if(
|
||||
std::begin(kBaseTerrain), std::end(kBaseTerrain), std::back_inserter(availableTerrains),
|
||||
[&](auto terrain) { return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr; });
|
||||
|
||||
if (availableTerrains.empty())
|
||||
// Fall back to the first available surface texture that is available in the park
|
||||
surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier();
|
||||
else
|
||||
surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()];
|
||||
}
|
||||
|
||||
auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture));
|
||||
return surfaceTextureId;
|
||||
}
|
||||
|
||||
ObjectEntryIndex generateEdgeTextureId(Settings* settings, ObjectEntryIndex surfaceTextureId)
|
||||
{
|
||||
auto& objectManager = GetContext()->GetObjectManager();
|
||||
|
||||
const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture);
|
||||
std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : "";
|
||||
|
||||
if (edgeTexture.empty())
|
||||
{
|
||||
auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId);
|
||||
auto surfaceTexture = surfaceObject->GetIdentifier();
|
||||
|
||||
// Base edge type on surface type
|
||||
if (surfaceTexture == "rct2.terrain_surface.dirt")
|
||||
edgeTexture = "rct2.terrain_edge.wood_red";
|
||||
else if (surfaceTexture == "rct2.terrain_surface.ice")
|
||||
edgeTexture = "rct2.terrain_edge.ice";
|
||||
else
|
||||
edgeTexture = "rct2.terrain_edge.rock";
|
||||
|
||||
// Fall back to the first available edge texture that is available in the park
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr)
|
||||
edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier();
|
||||
}
|
||||
|
||||
auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture));
|
||||
return edgeTextureId;
|
||||
}
|
||||
|
||||
ObjectEntryIndex generateBeachTextureId()
|
||||
{
|
||||
auto& objectManager = GetContext()->GetObjectManager();
|
||||
|
||||
// Figure out what beach texture to use
|
||||
std::vector<std::string_view> availableBeachTextures;
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr)
|
||||
availableBeachTextures.push_back("rct2.terrain_surface.sand");
|
||||
if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr)
|
||||
availableBeachTextures.push_back("rct2.terrain_surface.sand_brown");
|
||||
|
||||
if (availableBeachTextures.empty())
|
||||
return OBJECT_ENTRY_INDEX_NULL;
|
||||
|
||||
std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()];
|
||||
return objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture));
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
21
src/openrct2/world/map_generator/SurfaceSelection.h
Normal file
21
src/openrct2/world/map_generator/SurfaceSelection.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../../object/ObjectTypes.h"
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
struct Settings;
|
||||
|
||||
ObjectEntryIndex generateSurfaceTextureId(Settings* settings);
|
||||
ObjectEntryIndex generateEdgeTextureId(Settings* settings, ObjectEntryIndex surfaceTextureId);
|
||||
ObjectEntryIndex generateBeachTextureId();
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
232
src/openrct2/world/map_generator/TreePlacement.cpp
Normal file
232
src/openrct2/world/map_generator/TreePlacement.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "TreePlacement.h"
|
||||
|
||||
#include "../../GameState.h"
|
||||
#include "../../core/Guard.hpp"
|
||||
#include "../../object/ObjectEntryManager.h"
|
||||
#include "../../object/ObjectList.h"
|
||||
#include "../../object/ObjectManager.h"
|
||||
#include "../../object/SmallSceneryEntry.h"
|
||||
#include "../../object/TerrainSurfaceObject.h"
|
||||
#include "../../util/Util.h"
|
||||
#include "../Map.h"
|
||||
#include "../tile_element/SmallSceneryElement.h"
|
||||
#include "../tile_element/SurfaceElement.h"
|
||||
#include "../tile_element/TileElement.h"
|
||||
#include "MapGen.h"
|
||||
#include "SimplexNoise.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
static constexpr const char* kGrassTrees[] = {
|
||||
// Dark
|
||||
"rct2.scenery_small.tcf", // Caucasian Fir Tree
|
||||
"rct2.scenery_small.trf", // Red Fir Tree
|
||||
"rct2.scenery_small.trf2", // Red Fir Tree
|
||||
"rct2.scenery_small.tsp", // Scots Pine Tree
|
||||
"rct2.scenery_small.tmzp", // Montezuma Pine Tree
|
||||
"rct2.scenery_small.tap", // Aleppo Pine Tree
|
||||
"rct2.scenery_small.tcrp", // Corsican Pine Tree
|
||||
"rct2.scenery_small.tbp", // Black Poplar Tree
|
||||
|
||||
// Light
|
||||
"rct2.scenery_small.tcl", // Cedar of Lebanon Tree
|
||||
"rct2.scenery_small.tel", // European Larch Tree
|
||||
};
|
||||
|
||||
static constexpr const char* kDesertTrees[] = {
|
||||
"rct2.scenery_small.tmp", // Monkey-Puzzle Tree
|
||||
"rct2.scenery_small.thl", // Honey Locust Tree
|
||||
"rct2.scenery_small.th1", // Canary Palm Tree
|
||||
"rct2.scenery_small.th2", // Palm Tree
|
||||
"rct2.scenery_small.tpm", // Palm Tree
|
||||
"rct2.scenery_small.tropt1", // Tree
|
||||
"rct2.scenery_small.tbc", // Cactus
|
||||
"rct2.scenery_small.tsc", // Cactus
|
||||
};
|
||||
|
||||
static constexpr const char* kSnowTrees[] = {
|
||||
"rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree
|
||||
"rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree
|
||||
"rct2.scenery_small.trf3", // Snow-covered Red Fir Tree
|
||||
"rct2.scenery_small.trfs", // Snow-covered Red Fir Tree
|
||||
};
|
||||
|
||||
static void placeTree(ObjectEntryIndex type, const CoordsXY& loc)
|
||||
{
|
||||
auto* sceneryEntry = ObjectManager::GetObjectEntry<SmallSceneryEntry>(type);
|
||||
if (sceneryEntry == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t surfaceZ = TileElementHeight(loc.ToTileCentre());
|
||||
|
||||
auto* sceneryElement = TileElementInsert<SmallSceneryElement>({ loc, surfaceZ }, 0b1111);
|
||||
Guard::Assert(sceneryElement != nullptr);
|
||||
|
||||
sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height);
|
||||
sceneryElement->SetDirection(UtilRand() & 3);
|
||||
sceneryElement->SetEntryIndex(type);
|
||||
sceneryElement->SetAge(0);
|
||||
sceneryElement->SetPrimaryColour(COLOUR_YELLOW);
|
||||
}
|
||||
|
||||
static bool surfaceTakesGrassTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps"
|
||||
|| id == "rct2.terrain_surface.dirt";
|
||||
}
|
||||
|
||||
static bool surfaceTakesSandTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown"
|
||||
|| id == "rct2.terrain_surface.sand_red";
|
||||
}
|
||||
|
||||
static bool surfaceTakesSnowTrees(const TerrainSurfaceObject& surface)
|
||||
{
|
||||
const auto& id = surface.GetIdentifier();
|
||||
return id == "rct2.terrain_surface.ice";
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool TryFindTreeInList(std::string_view id, const T& treeList)
|
||||
{
|
||||
for (size_t j = 0; j < std::size(treeList); j++)
|
||||
{
|
||||
if (treeList[j] == id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on.
|
||||
*/
|
||||
void placeTrees(Settings* settings)
|
||||
{
|
||||
std::vector<int32_t> grassTreeIds;
|
||||
std::vector<int32_t> desertTreeIds;
|
||||
std::vector<int32_t> snowTreeIds;
|
||||
|
||||
for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++)
|
||||
{
|
||||
auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry<SmallSceneryEntry>(i);
|
||||
auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i);
|
||||
|
||||
if (sceneryEntry == nullptr)
|
||||
continue;
|
||||
|
||||
if (TryFindTreeInList(entry->GetIdentifier(), kGrassTrees))
|
||||
{
|
||||
grassTreeIds.push_back(i);
|
||||
}
|
||||
else if (TryFindTreeInList(entry->GetIdentifier(), kDesertTrees))
|
||||
{
|
||||
desertTreeIds.push_back(i);
|
||||
}
|
||||
else if (TryFindTreeInList(entry->GetIdentifier(), kSnowTrees))
|
||||
{
|
||||
snowTreeIds.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Place trees
|
||||
float treeToLandRatio = static_cast<float>(settings->treeToLandRatio) / 100.0f;
|
||||
|
||||
// Randomise simplex noise
|
||||
NoiseRand();
|
||||
|
||||
auto& gameState = GetGameState();
|
||||
for (int32_t y = 1; y < gameState.MapSize.y - 1; y++)
|
||||
{
|
||||
for (int32_t x = 1; x < gameState.MapSize.x - 1; x++)
|
||||
{
|
||||
auto pos = CoordsXY{ x, y } * kCoordsXYStep;
|
||||
auto* surfaceElement = MapGetSurfaceElementAt(pos);
|
||||
if (surfaceElement == nullptr)
|
||||
continue;
|
||||
|
||||
// Don't place on water
|
||||
if (surfaceElement->GetWaterHeight() > 0)
|
||||
continue;
|
||||
|
||||
if (settings->minTreeAltitude > surfaceElement->BaseHeight
|
||||
|| settings->maxTreeAltitude < surfaceElement->BaseHeight)
|
||||
continue;
|
||||
|
||||
// On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn
|
||||
// vegetation
|
||||
float oasisScore = 0.0f;
|
||||
ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL;
|
||||
const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex());
|
||||
if (surfaceTakesSandTrees(surfaceStyleObject))
|
||||
{
|
||||
oasisScore = -0.5f;
|
||||
constexpr auto maxOasisDistance = 4;
|
||||
for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++)
|
||||
{
|
||||
for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++)
|
||||
{
|
||||
// Get map coord, clamped to the edges
|
||||
const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep };
|
||||
auto neighbourPos = pos + offset;
|
||||
neighbourPos.x = std::clamp(
|
||||
neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1));
|
||||
neighbourPos.y = std::clamp(
|
||||
neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1));
|
||||
|
||||
const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos);
|
||||
if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0)
|
||||
{
|
||||
float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY);
|
||||
oasisScore += 0.5f / (maxOasisDistance * distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use tree:land ratio except when near an oasis
|
||||
constexpr static auto randModulo = 0xFFFF;
|
||||
if (static_cast<float>(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore))
|
||||
continue;
|
||||
|
||||
// Use fractal noise to group tiles that are likely to spawn trees together
|
||||
float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f);
|
||||
// Reduces the range to rarely stray further than 0.5 from the mean.
|
||||
float noiseOffset = UtilRandNormalDistributed() * 0.25f;
|
||||
if (noiseValue + oasisScore < noiseOffset)
|
||||
continue;
|
||||
|
||||
if (!grassTreeIds.empty() && surfaceTakesGrassTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()];
|
||||
}
|
||||
else if (!desertTreeIds.empty() && surfaceTakesSandTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()];
|
||||
}
|
||||
else if (!snowTreeIds.empty() && surfaceTakesSnowTrees(surfaceStyleObject))
|
||||
{
|
||||
treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()];
|
||||
}
|
||||
|
||||
if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL)
|
||||
placeTree(treeObjectEntryIndex, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
15
src/openrct2/world/map_generator/TreePlacement.h
Normal file
15
src/openrct2/world/map_generator/TreePlacement.h
Normal file
@@ -0,0 +1,15 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2025 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
namespace OpenRCT2::World::MapGenerator
|
||||
{
|
||||
struct Settings;
|
||||
|
||||
void placeTrees(Settings* settings);
|
||||
} // namespace OpenRCT2::World::MapGenerator
|
||||
Reference in New Issue
Block a user