From d8fd07a5def24bcc3de411a61f501ed7db1b1851 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Wed, 1 May 2024 15:00:08 -0400 Subject: [PATCH 01/26] Rough code for terrain generation --- src/fheroes2/editor/editor_interface.cpp | 185 ++++++++++++++++++++++- src/fheroes2/world/world_regions.cpp | 2 +- 2 files changed, 185 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 22e9ee56e1b..79a4af4c265 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -78,6 +78,7 @@ #include "view_world.h" #include "world.h" #include "world_object_uid.h" +#include namespace { @@ -902,10 +903,192 @@ namespace Interface return result; } + enum + { + TOP = 0, + RIGHT = 1, + BOTTOM = 2, + LEFT = 3, + TOP_LEFT = 4, + TOP_RIGHT = 5, + BOTTOM_RIGHT = 6, + BOTTOM_LEFT = 7, + }; + + std::vector GetDirectionOffsets( const int width ) + { + std::vector offsets( 8 ); + offsets[TOP] = -width; + offsets[RIGHT] = 1; + offsets[BOTTOM] = width; + offsets[LEFT] = -1; + offsets[TOP_LEFT] = -width - 1; + offsets[TOP_RIGHT] = -width + 1; + offsets[BOTTOM_RIGHT] = width + 1; + offsets[BOTTOM_LEFT] = width - 1; + return offsets; + } + + uint16_t GetDirectionBitmask( uint8_t direction, bool reflect = false ) + { + return 1 << ( reflect ? ( direction + 4 ) % 8 : direction ); + } + + int ConvertExtendedIndex( int index, uint32_t width ) + { + const uint32_t originalWidth = width - 2; + return ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1; + } + + void CheckAdjacentTiles( std::vector & rawData, MapRegion & region, uint32_t rawDataWidth, const std::vector & offsets ) + { + const int nodeIndex = region._nodes[region._lastProcessedNode].index; + + for ( uint8_t direction = 0; direction < 8; ++direction ) { + if ( direction > 3 && Rand::Get( 1 ) ) { + break; + } + const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; + MapRegionNode & newTile = rawData[newIndex]; + if ( newTile.passable & GetDirectionBitmask( direction, true ) && newTile.isWater == region._isWater ) { + if ( newTile.type == REGION_NODE_OPEN ) { + newTile.type = region._id; + region._nodes.push_back( newTile ); + } + else if ( newTile.type >= REGION_NODE_BORDER && newTile.type != region._id ) { + region._nodes[region._lastProcessedNode].type = REGION_NODE_BORDER; + region._neighbours.insert( newTile.type ); + } + } + } + } + + void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, MapRegion & region, const std::vector & offsets ) + { + // Process only "open" nodes that exist at the start of the loop and ignore what's added + const size_t nodesEnd = region._nodes.size(); + + while ( region._lastProcessedNode < nodesEnd ) { + CheckAdjacentTiles( rawData, region, rawDataWidth, offsets ); + ++region._lastProcessedNode; + } + } + void EditorInterface::eventViewWorld() { // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. - ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); + // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); + + const int32_t width = world.w(); + const int32_t height = world.h(); + + // TODO: Balanced set up only / Pyramid later + const int playerCount = 2; + const double average = ( width * height ) / ( playerCount + 1 ); + const double radius = sqrt( average / M_PI ); + const int side = static_cast( radius / sqrt( 2 ) ); + + std::vector regionCenters; + regionCenters.push_back( ( width / 2 - 1 ) * height + height / 2 - 1 ); + //regionCenters.push_back( 0 ); + //regionCenters.push_back( width - 1 ); + //regionCenters.push_back( ( width - 1 ) * height ); + //regionCenters.push_back( width * height - 1 ); + regionCenters.push_back( width * side + side ); + regionCenters.push_back( width * ( width - side ) + side ); + regionCenters.push_back( width * side + height - side ); + regionCenters.push_back( width * ( width - side ) + height - side ); + + const uint32_t extendedWidth = width + 2; + std::vector data( extendedWidth * ( height + 2 ) ); + for ( int y = 0; y < height; ++y ) { + const int rowIndex = y * width; + for ( int x = 0; x < width; ++x ) { + const int index = rowIndex + x; + MapRegionNode & node = data[ConvertExtendedIndex( index, extendedWidth )]; + + node.index = index; + node.passable = DIRECTION_ALL; + node.isWater = false; + node.type = REGION_NODE_OPEN; + node.mapObject = 0; + } + } + + size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); + std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; + + for ( const int tileIndex : regionCenters ) { + const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions + mapRegions.emplace_back( regionID, tileIndex, false, averageRegionSize ); + data[ConvertExtendedIndex( tileIndex, extendedWidth )].type = regionID; + } + + // Step 7. Grow all regions one step at the time so they would compete for space + const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); + bool stillRoomToExpand = true; + while ( stillRoomToExpand ) { + stillRoomToExpand = false; + for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { + MapRegion & region = mapRegions[regionID]; + RegionExpansion( data, extendedWidth, region, offsets ); + if ( region._lastProcessedNode != region._nodes.size() ) + stillRoomToExpand = true; + } + } + + std::vector cache( width * height ); + for ( MapRegion & reg : mapRegions ) { + if ( reg._id < REGION_NODE_FOUND ) + continue; + + const int terrainType = 1 << ( reg._id % 9 ); + for ( const MapRegionNode & node : reg._nodes ) { + if ( cache[node.index] >= REGION_NODE_FOUND ) { + break; + } + cache[node.index] = node.type; + + // connect regions through teleports + MapsIndexes exits; + + if ( node.mapObject == MP2::OBJ_STONE_LITHS ) { + exits = world.GetTeleportEndPoints( node.index ); + } + else if ( node.mapObject == MP2::OBJ_WHIRLPOOL ) { + exits = world.GetWhirlpoolEndPoints( node.index ); + } + + for ( const int exitIndex : exits ) { + // neighbours is a set that will force the uniqueness + reg._neighbours.insert( cache[exitIndex] ); + } + world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); + } + + // Fix missing references + for ( uint32_t adjacent : reg._neighbours ) { + mapRegions[adjacent]._neighbours.insert( reg._id ); + } + } + + for ( MapRegion & reg : mapRegions ) { + if ( reg._id < REGION_NODE_FOUND ) + continue; + + const int terrainType = 1 << ( reg._id % 9 ); + for ( const MapRegionNode & node : reg._nodes ) { + if ( node.type == REGION_NODE_BORDER ) { + Maps::setTerrainOnTiles( node.index, node.index, terrainType ); + } + } + } + + for ( const int tileIndex : regionCenters ) { + Maps::updateRoadOnTile( world.GetTiles( tileIndex ), true ); + } + + _redraw |= mapUpdateFlags; } void EditorInterface::mouseCursorAreaClickLeft( const int32_t tileIndex ) diff --git a/src/fheroes2/world/world_regions.cpp b/src/fheroes2/world/world_regions.cpp index 23dfd96c6ca..a4c3b882edd 100644 --- a/src/fheroes2/world/world_regions.cpp +++ b/src/fheroes2/world/world_regions.cpp @@ -335,7 +335,7 @@ void World::ComputeStaticAnalysis() bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; - for ( size_t regionID = REGION_NODE_FOUND; regionID < regionCenters.size(); ++regionID ) { + for ( size_t regionID = REGION_NODE_FOUND; regionID < _regions.size(); ++regionID ) { MapRegion & region = _regions[regionID]; RegionExpansion( data, extendedWidth, region, offsets ); if ( region._lastProcessedNode != region._nodes.size() ) From 0ba9299061cca95b234cc2cc5b4338a1ed899a9c Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Wed, 1 May 2024 19:05:39 -0400 Subject: [PATCH 02/26] Generate circular map design --- src/fheroes2/editor/editor_interface.cpp | 60 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 79a4af4c265..44356820838 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -30,6 +30,9 @@ #include #include +#include +#include + #include "agg_image.h" #include "artifact.h" #include "audio_manager.h" @@ -78,7 +81,6 @@ #include "view_world.h" #include "world.h" #include "world_object_uid.h" -#include namespace { @@ -979,25 +981,44 @@ namespace Interface // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); + const int32_t width = world.w(); const int32_t height = world.h(); + // Step 0. Reset world first + world.generateForEditor( width ); + + // Step 1. Map generator configuration // TODO: Balanced set up only / Pyramid later const int playerCount = 2; - const double average = ( width * height ) / ( playerCount + 1 ); - const double radius = sqrt( average / M_PI ); + + // Aiming for region size to be ~300 tiles in a 200-500 range + const double playerArea = ( width * height ) / static_cast( playerCount + 1 ); + const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; + // const int regionCount = static_cast (( width * height ) / regionSize); + + const double radius = sqrt( playerArea / M_PI ); const int side = static_cast( radius / sqrt( 2 ) ); + auto indexLambda = [width, height]( int x, int y ) { + x = std::max( std::min( x - 1, width ), 0 ); + y = std::max( std::min( y - 1, height ), 0 ); + return x * width + y; + }; + + // Step 2. Determine region layout and placement std::vector regionCenters; - regionCenters.push_back( ( width / 2 - 1 ) * height + height / 2 - 1 ); - //regionCenters.push_back( 0 ); - //regionCenters.push_back( width - 1 ); - //regionCenters.push_back( ( width - 1 ) * height ); - //regionCenters.push_back( width * height - 1 ); - regionCenters.push_back( width * side + side ); - regionCenters.push_back( width * ( width - side ) + side ); - regionCenters.push_back( width * side + height - side ); - regionCenters.push_back( width * ( width - side ) + height - side ); + regionCenters.push_back( indexLambda( width / 2, height / 2 ) ); + + const double distance = std::min( width, height ) * 0.4; + const double startingAngle = Rand::Get( 360 ); + const double offsetAngle = 360.0 / playerCount; + for ( int i = 0; i < playerCount; i++ ) { + const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; + const int x = width / 2 + static_cast( cos( radians ) * distance ); + const int y = height / 2 + static_cast( sin( radians ) * distance ); + regionCenters.push_back( indexLambda( x, y ) ); + } const uint32_t extendedWidth = width + 2; std::vector data( extendedWidth * ( height + 2 ) ); @@ -1042,7 +1063,7 @@ namespace Interface if ( reg._id < REGION_NODE_FOUND ) continue; - const int terrainType = 1 << ( reg._id % 9 ); + const int terrainType = 1 << ( reg._id % 8 ); for ( const MapRegionNode & node : reg._nodes ) { if ( cache[node.index] >= REGION_NODE_FOUND ) { break; @@ -1076,7 +1097,9 @@ namespace Interface if ( reg._id < REGION_NODE_FOUND ) continue; - const int terrainType = 1 << ( reg._id % 9 ); + DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) + + const int terrainType = 1 << ( reg._id % 8 ); for ( const MapRegionNode & node : reg._nodes ) { if ( node.type == REGION_NODE_BORDER ) { Maps::setTerrainOnTiles( node.index, node.index, terrainType ); @@ -1088,6 +1111,15 @@ namespace Interface Maps::updateRoadOnTile( world.GetTiles( tileIndex ), true ); } + // set up region connectors based on frequency settings & border length + // generate road based paths + // place objects avoiding the borders + // + // make sure objects accessible before + // make sure paths are accessible - delete obstacles + // place treasures + // place monsters + _redraw |= mapUpdateFlags; } From 168f01abbe0290eb30fbd5ed8776494b841da67a Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Wed, 1 May 2024 22:30:07 -0400 Subject: [PATCH 03/26] Add a dialog to control map settings --- src/fheroes2/editor/editor_interface.cpp | 64 ++++++++++++++++-------- src/fheroes2/editor/editor_interface.h | 3 ++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 44356820838..0f07c890f3d 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -579,7 +579,16 @@ namespace Interface } else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_SCENARIO_INFORMATION ) ) { // TODO: Make the scenario info editor. - Dialog::GameInfo(); + // Dialog::GameInfo(); + + uint32_t newCount = _playerCount; + if ( Dialog::SelectCount( "Pick player count", 2, 6, newCount ) ) { + _playerCount = newCount; + } + newCount = _regionSizeLimit; + if ( Dialog::SelectCount( "Limit region size", 100, 10000, newCount ) ) { + _regionSizeLimit = newCount; + } } else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_VIEW_WORLD ) ) { eventViewWorld(); @@ -990,34 +999,47 @@ namespace Interface // Step 1. Map generator configuration // TODO: Balanced set up only / Pyramid later - const int playerCount = 2; + const int playerCount = _playerCount; + const int regionSizeLimit = _regionSizeLimit; // Aiming for region size to be ~300 tiles in a 200-500 range - const double playerArea = ( width * height ) / static_cast( playerCount + 1 ); - const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; - // const int regionCount = static_cast (( width * height ) / regionSize); + const int minimumRegionCount = playerCount + 1; + const int expectedRegionCount = ( width * height ) / regionSizeLimit; + //const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; + //const int expectedRegionCount = static_cast (( width * height ) / regionSize); - const double radius = sqrt( playerArea / M_PI ); - const int side = static_cast( radius / sqrt( 2 ) ); + //const double radius = sqrt( playerArea / M_PI ); + //const int side = static_cast( radius / sqrt( 2 ) ); - auto indexLambda = [width, height]( int x, int y ) { - x = std::max( std::min( x - 1, width ), 0 ); - y = std::max( std::min( y - 1, height ), 0 ); + auto mapBoundsCheck = [width, height]( int x, int y ) { + x = std::max( std::min( x, width - 1 ), 0 ); + y = std::max( std::min( y, height - 1 ), 0 ); return x * width + y; }; // Step 2. Determine region layout and placement std::vector regionCenters; - regionCenters.push_back( indexLambda( width / 2, height / 2 ) ); - - const double distance = std::min( width, height ) * 0.4; - const double startingAngle = Rand::Get( 360 ); - const double offsetAngle = 360.0 / playerCount; - for ( int i = 0; i < playerCount; i++ ) { - const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; - const int x = width / 2 + static_cast( cos( radians ) * distance ); - const int y = height / 2 + static_cast( sin( radians ) * distance ); - regionCenters.push_back( indexLambda( x, y ) ); + + const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); + const int innerLayer = std::min( neutralRegionCount, playerCount ); + const int outerLayer = std::max( std::min( neutralRegionCount, innerLayer * 2 ), playerCount ); + + const double outerRadius = 0.2 + ( innerLayer + outerLayer ) / static_cast( expectedRegionCount ); + const double innerRadius = innerLayer == 1 ? 0 : outerRadius / 3; + + const std::vector> mapLayers = { { innerLayer, innerRadius }, { outerLayer, outerRadius } }; + + const double distance = std::max( width, height ) / 2.0; + for ( const auto layer : mapLayers ) { + const double startingAngle = Rand::Get( 360 ); + const double offsetAngle = 360.0 / layer.first; + for ( int i = 0; i < layer.first; i++ ) { + const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; + + const int x = width / 2 + static_cast( cos( radians ) * distance * layer.second ); + const int y = height / 2 + static_cast( sin( radians ) * distance * layer.second ); + regionCenters.push_back( mapBoundsCheck( x, y ) ); + } } const uint32_t extendedWidth = width + 2; @@ -1053,7 +1075,7 @@ namespace Interface for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { MapRegion & region = mapRegions[regionID]; RegionExpansion( data, extendedWidth, region, offsets ); - if ( region._lastProcessedNode != region._nodes.size() ) + if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < regionSizeLimit ) stillRoomToExpand = true; } } diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index a4bb181da5b..3413ab0d53d 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -147,6 +147,9 @@ namespace Interface int32_t _selectedTile{ -1 }; int32_t _tileUnderCursor{ -1 }; + uint32_t _playerCount = 2; + uint32_t _regionSizeLimit = 1000; + std::function _cursorUpdater; fheroes2::HistoryManager _historyManager; From 2b26e6b32e9b1844865d14ba80918984a7e83166 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 01:53:48 -0400 Subject: [PATCH 04/26] Test castle placement --- src/fheroes2/editor/editor_interface.cpp | 102 +++++++++++++++++++++-- 1 file changed, 96 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 0f07c890f3d..90baa558a00 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -990,7 +990,6 @@ namespace Interface // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); - const int32_t width = world.w(); const int32_t height = world.h(); @@ -1005,11 +1004,11 @@ namespace Interface // Aiming for region size to be ~300 tiles in a 200-500 range const int minimumRegionCount = playerCount + 1; const int expectedRegionCount = ( width * height ) / regionSizeLimit; - //const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; - //const int expectedRegionCount = static_cast (( width * height ) / regionSize); + // const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; + // const int expectedRegionCount = static_cast (( width * height ) / regionSize); - //const double radius = sqrt( playerArea / M_PI ); - //const int side = static_cast( radius / sqrt( 2 ) ); + // const double radius = sqrt( playerArea / M_PI ); + // const int side = static_cast( radius / sqrt( 2 ) ); auto mapBoundsCheck = [width, height]( int x, int y ) { x = std::max( std::min( x, width - 1 ), 0 ); @@ -1080,6 +1079,15 @@ namespace Interface } } + auto objectPlacer = [this]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { + const fheroes2::Point tilePos = tile.GetCenter(); + const auto & objectInfo = Maps::getObjectInfo( groupType, type ); + if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) ) { + return setObjectOnTile( tile, groupType, type ); + } + return false; + }; + std::vector cache( width * height ); for ( MapRegion & reg : mapRegions ) { if ( reg._id < REGION_NODE_FOUND ) @@ -1121,16 +1129,98 @@ namespace Interface DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) + int xMin = 0; + int xMax = width; + int yMin = 0; + int yMax = height; + const int terrainType = 1 << ( reg._id % 8 ); for ( const MapRegionNode & node : reg._nodes ) { + const int nodeX = node.index % width; + const int nodeY = node.index / width; + xMin = std::max( xMin, nodeX ); + xMax = std::min( xMax, nodeX ); + yMin = std::max( yMin, nodeY ); + yMax = std::min( yMax, nodeY ); + if ( node.type == REGION_NODE_BORDER ) { Maps::setTerrainOnTiles( node.index, node.index, terrainType ); } } + + const int centerX = ( xMin + xMax ) / 2; + const int centerY = ( yMin + yMax ) / 2; + const int regionX = regionCenters[reg._id - 3] % width; + const int regionY = regionCenters[reg._id - 3] / width; + const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); + const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); + const int color = 0; + + auto & tile = world.GetTiles( castleY * width + castleX ); + fheroes2::Point tilePos = tile.GetCenter(); + + const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); + + const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + + //const bool isX = std::abs( centerX - castleX ) > std::abs( centerY - castleY ); + //while ( !isObjectPlacementAllowed( castleInfo, tilePos ) ) { + // if ( ( isX && castleX == centerX ) || ( !isX && castleY == centerY ) ) { + // break; + // } + // const int diffX = centerX - castleX; + // castleX += ( diffX < 0 ) ? -1 : ( diffX == 0 ) ? 0 : 1; + + // if ( !isX ) { + // const int diffY = centerY - castleY; + // castleY += ( diffY < 0 ) ? -1 : ( diffY == 0 ) ? 0 : 1; + // } + //} + //fheroes2::Point temporary = world.GetTiles( castleY * width + castleX ).GetCenter(); + //if ( !isObjectPlacementAllowed( castleInfo, temporary ) || !isActionObjectAllowed( castleInfo, temporary ) ) { + // castleX = std::min( std::max( castleX, 4 ), width - 4 ); + // castleY = std::min( std::max( castleY, 3 ), height - 3 ); + //} + + if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { + setObjectOnTile( tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + + assert( Maps::getLastObjectUID() > 0 ); + const uint32_t objectId = Maps::getLastObjectUID() - 1; + + Maps::setLastObjectUID( objectId ); + setObjectOnTile( tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + + // By default use random (default) army for the neutral race town/castle. + if ( Color::IndexToColor( color ) == Color::NONE ) { + Maps::setDefaultCastleDefenderArmy( _mapFormat.castleMetadata[Maps::getLastObjectUID()] ); + } + + // Add flags. + assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 ) ) { + return; + } + + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 + 1 ) ) { + return; + } + + world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); + } + + // objectPlacer( tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + // objectPlacer( tile, Maps::ObjectGroup::ADVENTURE_MINES, 42 ); } for ( const int tileIndex : regionCenters ) { - Maps::updateRoadOnTile( world.GetTiles( tileIndex ), true ); + auto & tile = world.GetTiles( tileIndex ); + Maps::updateRoadOnTile( tile, true ); } // set up region connectors based on frequency settings & border length From 7359ad293f71f86400b0e321201342d13c9ce3bb Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 17:46:00 -0400 Subject: [PATCH 05/26] Mine random placement --- src/fheroes2/editor/editor_interface.cpp | 61 ++++++++------ src/fheroes2/gui/ui_map_object.cpp | 101 +++++++++++++++++++++++ src/fheroes2/gui/ui_map_object.h | 1 + 3 files changed, 136 insertions(+), 27 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 90baa558a00..a697f56769a 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -1079,15 +1079,32 @@ namespace Interface } } - auto objectPlacer = [this]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { + auto entranceCheck = []( const fheroes2::Point tilePos ) { + for ( int i = -2; i < 3; i++ ) { + if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { + return false; + } + } + return true; + }; + + auto objectPlacer = [this, entranceCheck]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) ) { - return setObjectOnTile( tile, groupType, type ); + if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) && entranceCheck( tilePos ) ) { + // do not update passabilities after every object + if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { + return false; + } + + Maps::addObjectToMap( _mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); + return true; } return false; }; + fheroes2::ActionCreator action( _historyManager, _mapFormat ); + std::vector cache( width * height ); for ( MapRegion & reg : mapRegions ) { if ( reg._id < REGION_NODE_FOUND ) @@ -1148,13 +1165,11 @@ namespace Interface } } - const int centerX = ( xMin + xMax ) / 2; - const int centerY = ( yMin + yMax ) / 2; const int regionX = regionCenters[reg._id - 3] % width; const int regionY = regionCenters[reg._id - 3] / width; const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); - const int color = 0; + const int color = reg._id % 6; auto & tile = world.GetTiles( castleY * width + castleX ); fheroes2::Point tilePos = tile.GetCenter(); @@ -1164,25 +1179,6 @@ namespace Interface const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - //const bool isX = std::abs( centerX - castleX ) > std::abs( centerY - castleY ); - //while ( !isObjectPlacementAllowed( castleInfo, tilePos ) ) { - // if ( ( isX && castleX == centerX ) || ( !isX && castleY == centerY ) ) { - // break; - // } - // const int diffX = centerX - castleX; - // castleX += ( diffX < 0 ) ? -1 : ( diffX == 0 ) ? 0 : 1; - - // if ( !isX ) { - // const int diffY = centerY - castleY; - // castleY += ( diffY < 0 ) ? -1 : ( diffY == 0 ) ? 0 : 1; - // } - //} - //fheroes2::Point temporary = world.GetTiles( castleY * width + castleX ).GetCenter(); - //if ( !isObjectPlacementAllowed( castleInfo, temporary ) || !isActionObjectAllowed( castleInfo, temporary ) ) { - // castleX = std::min( std::max( castleX, 4 ), width - 4 ); - // castleY = std::min( std::max( castleY, 3 ), height - 3 ); - //} - if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { setObjectOnTile( tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); @@ -1214,8 +1210,17 @@ namespace Interface world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); } - // objectPlacer( tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - // objectPlacer( tile, Maps::ObjectGroup::ADVENTURE_MINES, 42 ); + const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; + for ( const int resource : resoures ) { + for ( int tries = 0; tries < 5; tries++ ) { + const auto & node = Rand::Get( reg._nodes ); + Maps::Tiles & mineTile = world.GetTiles( node.index ); + const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); + if ( node.type != REGION_NODE_BORDER && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { + break; + } + } + } } for ( const int tileIndex : regionCenters ) { @@ -1233,6 +1238,8 @@ namespace Interface // place monsters _redraw |= mapUpdateFlags; + + action.commit(); } void EditorInterface::mouseCursorAreaClickLeft( const int32_t tileIndex ) diff --git a/src/fheroes2/gui/ui_map_object.cpp b/src/fheroes2/gui/ui_map_object.cpp index 5856cb88b24..ac03d379d35 100644 --- a/src/fheroes2/gui/ui_map_object.cpp +++ b/src/fheroes2/gui/ui_map_object.cpp @@ -270,4 +270,105 @@ namespace fheroes2 return 0; } + + constexpr int32_t mineIndexFromGroundType( const int groundType ) + { + switch ( groundType ) { + case Maps::Ground::WATER: + // Logically Water is not allowed but let's do this. + assert( 0 ); + return 0; + case Maps::Ground::GRASS: + return 1; + case Maps::Ground::SNOW: + return 2; + case Maps::Ground::SWAMP: + return 3; + case Maps::Ground::LAVA: + return 4; + case Maps::Ground::DESERT: + return 5; + case Maps::Ground::DIRT: + return 6; + case Maps::Ground::WASTELAND: + return 7; + case Maps::Ground::BEACH: + return 0; + default: + // Have you added a new ground? Add the logic above! + assert( 0 ); + break; + } + return 0; + } + + constexpr int32_t sawmillIndexFromGroundType( const int groundType ) + { + switch ( groundType ) { + case Maps::Ground::WATER: + // Logically Water is not allowed but let's do this. + assert( 0 ); + return 0; + case Maps::Ground::GRASS: + return 0; + case Maps::Ground::SNOW: + return 1; + case Maps::Ground::SWAMP: + return 0; + case Maps::Ground::LAVA: + return 2; + case Maps::Ground::DESERT: + return 3; + case Maps::Ground::DIRT: + return 4; + case Maps::Ground::WASTELAND: + return 5; + case Maps::Ground::BEACH: + return 3; + default: + // Have you added a new ground? Add the logic above! + assert( 0 ); + break; + } + return 0; + } + + int32_t getMineObjectInfoId( const int resource, const int groundType ) + { + // 8 terrain and 5 resources + // 2 abandoned mines: grass & dirt + // Sawmills for different terrains: Grass/Swamp, Snow, Lava, Desert, Dirt, Wasteland. + // 2 alchemists labs: regular and snow + + // if you add new mine type update this logic! + assert( Maps::getObjectsByGroup( Maps::ObjectGroup::ADVENTURE_MINES ).size() == 50 ); + + int groundIndex = mineIndexFromGroundType( groundType ); + + switch ( resource ) { + case Resource::ORE: + return groundIndex * 5; + case Resource::SULFUR: + return groundIndex * 5 + 1; + case Resource::CRYSTAL: + return groundIndex * 5 + 2; + case Resource::GEMS: + return groundIndex * 5 + 3; + case Resource::GOLD: + return groundIndex * 5 + 4; + case Resource::WOOD: + return 5 * 8 + 2 + sawmillIndexFromGroundType( groundType ); + case Resource::MERCURY: + return groundType == Maps::Ground::SNOW ? 49 : 48; + case Resource::UNKNOWN: + // must be an abandoned mine + return groundType == Maps::Ground::GRASS ? 40 : 41; + default: + // Have you added a new resource type?! + assert( 0 ); + break; + } + + return 0; + } } diff --git a/src/fheroes2/gui/ui_map_object.h b/src/fheroes2/gui/ui_map_object.h index 0657aef8f86..5800d741509 100644 --- a/src/fheroes2/gui/ui_map_object.h +++ b/src/fheroes2/gui/ui_map_object.h @@ -35,4 +35,5 @@ namespace fheroes2 Sprite generateTownObjectImage( const int townType, const int color, const int groundId ); int32_t getTownBasementId( const int groundType ); + int32_t getMineObjectInfoId( const int resource, const int groundType ); } From 326064852cbe237eb752a1a83003c6cbd4fb647c Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 18:21:41 -0400 Subject: [PATCH 06/26] Move map generation code into its own namespace --- fheroes2-vs2019.vcxproj | 8 +- src/fheroes2/editor/editor_interface.cpp | 319 +---------------- src/fheroes2/maps/map_generator.cpp | 423 +++++++++++++++++++++++ src/fheroes2/maps/map_generator.h | 29 ++ 4 files changed, 462 insertions(+), 317 deletions(-) create mode 100644 src/fheroes2/maps/map_generator.cpp create mode 100644 src/fheroes2/maps/map_generator.h diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index c5b35a0ad7c..8bf51e40055 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -18,6 +18,12 @@ x64 + + + + + + {DD8F214C-C405-4951-8F98-66B969BA8E08} Win32Proj @@ -48,4 +54,4 @@ - + \ No newline at end of file diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index a697f56769a..c5fd1f1d4e4 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -58,6 +58,7 @@ #include "interface_radar.h" #include "localevent.h" #include "map_format_helper.h" +#include "map_generator.h" #include "map_object_info.h" #include "maps.h" #include "maps_tiles.h" @@ -914,328 +915,14 @@ namespace Interface return result; } - enum - { - TOP = 0, - RIGHT = 1, - BOTTOM = 2, - LEFT = 3, - TOP_LEFT = 4, - TOP_RIGHT = 5, - BOTTOM_RIGHT = 6, - BOTTOM_LEFT = 7, - }; - - std::vector GetDirectionOffsets( const int width ) - { - std::vector offsets( 8 ); - offsets[TOP] = -width; - offsets[RIGHT] = 1; - offsets[BOTTOM] = width; - offsets[LEFT] = -1; - offsets[TOP_LEFT] = -width - 1; - offsets[TOP_RIGHT] = -width + 1; - offsets[BOTTOM_RIGHT] = width + 1; - offsets[BOTTOM_LEFT] = width - 1; - return offsets; - } - - uint16_t GetDirectionBitmask( uint8_t direction, bool reflect = false ) - { - return 1 << ( reflect ? ( direction + 4 ) % 8 : direction ); - } - - int ConvertExtendedIndex( int index, uint32_t width ) - { - const uint32_t originalWidth = width - 2; - return ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1; - } - - void CheckAdjacentTiles( std::vector & rawData, MapRegion & region, uint32_t rawDataWidth, const std::vector & offsets ) - { - const int nodeIndex = region._nodes[region._lastProcessedNode].index; - - for ( uint8_t direction = 0; direction < 8; ++direction ) { - if ( direction > 3 && Rand::Get( 1 ) ) { - break; - } - const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; - MapRegionNode & newTile = rawData[newIndex]; - if ( newTile.passable & GetDirectionBitmask( direction, true ) && newTile.isWater == region._isWater ) { - if ( newTile.type == REGION_NODE_OPEN ) { - newTile.type = region._id; - region._nodes.push_back( newTile ); - } - else if ( newTile.type >= REGION_NODE_BORDER && newTile.type != region._id ) { - region._nodes[region._lastProcessedNode].type = REGION_NODE_BORDER; - region._neighbours.insert( newTile.type ); - } - } - } - } - - void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, MapRegion & region, const std::vector & offsets ) - { - // Process only "open" nodes that exist at the start of the loop and ignore what's added - const size_t nodesEnd = region._nodes.size(); - - while ( region._lastProcessedNode < nodesEnd ) { - CheckAdjacentTiles( rawData, region, rawDataWidth, offsets ); - ++region._lastProcessedNode; - } - } - void EditorInterface::eventViewWorld() { // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. - // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); - - const int32_t width = world.w(); - const int32_t height = world.h(); - - // Step 0. Reset world first - world.generateForEditor( width ); - - // Step 1. Map generator configuration - // TODO: Balanced set up only / Pyramid later - const int playerCount = _playerCount; - const int regionSizeLimit = _regionSizeLimit; - - // Aiming for region size to be ~300 tiles in a 200-500 range - const int minimumRegionCount = playerCount + 1; - const int expectedRegionCount = ( width * height ) / regionSizeLimit; - // const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; - // const int expectedRegionCount = static_cast (( width * height ) / regionSize); - - // const double radius = sqrt( playerArea / M_PI ); - // const int side = static_cast( radius / sqrt( 2 ) ); - - auto mapBoundsCheck = [width, height]( int x, int y ) { - x = std::max( std::min( x, width - 1 ), 0 ); - y = std::max( std::min( y, height - 1 ), 0 ); - return x * width + y; - }; - - // Step 2. Determine region layout and placement - std::vector regionCenters; - - const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); - const int innerLayer = std::min( neutralRegionCount, playerCount ); - const int outerLayer = std::max( std::min( neutralRegionCount, innerLayer * 2 ), playerCount ); - - const double outerRadius = 0.2 + ( innerLayer + outerLayer ) / static_cast( expectedRegionCount ); - const double innerRadius = innerLayer == 1 ? 0 : outerRadius / 3; - - const std::vector> mapLayers = { { innerLayer, innerRadius }, { outerLayer, outerRadius } }; - - const double distance = std::max( width, height ) / 2.0; - for ( const auto layer : mapLayers ) { - const double startingAngle = Rand::Get( 360 ); - const double offsetAngle = 360.0 / layer.first; - for ( int i = 0; i < layer.first; i++ ) { - const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; - - const int x = width / 2 + static_cast( cos( radians ) * distance * layer.second ); - const int y = height / 2 + static_cast( sin( radians ) * distance * layer.second ); - regionCenters.push_back( mapBoundsCheck( x, y ) ); - } - } - - const uint32_t extendedWidth = width + 2; - std::vector data( extendedWidth * ( height + 2 ) ); - for ( int y = 0; y < height; ++y ) { - const int rowIndex = y * width; - for ( int x = 0; x < width; ++x ) { - const int index = rowIndex + x; - MapRegionNode & node = data[ConvertExtendedIndex( index, extendedWidth )]; - - node.index = index; - node.passable = DIRECTION_ALL; - node.isWater = false; - node.type = REGION_NODE_OPEN; - node.mapObject = 0; - } - } - - size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); - std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; - - for ( const int tileIndex : regionCenters ) { - const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions - mapRegions.emplace_back( regionID, tileIndex, false, averageRegionSize ); - data[ConvertExtendedIndex( tileIndex, extendedWidth )].type = regionID; - } - - // Step 7. Grow all regions one step at the time so they would compete for space - const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); - bool stillRoomToExpand = true; - while ( stillRoomToExpand ) { - stillRoomToExpand = false; - for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { - MapRegion & region = mapRegions[regionID]; - RegionExpansion( data, extendedWidth, region, offsets ); - if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < regionSizeLimit ) - stillRoomToExpand = true; - } - } - - auto entranceCheck = []( const fheroes2::Point tilePos ) { - for ( int i = -2; i < 3; i++ ) { - if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { - return false; - } - } - return true; - }; - - auto objectPlacer = [this, entranceCheck]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { - const fheroes2::Point tilePos = tile.GetCenter(); - const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) && entranceCheck( tilePos ) ) { - // do not update passabilities after every object - if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { - return false; - } - - Maps::addObjectToMap( _mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); - return true; - } - return false; - }; + // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); fheroes2::ActionCreator action( _historyManager, _mapFormat ); - std::vector cache( width * height ); - for ( MapRegion & reg : mapRegions ) { - if ( reg._id < REGION_NODE_FOUND ) - continue; - - const int terrainType = 1 << ( reg._id % 8 ); - for ( const MapRegionNode & node : reg._nodes ) { - if ( cache[node.index] >= REGION_NODE_FOUND ) { - break; - } - cache[node.index] = node.type; - - // connect regions through teleports - MapsIndexes exits; - - if ( node.mapObject == MP2::OBJ_STONE_LITHS ) { - exits = world.GetTeleportEndPoints( node.index ); - } - else if ( node.mapObject == MP2::OBJ_WHIRLPOOL ) { - exits = world.GetWhirlpoolEndPoints( node.index ); - } - - for ( const int exitIndex : exits ) { - // neighbours is a set that will force the uniqueness - reg._neighbours.insert( cache[exitIndex] ); - } - world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); - } - - // Fix missing references - for ( uint32_t adjacent : reg._neighbours ) { - mapRegions[adjacent]._neighbours.insert( reg._id ); - } - } - - for ( MapRegion & reg : mapRegions ) { - if ( reg._id < REGION_NODE_FOUND ) - continue; - - DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) - - int xMin = 0; - int xMax = width; - int yMin = 0; - int yMax = height; - - const int terrainType = 1 << ( reg._id % 8 ); - for ( const MapRegionNode & node : reg._nodes ) { - const int nodeX = node.index % width; - const int nodeY = node.index / width; - xMin = std::max( xMin, nodeX ); - xMax = std::min( xMax, nodeX ); - yMin = std::max( yMin, nodeY ); - yMax = std::min( yMax, nodeY ); - - if ( node.type == REGION_NODE_BORDER ) { - Maps::setTerrainOnTiles( node.index, node.index, terrainType ); - } - } - - const int regionX = regionCenters[reg._id - 3] % width; - const int regionY = regionCenters[reg._id - 3] / width; - const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); - const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); - const int color = reg._id % 6; - - auto & tile = world.GetTiles( castleY * width + castleX ); - fheroes2::Point tilePos = tile.GetCenter(); - - const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); - - const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - - if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { - setObjectOnTile( tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - - assert( Maps::getLastObjectUID() > 0 ); - const uint32_t objectId = Maps::getLastObjectUID() - 1; - - Maps::setLastObjectUID( objectId ); - setObjectOnTile( tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - - // By default use random (default) army for the neutral race town/castle. - if ( Color::IndexToColor( color ) == Color::NONE ) { - Maps::setDefaultCastleDefenderArmy( _mapFormat.castleMetadata[Maps::getLastObjectUID()] ); - } - - // Add flags. - assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); - Maps::setLastObjectUID( objectId ); - - if ( !setObjectOnTile( world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 ) ) { - return; - } - - Maps::setLastObjectUID( objectId ); - - if ( !setObjectOnTile( world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 + 1 ) ) { - return; - } - - world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); - } - - const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; - for ( const int resource : resoures ) { - for ( int tries = 0; tries < 5; tries++ ) { - const auto & node = Rand::Get( reg._nodes ); - Maps::Tiles & mineTile = world.GetTiles( node.index ); - const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - if ( node.type != REGION_NODE_BORDER && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { - break; - } - } - } - } - - for ( const int tileIndex : regionCenters ) { - auto & tile = world.GetTiles( tileIndex ); - Maps::updateRoadOnTile( tile, true ); - } - - // set up region connectors based on frequency settings & border length - // generate road based paths - // place objects avoiding the borders - // - // make sure objects accessible before - // make sure paths are accessible - delete obstacles - // place treasures - // place monsters + Maps::Generator::generateWorld( _mapFormat, _playerCount, _regionSizeLimit ); _redraw |= mapUpdateFlags; diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp new file mode 100644 index 00000000000..ad57cece0e1 --- /dev/null +++ b/src/fheroes2/maps/map_generator.cpp @@ -0,0 +1,423 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2024 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "rand.h" +#include "map_generator.h" +#include "world_regions.h" +#include "world.h" +#include "maps_tiles_helper.h" +#include "map_object_info.h" +#include "map_format_helper.h" +#include "logging.h" +#include "ui_map_object.h" +#include "race.h" +#include "world_object_uid.h" + +namespace Maps::Generator +{ + enum + { + TOP = 0, + RIGHT = 1, + BOTTOM = 2, + LEFT = 3, + TOP_LEFT = 4, + TOP_RIGHT = 5, + BOTTOM_RIGHT = 6, + BOTTOM_LEFT = 7, + }; + + std::vector GetDirectionOffsets( const int width ) + { + std::vector offsets( 8 ); + offsets[TOP] = -width; + offsets[RIGHT] = 1; + offsets[BOTTOM] = width; + offsets[LEFT] = -1; + offsets[TOP_LEFT] = -width - 1; + offsets[TOP_RIGHT] = -width + 1; + offsets[BOTTOM_RIGHT] = width + 1; + offsets[BOTTOM_LEFT] = width - 1; + return offsets; + } + + uint16_t GetDirectionBitmask( uint8_t direction, bool reflect = false ) + { + return 1 << ( reflect ? ( direction + 4 ) % 8 : direction ); + } + + int ConvertExtendedIndex( int index, uint32_t width ) + { + const uint32_t originalWidth = width - 2; + return ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1; + } + + void CheckAdjacentTiles( std::vector & rawData, MapRegion & region, uint32_t rawDataWidth, const std::vector & offsets ) + { + const int nodeIndex = region._nodes[region._lastProcessedNode].index; + + for ( uint8_t direction = 0; direction < 8; ++direction ) { + if ( direction > 3 && Rand::Get( 1 ) ) { + break; + } + const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; + MapRegionNode & newTile = rawData[newIndex]; + if ( newTile.passable & GetDirectionBitmask( direction, true ) && newTile.isWater == region._isWater ) { + if ( newTile.type == REGION_NODE_OPEN ) { + newTile.type = region._id; + region._nodes.push_back( newTile ); + } + else if ( newTile.type >= REGION_NODE_BORDER && newTile.type != region._id ) { + region._nodes[region._lastProcessedNode].type = REGION_NODE_BORDER; + region._neighbours.insert( newTile.type ); + } + } + } + } + + void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, MapRegion & region, const std::vector & offsets ) + { + // Process only "open" nodes that exist at the start of the loop and ignore what's added + const size_t nodesEnd = region._nodes.size(); + + while ( region._lastProcessedNode < nodesEnd ) { + CheckAdjacentTiles( rawData, region, rawDataWidth, offsets ); + ++region._lastProcessedNode; + } + } + + bool isObjectPlacementAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + // Run through all tile offsets and check that all objects parts can be put on the map. + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER ) { + // Shadow layer parts are ignored. + continue; + } + + if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { + return false; + } + } + + for ( const auto & objectPart : info.topLevelParts ) { + if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { + return false; + } + } + + return true; + } + + bool isActionObjectAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + // Active action object parts must be placed on a tile without any other objects. + // Only ground parts should be checked for this condition. + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { + // Shadow and terrain layer parts are ignored. + continue; + } + + const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; + if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { + return false; + } + + const auto & tile = world.GetTiles( pos.x, pos.y ); + + if ( MP2::isActionObject( tile.GetObject() ) ) { + // An action already exist. We cannot allow to put anything on top of it. + return false; + } + + if ( MP2::isActionObject( objectPart.objectType ) && !Maps::isClearGround( tile ) ) { + // We are trying to place an action object on a tile that has some other objects. + return false; + } + } + + return true; + } + + bool setObjectOnTile( Maps::Map_Format::MapFormat & mapFormat, Maps::Tiles & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) + { + const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); + if ( objectInfo.empty() ) { + // Check your logic as you are trying to insert an empty object! + assert( 0 ); + return false; + } + + if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { + return false; + } + + Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( objectIndex ) ); + + return true; + } + + void generateWorld( Maps::Map_Format::MapFormat & mapFormat, const int playerCount, const int regionSizeLimit ) + { + const int32_t width = world.w(); + const int32_t height = world.h(); + + // Step 0. Reset world first + world.generateForEditor( width ); + + // Step 1. Map generator configuration + // TODO: Balanced set up only / Pyramid later + + // Aiming for region size to be ~300 tiles in a 200-500 range + const int minimumRegionCount = playerCount + 1; + const int expectedRegionCount = ( width * height ) / regionSizeLimit; + // const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; + // const int expectedRegionCount = static_cast (( width * height ) / regionSize); + + // const double radius = sqrt( playerArea / M_PI ); + // const int side = static_cast( radius / sqrt( 2 ) ); + + auto mapBoundsCheck = [width, height]( int x, int y ) { + x = std::max( std::min( x, width - 1 ), 0 ); + y = std::max( std::min( y, height - 1 ), 0 ); + return x * width + y; + }; + + // Step 2. Determine region layout and placement + std::vector regionCenters; + + const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); + const int innerLayer = std::min( neutralRegionCount, playerCount ); + const int outerLayer = std::max( std::min( neutralRegionCount, innerLayer * 2 ), playerCount ); + + const double outerRadius = 0.2 + ( innerLayer + outerLayer ) / static_cast( expectedRegionCount ); + const double innerRadius = innerLayer == 1 ? 0 : outerRadius / 3; + + const std::vector> mapLayers = { { innerLayer, innerRadius }, { outerLayer, outerRadius } }; + + const double distance = std::max( width, height ) / 2.0; + for ( const auto layer : mapLayers ) { + const double startingAngle = Rand::Get( 360 ); + const double offsetAngle = 360.0 / layer.first; + for ( int i = 0; i < layer.first; i++ ) { + const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; + + const int x = width / 2 + static_cast( cos( radians ) * distance * layer.second ); + const int y = height / 2 + static_cast( sin( radians ) * distance * layer.second ); + regionCenters.push_back( mapBoundsCheck( x, y ) ); + } + } + + const uint32_t extendedWidth = width + 2; + std::vector data( extendedWidth * ( height + 2 ) ); + for ( int y = 0; y < height; ++y ) { + const int rowIndex = y * width; + for ( int x = 0; x < width; ++x ) { + const int index = rowIndex + x; + MapRegionNode & node = data[ConvertExtendedIndex( index, extendedWidth )]; + + node.index = index; + node.passable = DIRECTION_ALL; + node.isWater = false; + node.type = REGION_NODE_OPEN; + node.mapObject = 0; + } + } + + size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); + std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; + + for ( const int tileIndex : regionCenters ) { + const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions + mapRegions.emplace_back( regionID, tileIndex, false, averageRegionSize ); + data[ConvertExtendedIndex( tileIndex, extendedWidth )].type = regionID; + } + + // Step 7. Grow all regions one step at the time so they would compete for space + const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); + bool stillRoomToExpand = true; + while ( stillRoomToExpand ) { + stillRoomToExpand = false; + for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { + MapRegion & region = mapRegions[regionID]; + RegionExpansion( data, extendedWidth, region, offsets ); + if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < regionSizeLimit ) + stillRoomToExpand = true; + } + } + + auto entranceCheck = []( const fheroes2::Point tilePos ) { + for ( int i = -2; i < 3; i++ ) { + if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { + return false; + } + } + return true; + }; + + auto objectPlacer = [&mapFormat, entranceCheck]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { + const fheroes2::Point tilePos = tile.GetCenter(); + const auto & objectInfo = Maps::getObjectInfo( groupType, type ); + if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) && entranceCheck( tilePos ) ) { + // do not update passabilities after every object + if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { + return false; + } + + Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); + return true; + } + return false; + }; + + std::vector cache( width * height ); + for ( MapRegion & reg : mapRegions ) { + if ( reg._id < REGION_NODE_FOUND ) + continue; + + const int terrainType = 1 << ( reg._id % 8 ); + for ( const MapRegionNode & node : reg._nodes ) { + if ( cache[node.index] >= REGION_NODE_FOUND ) { + break; + } + cache[node.index] = node.type; + + // connect regions through teleports + MapsIndexes exits; + + if ( node.mapObject == MP2::OBJ_STONE_LITHS ) { + exits = world.GetTeleportEndPoints( node.index ); + } + else if ( node.mapObject == MP2::OBJ_WHIRLPOOL ) { + exits = world.GetWhirlpoolEndPoints( node.index ); + } + + for ( const int exitIndex : exits ) { + // neighbours is a set that will force the uniqueness + reg._neighbours.insert( cache[exitIndex] ); + } + world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); + } + + // Fix missing references + for ( uint32_t adjacent : reg._neighbours ) { + mapRegions[adjacent]._neighbours.insert( reg._id ); + } + } + + for ( MapRegion & reg : mapRegions ) { + if ( reg._id < REGION_NODE_FOUND ) + continue; + + DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) + + int xMin = 0; + int xMax = width; + int yMin = 0; + int yMax = height; + + const int terrainType = 1 << ( reg._id % 8 ); + for ( const MapRegionNode & node : reg._nodes ) { + const int nodeX = node.index % width; + const int nodeY = node.index / width; + xMin = std::max( xMin, nodeX ); + xMax = std::min( xMax, nodeX ); + yMin = std::max( yMin, nodeY ); + yMax = std::min( yMax, nodeY ); + + if ( node.type == REGION_NODE_BORDER ) { + Maps::setTerrainOnTiles( node.index, node.index, terrainType ); + } + } + + const int regionX = regionCenters[reg._id - 3] % width; + const int regionY = regionCenters[reg._id - 3] / width; + const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); + const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); + const int color = reg._id % 6; + + auto & tile = world.GetTiles( castleY * width + castleX ); + fheroes2::Point tilePos = tile.GetCenter(); + + const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); + + const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + + if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { + setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + + assert( Maps::getLastObjectUID() > 0 ); + const uint32_t objectId = Maps::getLastObjectUID() - 1; + + Maps::setLastObjectUID( objectId ); + setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + + // By default use random (default) army for the neutral race town/castle. + if ( Color::IndexToColor( color ) == Color::NONE ) { + Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); + } + + // Add flags. + assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 ) ) { + return; + } + + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 + 1 ) ) { + return; + } + + world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); + } + + const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; + for ( const int resource : resoures ) { + for ( int tries = 0; tries < 5; tries++ ) { + const auto & node = Rand::Get( reg._nodes ); + Maps::Tiles & mineTile = world.GetTiles( node.index ); + const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); + if ( node.type != REGION_NODE_BORDER && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { + break; + } + } + } + } + + for ( const int tileIndex : regionCenters ) { + auto & tile = world.GetTiles( tileIndex ); + Maps::updateRoadOnTile( tile, true ); + } + + // set up region connectors based on frequency settings & border length + // generate road based paths + // place objects avoiding the borders + // + // make sure objects accessible before + // make sure paths are accessible - delete obstacles + // place treasures + // place monsters + } +} \ No newline at end of file diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h new file mode 100644 index 00000000000..addb6aa2f4b --- /dev/null +++ b/src/fheroes2/maps/map_generator.h @@ -0,0 +1,29 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2024 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +#pragma once +#include "map_format_info.h" + +namespace Maps +{ + namespace Generator + { + void generateWorld( Map_Format::MapFormat & mapFormat, const int playerCount, const int regionSizeLimit ); + } +} From 46a8a9b6b1990a8ccaa10360ce6a46395a15e2a6 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 19:58:07 -0400 Subject: [PATCH 07/26] Clean up shared code --- src/fheroes2/editor/editor_interface.cpp | 66 ++------- src/fheroes2/gui/ui_map_object.cpp | 4 +- src/fheroes2/maps/map_generator.cpp | 175 ++++++++++++----------- src/fheroes2/maps/map_generator.h | 9 +- src/fheroes2/maps/map_object_info.cpp | 56 ++++++++ src/fheroes2/maps/map_object_info.h | 3 + 6 files changed, 170 insertions(+), 143 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index c5fd1f1d4e4..1b4fd2c546f 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -107,60 +107,6 @@ namespace return { startPos.x + startPos.y * worldWidth, endPos.x + endPos.y * worldWidth }; } - bool isObjectPlacementAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) - { - // Run through all tile offsets and check that all objects parts can be put on the map. - for ( const auto & objectPart : info.groundLevelParts ) { - if ( objectPart.layerType == Maps::SHADOW_LAYER ) { - // Shadow layer parts are ignored. - continue; - } - - if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { - return false; - } - } - - for ( const auto & objectPart : info.topLevelParts ) { - if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { - return false; - } - } - - return true; - } - - bool isActionObjectAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) - { - // Active action object parts must be placed on a tile without any other objects. - // Only ground parts should be checked for this condition. - for ( const auto & objectPart : info.groundLevelParts ) { - if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { - // Shadow and terrain layer parts are ignored. - continue; - } - - const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; - if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { - return false; - } - - const auto & tile = world.GetTiles( pos.x, pos.y ); - - if ( MP2::isActionObject( tile.GetObject() ) ) { - // An action already exist. We cannot allow to put anything on top of it. - return false; - } - - if ( MP2::isActionObject( objectPart.objectType ) && !Maps::isClearGround( tile ) ) { - // We are trying to place an action object on a tile that has some other objects. - return false; - } - } - - return true; - } - bool isConditionValid( const std::vector & offsets, const fheroes2::Point & mainTilePos, const std::function & condition ) { @@ -918,15 +864,19 @@ namespace Interface void EditorInterface::eventViewWorld() { // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. - // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); + // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); fheroes2::ActionCreator action( _historyManager, _mapFormat ); - Maps::Generator::generateWorld( _mapFormat, _playerCount, _regionSizeLimit ); + Maps::Generator::Configuration rmgConfig; + rmgConfig.playerCount = _playerCount; + rmgConfig.regionSizeLimit = _regionSizeLimit; - _redraw |= mapUpdateFlags; + if ( Maps::Generator::generateWorld( _mapFormat, rmgConfig ) ) { + _redraw |= mapUpdateFlags; - action.commit(); + action.commit(); + } } void EditorInterface::mouseCursorAreaClickLeft( const int32_t tileIndex ) diff --git a/src/fheroes2/gui/ui_map_object.cpp b/src/fheroes2/gui/ui_map_object.cpp index ac03d379d35..c3b94bd00a6 100644 --- a/src/fheroes2/gui/ui_map_object.cpp +++ b/src/fheroes2/gui/ui_map_object.cpp @@ -344,7 +344,7 @@ namespace fheroes2 assert( Maps::getObjectsByGroup( Maps::ObjectGroup::ADVENTURE_MINES ).size() == 50 ); int groundIndex = mineIndexFromGroundType( groundType ); - + switch ( resource ) { case Resource::ORE: return groundIndex * 5; @@ -368,7 +368,7 @@ namespace fheroes2 assert( 0 ); break; } - + return 0; } } diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index ad57cece0e1..014815a052f 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -18,16 +18,16 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include "rand.h" #include "map_generator.h" -#include "world_regions.h" -#include "world.h" -#include "maps_tiles_helper.h" -#include "map_object_info.h" -#include "map_format_helper.h" + #include "logging.h" -#include "ui_map_object.h" +#include "map_format_helper.h" +#include "map_object_info.h" +#include "maps_tiles_helper.h" #include "race.h" +#include "rand.h" +#include "ui_map_object.h" +#include "world.h" #include "world_object_uid.h" namespace Maps::Generator @@ -44,6 +44,54 @@ namespace Maps::Generator BOTTOM_LEFT = 7, }; + enum + { + REGION_NODE_BLOCKED = 0, + REGION_NODE_OPEN = 1, + REGION_NODE_BORDER = 2, + REGION_NODE_FOUND = 3 + }; + + struct Node + { + int index = -1; + uint32_t type = REGION_NODE_BLOCKED; + uint16_t mapObject = 0; + uint16_t passable = 0; + bool isWater = false; + + Node() = default; + explicit Node( int index_ ) + : index( index_ ) + , type( REGION_NODE_OPEN ) + {} + }; + + struct Region + { + public: + uint32_t _id = REGION_NODE_FOUND; + bool _isWater = false; + std::set _neighbours; + std::vector _nodes; + size_t _lastProcessedNode = 0; + + Region() = default; + + Region(int regionIndex, int mapIndex, bool water, size_t expectedSize) + : _id(regionIndex) + , _isWater(water) + { + _nodes.reserve( expectedSize ); + _nodes.emplace_back( mapIndex ); + _nodes[0].type = regionIndex; + } + + size_t getNeighboursCount() const { + return _neighbours.size(); + } + }; + std::vector GetDirectionOffsets( const int width ) { std::vector offsets( 8 ); @@ -69,7 +117,7 @@ namespace Maps::Generator return ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1; } - void CheckAdjacentTiles( std::vector & rawData, MapRegion & region, uint32_t rawDataWidth, const std::vector & offsets ) + void CheckAdjacentTiles( std::vector & rawData, Region & region, uint32_t rawDataWidth, const std::vector & offsets ) { const int nodeIndex = region._nodes[region._lastProcessedNode].index; @@ -78,7 +126,7 @@ namespace Maps::Generator break; } const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; - MapRegionNode & newTile = rawData[newIndex]; + Node & newTile = rawData[newIndex]; if ( newTile.passable & GetDirectionBitmask( direction, true ) && newTile.isWater == region._isWater ) { if ( newTile.type == REGION_NODE_OPEN ) { newTile.type = region._id; @@ -92,7 +140,7 @@ namespace Maps::Generator } } - void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, MapRegion & region, const std::vector & offsets ) + void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, Region & region, const std::vector & offsets ) { // Process only "open" nodes that exist at the start of the loop and ignore what's added const size_t nodesEnd = region._nodes.size(); @@ -103,60 +151,6 @@ namespace Maps::Generator } } - bool isObjectPlacementAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) - { - // Run through all tile offsets and check that all objects parts can be put on the map. - for ( const auto & objectPart : info.groundLevelParts ) { - if ( objectPart.layerType == Maps::SHADOW_LAYER ) { - // Shadow layer parts are ignored. - continue; - } - - if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { - return false; - } - } - - for ( const auto & objectPart : info.topLevelParts ) { - if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { - return false; - } - } - - return true; - } - - bool isActionObjectAllowed( const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos ) - { - // Active action object parts must be placed on a tile without any other objects. - // Only ground parts should be checked for this condition. - for ( const auto & objectPart : info.groundLevelParts ) { - if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { - // Shadow and terrain layer parts are ignored. - continue; - } - - const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; - if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { - return false; - } - - const auto & tile = world.GetTiles( pos.x, pos.y ); - - if ( MP2::isActionObject( tile.GetObject() ) ) { - // An action already exist. We cannot allow to put anything on top of it. - return false; - } - - if ( MP2::isActionObject( objectPart.objectType ) && !Maps::isClearGround( tile ) ) { - // We are trying to place an action object on a tile that has some other objects. - return false; - } - } - - return true; - } - bool setObjectOnTile( Maps::Map_Format::MapFormat & mapFormat, Maps::Tiles & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); @@ -175,20 +169,25 @@ namespace Maps::Generator return true; } - void generateWorld( Maps::Map_Format::MapFormat & mapFormat, const int playerCount, const int regionSizeLimit ) + bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) { + if ( config.playerCount < 2 || config.playerCount > 6 ) { + return false; + } + if ( config.regionSizeLimit < 100 ) { + return false; + } + const int32_t width = world.w(); const int32_t height = world.h(); - // Step 0. Reset world first - world.generateForEditor( width ); - // Step 1. Map generator configuration // TODO: Balanced set up only / Pyramid later + const int playerCount = config.playerCount; // Aiming for region size to be ~300 tiles in a 200-500 range const int minimumRegionCount = playerCount + 1; - const int expectedRegionCount = ( width * height ) / regionSizeLimit; + const int expectedRegionCount = ( width * height ) / config.regionSizeLimit; // const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; // const int expectedRegionCount = static_cast (( width * height ) / regionSize); @@ -227,12 +226,12 @@ namespace Maps::Generator } const uint32_t extendedWidth = width + 2; - std::vector data( extendedWidth * ( height + 2 ) ); + std::vector data( extendedWidth * ( height + 2 ) ); for ( int y = 0; y < height; ++y ) { const int rowIndex = y * width; for ( int x = 0; x < width; ++x ) { const int index = rowIndex + x; - MapRegionNode & node = data[ConvertExtendedIndex( index, extendedWidth )]; + Node & node = data[ConvertExtendedIndex( index, extendedWidth )]; node.index = index; node.passable = DIRECTION_ALL; @@ -243,7 +242,7 @@ namespace Maps::Generator } size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); - std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; + std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; for ( const int tileIndex : regionCenters ) { const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions @@ -251,15 +250,15 @@ namespace Maps::Generator data[ConvertExtendedIndex( tileIndex, extendedWidth )].type = regionID; } - // Step 7. Grow all regions one step at the time so they would compete for space + // Step 3. Grow all regions one step at the time so they would compete for space const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { - MapRegion & region = mapRegions[regionID]; + Region & region = mapRegions[regionID]; RegionExpansion( data, extendedWidth, region, offsets ); - if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < regionSizeLimit ) + if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < config.regionSizeLimit ) stillRoomToExpand = true; } } @@ -288,13 +287,16 @@ namespace Maps::Generator return false; }; + // Step 4. Reset world first + world.generateForEditor( width ); + std::vector cache( width * height ); - for ( MapRegion & reg : mapRegions ) { + for ( Region & reg : mapRegions ) { if ( reg._id < REGION_NODE_FOUND ) continue; const int terrainType = 1 << ( reg._id % 8 ); - for ( const MapRegionNode & node : reg._nodes ) { + for ( const Node & node : reg._nodes ) { if ( cache[node.index] >= REGION_NODE_FOUND ) { break; } @@ -323,7 +325,7 @@ namespace Maps::Generator } } - for ( MapRegion & reg : mapRegions ) { + for ( Region & reg : mapRegions ) { if ( reg._id < REGION_NODE_FOUND ) continue; @@ -335,7 +337,7 @@ namespace Maps::Generator int yMax = height; const int terrainType = 1 << ( reg._id % 8 ); - for ( const MapRegionNode & node : reg._nodes ) { + for ( const Node & node : reg._nodes ) { const int nodeX = node.index % width; const int nodeY = node.index / width; xMin = std::max( xMin, nodeX ); @@ -348,6 +350,10 @@ namespace Maps::Generator } } + if ( config.terrainOnly ) { + continue; + } + const int regionX = regionCenters[reg._id - 3] % width; const int regionY = regionCenters[reg._id - 3] / width; const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); @@ -381,13 +387,13 @@ namespace Maps::Generator Maps::setLastObjectUID( objectId ); if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 ) ) { - return; + return false; } Maps::setLastObjectUID( objectId ); if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 + 1 ) ) { - return; + return false; } world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); @@ -406,6 +412,10 @@ namespace Maps::Generator } } + if ( config.terrainOnly ) { + return true; + } + for ( const int tileIndex : regionCenters ) { auto & tile = world.GetTiles( tileIndex ); Maps::updateRoadOnTile( tile, true ); @@ -418,6 +428,7 @@ namespace Maps::Generator // make sure objects accessible before // make sure paths are accessible - delete obstacles // place treasures - // place monsters + // place monsters + return true; } } \ No newline at end of file diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index addb6aa2f4b..5eec6fc4b34 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -24,6 +24,13 @@ namespace Maps { namespace Generator { - void generateWorld( Map_Format::MapFormat & mapFormat, const int playerCount, const int regionSizeLimit ); + struct Configuration + { + int playerCount = 2; + int regionSizeLimit = 300; + bool terrainOnly = true; + }; + + bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ); } } diff --git a/src/fheroes2/maps/map_object_info.cpp b/src/fheroes2/maps/map_object_info.cpp index f678145c5a4..7340a03a7ec 100644 --- a/src/fheroes2/maps/map_object_info.cpp +++ b/src/fheroes2/maps/map_object_info.cpp @@ -29,8 +29,10 @@ #include #include "artifact.h" +#include "maps_tiles_helper.h" #include "monster.h" #include "resource.h" +#include "world.h" namespace { @@ -4846,6 +4848,60 @@ namespace Maps return MP2::OBJ_NONE; } + bool isObjectPlacementAllowed( const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + // Run through all tile offsets and check that all objects parts can be put on the map. + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER ) { + // Shadow layer parts are ignored. + continue; + } + + if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { + return false; + } + } + + for ( const auto & objectPart : info.topLevelParts ) { + if ( !Maps::isValidAbsPoint( mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y ) ) { + return false; + } + } + + return true; + } + + bool isActionObjectAllowed( const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + // Active action object parts must be placed on a tile without any other objects. + // Only ground parts should be checked for this condition. + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { + // Shadow and terrain layer parts are ignored. + continue; + } + + const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; + if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { + return false; + } + + const auto & tile = world.GetTiles( pos.x, pos.y ); + + if ( MP2::isActionObject( tile.GetObject() ) ) { + // An action already exist. We cannot allow to put anything on top of it. + return false; + } + + if ( MP2::isActionObject( objectPart.objectType ) && !Maps::isClearGround( tile ) ) { + // We are trying to place an action object on a tile that has some other objects. + return false; + } + } + + return true; + } + std::vector getGroundLevelOccupiedTileOffset( const ObjectInfo & info ) { // If this assertion blows up then the object is not formed properly. diff --git a/src/fheroes2/maps/map_object_info.h b/src/fheroes2/maps/map_object_info.h index b1cc660a502..f04b633df5a 100644 --- a/src/fheroes2/maps/map_object_info.h +++ b/src/fheroes2/maps/map_object_info.h @@ -162,6 +162,9 @@ namespace Maps MP2::MapObjectType getObjectTypeByIcn( const MP2::ObjectIcnType icnType, const uint32_t icnIndex ); + bool isObjectPlacementAllowed( const ObjectInfo & info, const fheroes2::Point & mainTilePos ); + bool isActionObjectAllowed( const ObjectInfo & info, const fheroes2::Point & mainTilePos ); + // The function returns tile offsets only for ground level objects located on OBJECT_LAYER and BACKGROUND_LAYER layers. // Objects on other layers do not affect passabilities of tiles so they do not 'occupy' these tiles. std::vector getGroundLevelOccupiedTileOffset( const ObjectInfo & info ); From 992c645cf2971e437c8e1d76db92c57cca7cb42c Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 20:59:22 -0400 Subject: [PATCH 08/26] Detach map region structs from generator --- src/fheroes2/maps/map_generator.cpp | 88 ++++++++++++++--------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 014815a052f..4c9839fcaa5 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -32,6 +32,8 @@ namespace Maps::Generator { + const int COLOR_NONE_FLAG = 6; + enum { TOP = 0, @@ -44,50 +46,53 @@ namespace Maps::Generator BOTTOM_LEFT = 7, }; - enum + enum class NodeType { - REGION_NODE_BLOCKED = 0, - REGION_NODE_OPEN = 1, - REGION_NODE_BORDER = 2, - REGION_NODE_FOUND = 3 + OPEN, + BORDER, + ACTION, + OBSTACLE, + PATH }; struct Node { int index = -1; - uint32_t type = REGION_NODE_BLOCKED; + NodeType type = NodeType::OPEN; + uint32_t region = 0; uint16_t mapObject = 0; uint16_t passable = 0; - bool isWater = false; Node() = default; explicit Node( int index_ ) : index( index_ ) - , type( REGION_NODE_OPEN ) {} }; struct Region { public: - uint32_t _id = REGION_NODE_FOUND; - bool _isWater = false; + uint32_t _id = 0; + int32_t _centerIndex = -1; std::set _neighbours; std::vector _nodes; size_t _lastProcessedNode = 0; + int _colorIndex = COLOR_NONE_FLAG; Region() = default; - Region(int regionIndex, int mapIndex, bool water, size_t expectedSize) - : _id(regionIndex) - , _isWater(water) + Region( int regionIndex, int mapIndex, int playerColor, size_t expectedSize ) + : _id( regionIndex ) + , _centerIndex( mapIndex ) + , _colorIndex( playerColor ) { _nodes.reserve( expectedSize ); _nodes.emplace_back( mapIndex ); - _nodes[0].type = regionIndex; + _nodes[0].region = regionIndex; } - size_t getNeighboursCount() const { + size_t getNeighboursCount() const + { return _neighbours.size(); } }; @@ -127,14 +132,14 @@ namespace Maps::Generator } const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; Node & newTile = rawData[newIndex]; - if ( newTile.passable & GetDirectionBitmask( direction, true ) && newTile.isWater == region._isWater ) { - if ( newTile.type == REGION_NODE_OPEN ) { - newTile.type = region._id; + if ( newTile.passable & GetDirectionBitmask( direction, true ) ) { + if ( newTile.region == 0 ) { + newTile.region = region._id; region._nodes.push_back( newTile ); } - else if ( newTile.type >= REGION_NODE_BORDER && newTile.type != region._id ) { - region._nodes[region._lastProcessedNode].type = REGION_NODE_BORDER; - region._neighbours.insert( newTile.type ); + else if ( newTile.region != region._id ) { + region._nodes[region._lastProcessedNode].type = NodeType::BORDER; + region._neighbours.insert( newTile.region ); } } } @@ -235,19 +240,18 @@ namespace Maps::Generator node.index = index; node.passable = DIRECTION_ALL; - node.isWater = false; - node.type = REGION_NODE_OPEN; + node.type = NodeType::OPEN; node.mapObject = 0; } } size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); - std::vector mapRegions = { { REGION_NODE_BLOCKED, 0, false, 0 }, { REGION_NODE_OPEN, 0, false, 0 }, { REGION_NODE_BORDER, 0, false, 0 } }; + std::vector mapRegions = { { 0, 0, false, 0 } }; for ( const int tileIndex : regionCenters ) { const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions - mapRegions.emplace_back( regionID, tileIndex, false, averageRegionSize ); - data[ConvertExtendedIndex( tileIndex, extendedWidth )].type = regionID; + mapRegions.emplace_back( regionID, tileIndex, COLOR_NONE_FLAG, averageRegionSize ); + data[ConvertExtendedIndex( tileIndex, extendedWidth )].region = regionID; } // Step 3. Grow all regions one step at the time so they would compete for space @@ -255,7 +259,7 @@ namespace Maps::Generator bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; - for ( size_t regionID = REGION_NODE_FOUND; regionID < mapRegions.size(); ++regionID ) { + for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) { Region & region = mapRegions[regionID]; RegionExpansion( data, extendedWidth, region, offsets ); if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < config.regionSizeLimit ) @@ -290,18 +294,12 @@ namespace Maps::Generator // Step 4. Reset world first world.generateForEditor( width ); - std::vector cache( width * height ); for ( Region & reg : mapRegions ) { - if ( reg._id < REGION_NODE_FOUND ) + if ( reg._id == 0 ) continue; const int terrainType = 1 << ( reg._id % 8 ); for ( const Node & node : reg._nodes ) { - if ( cache[node.index] >= REGION_NODE_FOUND ) { - break; - } - cache[node.index] = node.type; - // connect regions through teleports MapsIndexes exits; @@ -314,7 +312,7 @@ namespace Maps::Generator for ( const int exitIndex : exits ) { // neighbours is a set that will force the uniqueness - reg._neighbours.insert( cache[exitIndex] ); + reg._neighbours.insert( node.region ); } world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); } @@ -326,7 +324,7 @@ namespace Maps::Generator } for ( Region & reg : mapRegions ) { - if ( reg._id < REGION_NODE_FOUND ) + if ( reg._id == 0 ) continue; DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) @@ -345,7 +343,7 @@ namespace Maps::Generator yMin = std::max( yMin, nodeY ); yMax = std::min( yMax, nodeY ); - if ( node.type == REGION_NODE_BORDER ) { + if ( node.type == NodeType::BORDER ) { Maps::setTerrainOnTiles( node.index, node.index, terrainType ); } } @@ -354,11 +352,10 @@ namespace Maps::Generator continue; } - const int regionX = regionCenters[reg._id - 3] % width; - const int regionY = regionCenters[reg._id - 3] / width; + const int regionX = reg._centerIndex % width; + const int regionY = reg._centerIndex / width; const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); - const int color = reg._id % 6; auto & tile = world.GetTiles( castleY * width + castleX ); fheroes2::Point tilePos = tile.GetCenter(); @@ -377,8 +374,9 @@ namespace Maps::Generator Maps::setLastObjectUID( objectId ); setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + const uint8_t color = Color::IndexToColor( reg._colorIndex ); // By default use random (default) army for the neutral race town/castle. - if ( Color::IndexToColor( color ) == Color::NONE ) { + if ( color == Color::NONE ) { Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); } @@ -386,17 +384,17 @@ namespace Maps::Generator assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); Maps::setLastObjectUID( objectId ); - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 ) ) { + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, reg._colorIndex * 2 ) ) { return false; } Maps::setLastObjectUID( objectId ); - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, color * 2 + 1 ) ) { + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, reg._colorIndex * 2 + 1 ) ) { return false; } - world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), Color::IndexToColor( color ) ); + world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), color ); } const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; @@ -405,7 +403,7 @@ namespace Maps::Generator const auto & node = Rand::Get( reg._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - if ( node.type != REGION_NODE_BORDER && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { + if ( node.type == NodeType::OPEN && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { break; } } From e766d2678ad02b4931aea857f496c976c07c7618 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 21:00:18 -0400 Subject: [PATCH 09/26] Define debug-only hotkeys --- src/fheroes2/editor/editor_interface.cpp | 42 ++++++++++++++---------- src/fheroes2/game/game_hotkeys.cpp | 7 ++++ src/fheroes2/game/game_hotkeys.h | 6 ++++ src/fheroes2/maps/map_generator.cpp | 6 +++- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 1b4fd2c546f..3feaf0824b8 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -526,8 +526,30 @@ namespace Interface } else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_SCENARIO_INFORMATION ) ) { // TODO: Make the scenario info editor. - // Dialog::GameInfo(); + Dialog::GameInfo(); + } + else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_VIEW_WORLD ) ) { + eventViewWorld(); + } +#if defined( WITH_DEBUG ) + else if ( HotKeyPressEvent( Game::HotKeyEvent::EDITOR_RANDOM_MAP_GENERATION ) ) { + fheroes2::ActionCreator action( _historyManager, _mapFormat ); + + Maps::Generator::Configuration rmgConfig; + rmgConfig.playerCount = _playerCount; + rmgConfig.regionSizeLimit = _regionSizeLimit; + rmgConfig.terrainOnly = false; + if ( Maps::Generator::generateWorld( _mapFormat, rmgConfig ) ) { + _redraw |= mapUpdateFlags; + + action.commit(); + } + else { + _warningMessage.reset( _( "Not able to generate a map with given parameters." ) ); + } + } + else if ( HotKeyPressEvent( Game::HotKeyEvent::EDITOR_RANDOM_MAP_CONFIGURATION ) ) { uint32_t newCount = _playerCount; if ( Dialog::SelectCount( "Pick player count", 2, 6, newCount ) ) { _playerCount = newCount; @@ -537,9 +559,7 @@ namespace Interface _regionSizeLimit = newCount; } } - else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_VIEW_WORLD ) ) { - eventViewWorld(); - } +#endif // map scrolling control else if ( HotKeyPressEvent( Game::HotKeyEvent::WORLD_SCROLL_LEFT ) ) { _gameArea.SetScroll( SCROLL_LEFT ); @@ -864,19 +884,7 @@ namespace Interface void EditorInterface::eventViewWorld() { // TODO: Make proper borders restoration for low height resolutions, like for hide interface mode. - // ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); - - fheroes2::ActionCreator action( _historyManager, _mapFormat ); - - Maps::Generator::Configuration rmgConfig; - rmgConfig.playerCount = _playerCount; - rmgConfig.regionSizeLimit = _regionSizeLimit; - - if ( Maps::Generator::generateWorld( _mapFormat, rmgConfig ) ) { - _redraw |= mapUpdateFlags; - - action.commit(); - } + ViewWorld::ViewWorldWindow( 0, ViewWorldMode::ViewAll, *this ); } void EditorInterface::mouseCursorAreaClickLeft( const int32_t tileIndex ) diff --git a/src/fheroes2/game/game_hotkeys.cpp b/src/fheroes2/game/game_hotkeys.cpp index 9ca574f2226..628497a5ca6 100644 --- a/src/fheroes2/game/game_hotkeys.cpp +++ b/src/fheroes2/game/game_hotkeys.cpp @@ -161,6 +161,13 @@ namespace hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::EDITOR_TO_GAME_MAIN_MENU )] = { Game::HotKeyCategory::EDITOR, gettext_noop( "hotkey|open game main menu" ), fheroes2::Key::KEY_M }; +#if defined( WITH_DEBUG ) + hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::EDITOR_RANDOM_MAP_GENERATION )] + = { Game::HotKeyCategory::WORLD_MAP, gettext_noop( "hotkey|generate random map" ), fheroes2::Key::KEY_F5 }; + hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::EDITOR_RANDOM_MAP_CONFIGURATION )] + = { Game::HotKeyCategory::WORLD_MAP, gettext_noop( "hotkey|configure random map generator" ), fheroes2::Key::KEY_F6 }; +#endif + hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::CAMPAIGN_ROLAND )] = { Game::HotKeyCategory::CAMPAIGN, gettext_noop( "hotkey|roland campaign" ), fheroes2::Key::KEY_1 }; hotKeyEventInfo[hotKeyEventToInt( Game::HotKeyEvent::CAMPAIGN_ARCHIBALD )] diff --git a/src/fheroes2/game/game_hotkeys.h b/src/fheroes2/game/game_hotkeys.h index 747cfbf2a7c..0c882bddeaa 100644 --- a/src/fheroes2/game/game_hotkeys.h +++ b/src/fheroes2/game/game_hotkeys.h @@ -75,6 +75,12 @@ namespace Game EDITOR_REDO_LAST_ACTION, EDITOR_TO_GAME_MAIN_MENU, +#if defined( WITH_DEBUG ) + // This hotkey is only for debug mode as of now. + EDITOR_RANDOM_MAP_GENERATION, + EDITOR_RANDOM_MAP_CONFIGURATION, +#endif + CAMPAIGN_ROLAND, CAMPAIGN_ARCHIBALD, CAMPAIGN_PRICE_OF_LOYALTY, diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 4c9839fcaa5..13b70a0a92f 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -32,7 +32,9 @@ namespace Maps::Generator { + // ObjectInfo ObjctGroup based indicies do not match old objects const int COLOR_NONE_FLAG = 6; + const int RANDOM_CASTLE_INDEX = 12; enum { @@ -127,6 +129,7 @@ namespace Maps::Generator const int nodeIndex = region._nodes[region._lastProcessedNode].index; for ( uint8_t direction = 0; direction < 8; ++direction ) { + // only check diagonal 50% of the time to get more circular distribution; randomness for uneven edges if ( direction > 3 && Rand::Get( 1 ) ) { break; } @@ -394,11 +397,12 @@ namespace Maps::Generator return false; } - world.addCastle( tile.GetIndex(), Race::IndexToRace( 12 ), color ); + world.addCastle( tile.GetIndex(), Race::IndexToRace( RANDOM_CASTLE_INDEX ), color ); } const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; for ( const int resource : resoures ) { + // TODO: do a gradual distribution instead of guesses for ( int tries = 0; tries < 5; tries++ ) { const auto & node = Rand::Get( reg._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); From 74b0a0fd251e2c40e40690631def9dd3cc6b6cc0 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 21:30:18 -0400 Subject: [PATCH 10/26] Fix map border rendering --- src/fheroes2/editor/editor_interface.h | 2 +- src/fheroes2/maps/map_generator.cpp | 87 +++++++++++++------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index 3413ab0d53d..dce47aef245 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -148,7 +148,7 @@ namespace Interface int32_t _tileUnderCursor{ -1 }; uint32_t _playerCount = 2; - uint32_t _regionSizeLimit = 1000; + uint32_t _regionSizeLimit = 500; std::function _cursorUpdater; diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 13b70a0a92f..4871d040dc9 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -60,10 +60,10 @@ namespace Maps::Generator struct Node { int index = -1; - NodeType type = NodeType::OPEN; + NodeType type = NodeType::BORDER; uint32_t region = 0; uint16_t mapObject = 0; - uint16_t passable = 0; + uint16_t passable = DIRECTION_ALL; Node() = default; explicit Node( int index_ ) @@ -78,13 +78,15 @@ namespace Maps::Generator int32_t _centerIndex = -1; std::set _neighbours; std::vector _nodes; + size_t _sizeLimit; size_t _lastProcessedNode = 0; int _colorIndex = COLOR_NONE_FLAG; Region() = default; - Region( int regionIndex, int mapIndex, int playerColor, size_t expectedSize ) + Region( uint32_t regionIndex, int32_t mapIndex, int playerColor, size_t expectedSize ) : _id( regionIndex ) + , _sizeLimit( expectedSize ) , _centerIndex( mapIndex ) , _colorIndex( playerColor ) { @@ -126,9 +128,15 @@ namespace Maps::Generator void CheckAdjacentTiles( std::vector & rawData, Region & region, uint32_t rawDataWidth, const std::vector & offsets ) { - const int nodeIndex = region._nodes[region._lastProcessedNode].index; + Node & previousNode = region._nodes[region._lastProcessedNode]; + const int nodeIndex = previousNode.index; for ( uint8_t direction = 0; direction < 8; ++direction ) { + if ( region._nodes.size() > region._sizeLimit ) { + previousNode.type = NodeType::BORDER; + break; + } + // only check diagonal 50% of the time to get more circular distribution; randomness for uneven edges if ( direction > 3 && Rand::Get( 1 ) ) { break; @@ -136,12 +144,12 @@ namespace Maps::Generator const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; Node & newTile = rawData[newIndex]; if ( newTile.passable & GetDirectionBitmask( direction, true ) ) { - if ( newTile.region == 0 ) { + if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { newTile.region = region._id; region._nodes.push_back( newTile ); } else if ( newTile.region != region._id ) { - region._nodes[region._lastProcessedNode].type = NodeType::BORDER; + previousNode.type = NodeType::BORDER; region._neighbours.insert( newTile.region ); } } @@ -189,6 +197,12 @@ namespace Maps::Generator const int32_t width = world.w(); const int32_t height = world.h(); + auto mapBoundsCheck = [width, height]( int x, int y ) { + x = std::max( std::min( x, width - 1 ), 0 ); + y = std::max( std::min( y, height - 1 ), 0 ); + return x * width + y; + }; + // Step 1. Map generator configuration // TODO: Balanced set up only / Pyramid later const int playerCount = config.playerCount; @@ -202,14 +216,22 @@ namespace Maps::Generator // const double radius = sqrt( playerArea / M_PI ); // const int side = static_cast( radius / sqrt( 2 ) ); - auto mapBoundsCheck = [width, height]( int x, int y ) { - x = std::max( std::min( x, width - 1 ), 0 ); - y = std::max( std::min( y, height - 1 ), 0 ); - return x * width + y; - }; + const uint32_t extendedWidth = width + 2; + std::vector data( extendedWidth * ( height + 2 ) ); + for ( int y = 0; y < height; ++y ) { + const int rowIndex = y * width; + for ( int x = 0; x < width; ++x ) { + const int index = rowIndex + x; + Node & node = data[ConvertExtendedIndex( index, extendedWidth )]; + + node.index = index; + node.type = NodeType::OPEN; + } + } // Step 2. Determine region layout and placement - std::vector regionCenters; + // Insert empty region that represents map borders + std::vector mapRegions = { { 0, 0, COLOR_NONE_FLAG, 0 } }; const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); const int innerLayer = std::min( neutralRegionCount, playerCount ); @@ -229,43 +251,24 @@ namespace Maps::Generator const int x = width / 2 + static_cast( cos( radians ) * distance * layer.second ); const int y = height / 2 + static_cast( sin( radians ) * distance * layer.second ); - regionCenters.push_back( mapBoundsCheck( x, y ) ); - } - } - - const uint32_t extendedWidth = width + 2; - std::vector data( extendedWidth * ( height + 2 ) ); - for ( int y = 0; y < height; ++y ) { - const int rowIndex = y * width; - for ( int x = 0; x < width; ++x ) { - const int index = rowIndex + x; - Node & node = data[ConvertExtendedIndex( index, extendedWidth )]; + const int centerTile = mapBoundsCheck( x, y ); - node.index = index; - node.passable = DIRECTION_ALL; - node.type = NodeType::OPEN; - node.mapObject = 0; + const uint32_t regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions + mapRegions.emplace_back( regionID, centerTile, COLOR_NONE_FLAG, config.regionSizeLimit ); + data[ConvertExtendedIndex( centerTile, extendedWidth )].region = regionID; } } - size_t averageRegionSize = ( static_cast( width ) * height * 2 ) / regionCenters.size(); - std::vector mapRegions = { { 0, 0, false, 0 } }; - - for ( const int tileIndex : regionCenters ) { - const int regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions - mapRegions.emplace_back( regionID, tileIndex, COLOR_NONE_FLAG, averageRegionSize ); - data[ConvertExtendedIndex( tileIndex, extendedWidth )].region = regionID; - } - // Step 3. Grow all regions one step at the time so they would compete for space const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; + // Skip the border region for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) { Region & region = mapRegions[regionID]; RegionExpansion( data, extendedWidth, region, offsets ); - if ( region._lastProcessedNode != region._nodes.size() && region._nodes.size() < config.regionSizeLimit ) + if ( region._lastProcessedNode != region._nodes.size() ) stillRoomToExpand = true; } } @@ -294,7 +297,7 @@ namespace Maps::Generator return false; }; - // Step 4. Reset world first + // Step 4. We're ready to save the result; reset the current world first world.generateForEditor( width ); for ( Region & reg : mapRegions ) { @@ -326,6 +329,7 @@ namespace Maps::Generator } } + // Step 5. Object placement for ( Region & reg : mapRegions ) { if ( reg._id == 0 ) continue; @@ -412,17 +416,14 @@ namespace Maps::Generator } } } + + Maps::updateRoadOnTile( world.GetTiles( reg._centerIndex ), true ); } if ( config.terrainOnly ) { return true; } - for ( const int tileIndex : regionCenters ) { - auto & tile = world.GetTiles( tileIndex ); - Maps::updateRoadOnTile( tile, true ); - } - // set up region connectors based on frequency settings & border length // generate road based paths // place objects avoiding the borders From 7185eccd8d0c3bafd13389a54096fe3b81a007c9 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 23:11:16 -0400 Subject: [PATCH 11/26] Region color placement and general improvements --- src/fheroes2/editor/editor_interface.h | 2 +- src/fheroes2/gui/ui_map_object.cpp | 2 +- src/fheroes2/maps/map_generator.cpp | 227 ++++++++++++++----------- 3 files changed, 127 insertions(+), 104 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index dce47aef245..5739c36453f 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -148,7 +148,7 @@ namespace Interface int32_t _tileUnderCursor{ -1 }; uint32_t _playerCount = 2; - uint32_t _regionSizeLimit = 500; + uint32_t _regionSizeLimit = 600; std::function _cursorUpdater; diff --git a/src/fheroes2/gui/ui_map_object.cpp b/src/fheroes2/gui/ui_map_object.cpp index c3b94bd00a6..c7d49632b1b 100644 --- a/src/fheroes2/gui/ui_map_object.cpp +++ b/src/fheroes2/gui/ui_map_object.cpp @@ -343,7 +343,7 @@ namespace fheroes2 // if you add new mine type update this logic! assert( Maps::getObjectsByGroup( Maps::ObjectGroup::ADVENTURE_MINES ).size() == 50 ); - int groundIndex = mineIndexFromGroundType( groundType ); + const int groundIndex = mineIndexFromGroundType( groundType ); switch ( resource ) { case Resource::ORE: diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 4871d040dc9..56805be857e 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -33,7 +33,7 @@ namespace Maps::Generator { // ObjectInfo ObjctGroup based indicies do not match old objects - const int COLOR_NONE_FLAG = 6; + const int NEUTRAL_COLOR = 6; const int RANDOM_CASTLE_INDEX = 12; enum @@ -80,7 +80,7 @@ namespace Maps::Generator std::vector _nodes; size_t _sizeLimit; size_t _lastProcessedNode = 0; - int _colorIndex = COLOR_NONE_FLAG; + int _colorIndex = NEUTRAL_COLOR; Region() = default; @@ -167,7 +167,7 @@ namespace Maps::Generator } } - bool setObjectOnTile( Maps::Map_Format::MapFormat & mapFormat, Maps::Tiles & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) + bool setObjectOnTile( Map_Format::MapFormat & mapFormat, Tiles & tile, const ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); if ( objectInfo.empty() ) { @@ -185,6 +185,92 @@ namespace Maps::Generator return true; } + bool entranceCheck( const fheroes2::Point tilePos ) + { + for ( int i = -2; i < 3; i++ ) { + if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { + return false; + } + } + return true; + } + + bool objectPlacer( Map_Format::MapFormat & mapFormat, Tiles & tile, ObjectGroup groupType, int32_t type ) + { + const fheroes2::Point tilePos = tile.GetCenter(); + const auto & objectInfo = Maps::getObjectInfo( groupType, type ); + if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) && entranceCheck( tilePos ) ) { + // do not update passabilities after every object + if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { + return false; + } + + Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); + return true; + } + return false; + } + + bool placeCastle( Map_Format::MapFormat & mapFormat, Region & region, int targetX, int targetY ) + { + const int regionX = region._centerIndex % mapFormat.size; + const int regionY = region._centerIndex / mapFormat.size; + const int castleX = std::min( std::max( ( targetX + regionX ) / 2, 4 ), mapFormat.size - 4 ); + const int castleY = std::min( std::max( ( targetY + regionY ) / 2, 3 ), mapFormat.size - 3 ); + + auto & tile = world.GetTiles( castleY * mapFormat.size + castleX ); + fheroes2::Point tilePos = tile.GetCenter(); + + const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); + + const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); + + if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { + setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + + assert( Maps::getLastObjectUID() > 0 ); + const uint32_t objectId = Maps::getLastObjectUID() - 1; + + Maps::setLastObjectUID( objectId ); + setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + + const uint8_t color = Color::IndexToColor( region._colorIndex ); + // By default use random (default) army for the neutral race town/castle. + if ( color == Color::NONE ) { + Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); + } + + // Add flags. + assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 ) ) { + return false; + } + + Maps::setLastObjectUID( objectId ); + + if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 + 1 ) ) { + return false; + } + + world.addCastle( tile.GetIndex(), Race::IndexToRace( RANDOM_CASTLE_INDEX ), color ); + } + return true; + } + + bool placeMine( Map_Format::MapFormat & mapFormat, Region & region, const int resource ) + { + const auto & node = Rand::Get( region._nodes ); + Maps::Tiles & mineTile = world.GetTiles( node.index ); + const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); + if ( node.type == NodeType::OPEN && objectPlacer( mapFormat, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { + return true; + } + return false; + } + bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) { if ( config.playerCount < 2 || config.playerCount > 6 ) { @@ -210,11 +296,6 @@ namespace Maps::Generator // Aiming for region size to be ~300 tiles in a 200-500 range const int minimumRegionCount = playerCount + 1; const int expectedRegionCount = ( width * height ) / config.regionSizeLimit; - // const double regionSize = ( playerArea > 500 ) ? playerArea / 2 : playerArea; - // const int expectedRegionCount = static_cast (( width * height ) / regionSize); - - // const double radius = sqrt( playerArea / M_PI ); - // const int side = static_cast( radius / sqrt( 2 ) ); const uint32_t extendedWidth = width + 2; std::vector data( extendedWidth * ( height + 2 ) ); @@ -231,30 +312,35 @@ namespace Maps::Generator // Step 2. Determine region layout and placement // Insert empty region that represents map borders - std::vector mapRegions = { { 0, 0, COLOR_NONE_FLAG, 0 } }; + std::vector mapRegions = { { 0, 0, NEUTRAL_COLOR, 0 } }; const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); const int innerLayer = std::min( neutralRegionCount, playerCount ); const int outerLayer = std::max( std::min( neutralRegionCount, innerLayer * 2 ), playerCount ); - const double outerRadius = 0.2 + ( innerLayer + outerLayer ) / static_cast( expectedRegionCount ); + const double radius = sqrt( ( innerLayer + outerLayer ) * config.regionSizeLimit / M_PI ); + const double outerRadius = ( ( innerLayer + outerLayer ) > expectedRegionCount ) ? std::max( width, height ) * 0.47 : radius * 0.85; const double innerRadius = innerLayer == 1 ? 0 : outerRadius / 3; const std::vector> mapLayers = { { innerLayer, innerRadius }, { outerLayer, outerRadius } }; - const double distance = std::max( width, height ) / 2.0; - for ( const auto layer : mapLayers ) { + for ( int layer = 0; layer < mapLayers.size(); layer++ ) { + const int regionCount = mapLayers[layer].first; const double startingAngle = Rand::Get( 360 ); - const double offsetAngle = 360.0 / layer.first; - for ( int i = 0; i < layer.first; i++ ) { + const double offsetAngle = 360.0 / regionCount; + for ( int i = 0; i < regionCount; i++ ) { const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180; + const double distance = mapLayers[layer].second; - const int x = width / 2 + static_cast( cos( radians ) * distance * layer.second ); - const int y = height / 2 + static_cast( sin( radians ) * distance * layer.second ); + const int x = width / 2 + static_cast( cos( radians ) * distance ); + const int y = height / 2 + static_cast( sin( radians ) * distance ); const int centerTile = mapBoundsCheck( x, y ); - const uint32_t regionID = static_cast( mapRegions.size() ); // Safe to do as we can't have so many regions - mapRegions.emplace_back( regionID, centerTile, COLOR_NONE_FLAG, config.regionSizeLimit ); + const int factor = regionCount / playerCount; + const int regionColor = ( layer == 1 && i % factor == 0 ) ? i / factor : NEUTRAL_COLOR; + + const uint32_t regionID = static_cast( mapRegions.size() ); + mapRegions.emplace_back( regionID, centerTile, regionColor, config.regionSizeLimit ); data[ConvertExtendedIndex( centerTile, extendedWidth )].region = regionID; } } @@ -273,39 +359,15 @@ namespace Maps::Generator } } - auto entranceCheck = []( const fheroes2::Point tilePos ) { - for ( int i = -2; i < 3; i++ ) { - if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { - return false; - } - } - return true; - }; - - auto objectPlacer = [&mapFormat, entranceCheck]( Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { - const fheroes2::Point tilePos = tile.GetCenter(); - const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( isObjectPlacementAllowed( objectInfo, tilePos ) && isActionObjectAllowed( objectInfo, tilePos ) && entranceCheck( tilePos ) ) { - // do not update passabilities after every object - if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { - return false; - } - - Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); - return true; - } - return false; - }; - // Step 4. We're ready to save the result; reset the current world first world.generateForEditor( width ); - for ( Region & reg : mapRegions ) { - if ( reg._id == 0 ) + for ( Region & region : mapRegions ) { + if ( region._id == 0 ) continue; - const int terrainType = 1 << ( reg._id % 8 ); - for ( const Node & node : reg._nodes ) { + const int terrainType = 1 << ( region._id % 8 ); + for ( const Node & node : region._nodes ) { // connect regions through teleports MapsIndexes exits; @@ -318,31 +380,31 @@ namespace Maps::Generator for ( const int exitIndex : exits ) { // neighbours is a set that will force the uniqueness - reg._neighbours.insert( node.region ); + region._neighbours.insert( node.region ); } world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); } // Fix missing references - for ( uint32_t adjacent : reg._neighbours ) { - mapRegions[adjacent]._neighbours.insert( reg._id ); + for ( uint32_t adjacent : region._neighbours ) { + mapRegions[adjacent]._neighbours.insert( region._id ); } } // Step 5. Object placement - for ( Region & reg : mapRegions ) { - if ( reg._id == 0 ) + for ( Region & region : mapRegions ) { + if ( region._id == 0 ) continue; - DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << reg._id << " size " << reg._nodes.size() << " has " << reg._neighbours.size() << "neighbours" ) + DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << region._id << " size " << region._nodes.size() << " has " << region._neighbours.size() << "neighbours" ) int xMin = 0; int xMax = width; int yMin = 0; int yMax = height; - const int terrainType = 1 << ( reg._id % 8 ); - for ( const Node & node : reg._nodes ) { + const int terrainType = 1 << ( region._id % 8 ); + for ( const Node & node : region._nodes ) { const int nodeX = node.index % width; const int nodeY = node.index / width; xMin = std::max( xMin, nodeX ); @@ -359,65 +421,26 @@ namespace Maps::Generator continue; } - const int regionX = reg._centerIndex % width; - const int regionY = reg._centerIndex / width; - const int castleX = std::min( std::max( ( ( xMin + xMax ) / 2 + regionX ) / 2, 4 ), width - 4 ); - const int castleY = std::min( std::max( ( ( yMin + yMax ) / 2 + regionY ) / 2, 3 ), height - 3 ); - - auto & tile = world.GetTiles( castleY * width + castleX ); - fheroes2::Point tilePos = tile.GetCenter(); - - const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); - - const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - - if ( isObjectPlacementAllowed( basementInfo, tilePos ) && isObjectPlacementAllowed( castleInfo, tilePos ) && isActionObjectAllowed( castleInfo, tilePos ) ) { - setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - - assert( Maps::getLastObjectUID() > 0 ); - const uint32_t objectId = Maps::getLastObjectUID() - 1; - - Maps::setLastObjectUID( objectId ); - setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); - - const uint8_t color = Color::IndexToColor( reg._colorIndex ); - // By default use random (default) army for the neutral race town/castle. - if ( color == Color::NONE ) { - Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); - } - - // Add flags. - assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); - Maps::setLastObjectUID( objectId ); - - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, reg._colorIndex * 2 ) ) { - return false; - } - - Maps::setLastObjectUID( objectId ); - - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, reg._colorIndex * 2 + 1 ) ) { - return false; - } - - world.addCastle( tile.GetIndex(), Race::IndexToRace( RANDOM_CASTLE_INDEX ), color ); + if ( region._colorIndex != NEUTRAL_COLOR && !placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { + // return early if we can't place a starting player castle + return false; + } + else if ( region._nodes.size() > 300 ) { + // place non-mandatory castles in bigger neutral regions + placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); } const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; for ( const int resource : resoures ) { // TODO: do a gradual distribution instead of guesses for ( int tries = 0; tries < 5; tries++ ) { - const auto & node = Rand::Get( reg._nodes ); - Maps::Tiles & mineTile = world.GetTiles( node.index ); - const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - if ( node.type == NodeType::OPEN && objectPlacer( mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { + if ( placeMine( mapFormat, region, resource ) ) { break; } } } - Maps::updateRoadOnTile( world.GetTiles( reg._centerIndex ), true ); + Maps::updateRoadOnTile( world.GetTiles( region._centerIndex ), true ); } if ( config.terrainOnly ) { From 82c8a1bb267cd52b178585a48422b32078f901fb Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 23:40:03 -0400 Subject: [PATCH 12/26] Improve player starting terrain selection --- src/fheroes2/editor/editor_interface.cpp | 1 - src/fheroes2/maps/map_generator.cpp | 35 ++++++++++++------------ src/fheroes2/maps/map_generator.h | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 3feaf0824b8..0c509050956 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -538,7 +538,6 @@ namespace Interface Maps::Generator::Configuration rmgConfig; rmgConfig.playerCount = _playerCount; rmgConfig.regionSizeLimit = _regionSizeLimit; - rmgConfig.terrainOnly = false; if ( Maps::Generator::generateWorld( _mapFormat, rmgConfig ) ) { _redraw |= mapUpdateFlags; diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 56805be857e..7500370bc20 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -35,6 +35,8 @@ namespace Maps::Generator // ObjectInfo ObjctGroup based indicies do not match old objects const int NEUTRAL_COLOR = 6; const int RANDOM_CASTLE_INDEX = 12; + const std::vector playerStartingTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND }; + const std::vector neutralTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND, Ground::BEACH, Ground::SWAMP, Ground::DESERT }; enum { @@ -81,14 +83,16 @@ namespace Maps::Generator size_t _sizeLimit; size_t _lastProcessedNode = 0; int _colorIndex = NEUTRAL_COLOR; + int _groundType = Ground::GRASS; Region() = default; - Region( uint32_t regionIndex, int32_t mapIndex, int playerColor, size_t expectedSize ) + Region( uint32_t regionIndex, int32_t mapIndex, int playerColor, int ground, size_t expectedSize ) : _id( regionIndex ) , _sizeLimit( expectedSize ) , _centerIndex( mapIndex ) , _colorIndex( playerColor ) + , _groundType( ground ) { _nodes.reserve( expectedSize ); _nodes.emplace_back( mapIndex ); @@ -311,8 +315,8 @@ namespace Maps::Generator } // Step 2. Determine region layout and placement - // Insert empty region that represents map borders - std::vector mapRegions = { { 0, 0, NEUTRAL_COLOR, 0 } }; + // Insert empty region that represents water and map edges + std::vector mapRegions = { { 0, 0, NEUTRAL_COLOR, Ground::WATER, 0 } }; const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); const int innerLayer = std::min( neutralRegionCount, playerCount ); @@ -337,10 +341,13 @@ namespace Maps::Generator const int centerTile = mapBoundsCheck( x, y ); const int factor = regionCount / playerCount; - const int regionColor = ( layer == 1 && i % factor == 0 ) ? i / factor : NEUTRAL_COLOR; + const bool isPlayerRegion = layer == 1 && ( i % factor ) == 0; + + const int groundType = isPlayerRegion ? Rand::Get( playerStartingTerrain ) : Rand::Get( neutralTerrain ); + const int regionColor = isPlayerRegion ? i / factor : NEUTRAL_COLOR; const uint32_t regionID = static_cast( mapRegions.size() ); - mapRegions.emplace_back( regionID, centerTile, regionColor, config.regionSizeLimit ); + mapRegions.emplace_back( regionID, centerTile, regionColor, groundType, config.regionSizeLimit ); data[ConvertExtendedIndex( centerTile, extendedWidth )].region = regionID; } } @@ -366,7 +373,6 @@ namespace Maps::Generator if ( region._id == 0 ) continue; - const int terrainType = 1 << ( region._id % 8 ); for ( const Node & node : region._nodes ) { // connect regions through teleports MapsIndexes exits; @@ -382,7 +388,7 @@ namespace Maps::Generator // neighbours is a set that will force the uniqueness region._neighbours.insert( node.region ); } - world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( terrainType, true ), false, false ); + world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( region._groundType, true ), false, false ); } // Fix missing references @@ -403,7 +409,6 @@ namespace Maps::Generator int yMin = 0; int yMax = height; - const int terrainType = 1 << ( region._id % 8 ); for ( const Node & node : region._nodes ) { const int nodeX = node.index % width; const int nodeY = node.index / width; @@ -413,14 +418,10 @@ namespace Maps::Generator yMax = std::min( yMax, nodeY ); if ( node.type == NodeType::BORDER ) { - Maps::setTerrainOnTiles( node.index, node.index, terrainType ); + Maps::setTerrainOnTiles( node.index, node.index, region._groundType ); } } - if ( config.terrainOnly ) { - continue; - } - if ( region._colorIndex != NEUTRAL_COLOR && !placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { // return early if we can't place a starting player castle return false; @@ -430,6 +431,10 @@ namespace Maps::Generator placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); } + if ( config.basicOnly ) { + continue; + } + const std::vector resoures = { Resource::WOOD, Resource::ORE, Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY, Resource::GOLD }; for ( const int resource : resoures ) { // TODO: do a gradual distribution instead of guesses @@ -443,10 +448,6 @@ namespace Maps::Generator Maps::updateRoadOnTile( world.GetTiles( region._centerIndex ), true ); } - if ( config.terrainOnly ) { - return true; - } - // set up region connectors based on frequency settings & border length // generate road based paths // place objects avoiding the borders diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index 5eec6fc4b34..b3d27271ee2 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -28,7 +28,7 @@ namespace Maps { int playerCount = 2; int regionSizeLimit = 300; - bool terrainOnly = true; + bool basicOnly = true; }; bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ); From e0212184c9778c4074c155684586a67a9fa51de4 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 2 May 2024 23:53:23 -0400 Subject: [PATCH 13/26] Code style fixes --- src/fheroes2/gui/ui_map_object.h | 2 +- src/fheroes2/maps/map_generator.cpp | 39 ++++++++-------------------- src/fheroes2/maps/map_generator.h | 19 ++++++-------- src/fheroes2/world/world_regions.cpp | 2 +- 4 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/fheroes2/gui/ui_map_object.h b/src/fheroes2/gui/ui_map_object.h index 5800d741509..8f4a3471972 100644 --- a/src/fheroes2/gui/ui_map_object.h +++ b/src/fheroes2/gui/ui_map_object.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2023 * + * Copyright (C) 2023 - 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 7500370bc20..7e60eb960e7 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -76,12 +76,12 @@ namespace Maps::Generator struct Region { public: - uint32_t _id = 0; + uint32_t _id{}; int32_t _centerIndex = -1; std::set _neighbours; std::vector _nodes; - size_t _sizeLimit; - size_t _lastProcessedNode = 0; + size_t _sizeLimit{}; + size_t _lastProcessedNode{}; int _colorIndex = NEUTRAL_COLOR; int _groundType = Ground::GRASS; @@ -89,8 +89,8 @@ namespace Maps::Generator Region( uint32_t regionIndex, int32_t mapIndex, int playerColor, int ground, size_t expectedSize ) : _id( regionIndex ) - , _sizeLimit( expectedSize ) , _centerIndex( mapIndex ) + , _sizeLimit( expectedSize ) , _colorIndex( playerColor ) , _groundType( ground ) { @@ -127,7 +127,7 @@ namespace Maps::Generator int ConvertExtendedIndex( int index, uint32_t width ) { const uint32_t originalWidth = width - 2; - return ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1; + return static_cast( ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1 ); } void CheckAdjacentTiles( std::vector & rawData, Region & region, uint32_t rawDataWidth, const std::vector & offsets ) @@ -269,10 +269,7 @@ namespace Maps::Generator const auto & node = Rand::Get( region._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - if ( node.type == NodeType::OPEN && objectPlacer( mapFormat, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ) ) { - return true; - } - return false; + return node.type == NodeType::OPEN && objectPlacer( mapFormat, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); } bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) @@ -298,11 +295,11 @@ namespace Maps::Generator const int playerCount = config.playerCount; // Aiming for region size to be ~300 tiles in a 200-500 range - const int minimumRegionCount = playerCount + 1; + // const int minimumRegionCount = playerCount + 1; const int expectedRegionCount = ( width * height ) / config.regionSizeLimit; const uint32_t extendedWidth = width + 2; - std::vector data( extendedWidth * ( height + 2 ) ); + std::vector data( static_cast( extendedWidth ) * ( height + 2 ) ); for ( int y = 0; y < height; ++y ) { const int rowIndex = y * width; for ( int x = 0; x < width; ++x ) { @@ -328,7 +325,7 @@ namespace Maps::Generator const std::vector> mapLayers = { { innerLayer, innerRadius }, { outerLayer, outerRadius } }; - for ( int layer = 0; layer < mapLayers.size(); layer++ ) { + for ( size_t layer = 0; layer < mapLayers.size(); layer++ ) { const int regionCount = mapLayers[layer].first; const double startingAngle = Rand::Get( 360 ); const double offsetAngle = 360.0 / regionCount; @@ -374,20 +371,6 @@ namespace Maps::Generator continue; for ( const Node & node : region._nodes ) { - // connect regions through teleports - MapsIndexes exits; - - if ( node.mapObject == MP2::OBJ_STONE_LITHS ) { - exits = world.GetTeleportEndPoints( node.index ); - } - else if ( node.mapObject == MP2::OBJ_WHIRLPOOL ) { - exits = world.GetWhirlpoolEndPoints( node.index ); - } - - for ( const int exitIndex : exits ) { - // neighbours is a set that will force the uniqueness - region._neighbours.insert( node.region ); - } world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( region._groundType, true ), false, false ); } @@ -426,7 +409,7 @@ namespace Maps::Generator // return early if we can't place a starting player castle return false; } - else if ( region._nodes.size() > 300 ) { + if ( region._nodes.size() > 300 ) { // place non-mandatory castles in bigger neutral regions placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); } diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index b3d27271ee2..33cefa42c13 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -20,17 +20,14 @@ #pragma once #include "map_format_info.h" -namespace Maps +namespace Maps::Generator { - namespace Generator + struct Configuration { - struct Configuration - { - int playerCount = 2; - int regionSizeLimit = 300; - bool basicOnly = true; - }; + uint32_t playerCount = 2; + uint32_t regionSizeLimit = 300; + bool basicOnly = true; + }; - bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ); - } + bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ); } diff --git a/src/fheroes2/world/world_regions.cpp b/src/fheroes2/world/world_regions.cpp index a4c3b882edd..fdf65bdfd53 100644 --- a/src/fheroes2/world/world_regions.cpp +++ b/src/fheroes2/world/world_regions.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2020 - 2023 * + * Copyright (C) 2020 - 2024 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * From 14e8178815a2d389e4726e2f2d8424a46ad2fcf6 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Fri, 3 May 2024 00:10:25 -0400 Subject: [PATCH 14/26] IWYU --- src/fheroes2/editor/editor_interface.cpp | 3 --- src/fheroes2/gui/ui_map_object.cpp | 1 + src/fheroes2/maps/map_generator.cpp | 25 +++++++++++++++++++++--- src/fheroes2/maps/map_generator.h | 6 +++++- src/fheroes2/maps/map_object_info.cpp | 1 + 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 0c509050956..ce6795ec9c8 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -30,9 +30,6 @@ #include #include -#include -#include - #include "agg_image.h" #include "artifact.h" #include "audio_manager.h" diff --git a/src/fheroes2/gui/ui_map_object.cpp b/src/fheroes2/gui/ui_map_object.cpp index c7d49632b1b..8ea95294daf 100644 --- a/src/fheroes2/gui/ui_map_object.cpp +++ b/src/fheroes2/gui/ui_map_object.cpp @@ -39,6 +39,7 @@ #include "maps_tiles.h" #include "math_base.h" #include "mp2.h" +#include "resource.h" namespace { diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 7e60eb960e7..4b54ab68be0 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -20,12 +20,31 @@ #include "map_generator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "direction.h" +#include "ground.h" #include "logging.h" #include "map_format_helper.h" +#include "map_format_info.h" #include "map_object_info.h" +#include "maps.h" +#include "maps_tiles.h" #include "maps_tiles_helper.h" +#include "math_base.h" #include "race.h" #include "rand.h" +#include "resource.h" #include "ui_map_object.h" #include "world.h" #include "world_object_uid.h" @@ -292,11 +311,11 @@ namespace Maps::Generator // Step 1. Map generator configuration // TODO: Balanced set up only / Pyramid later - const int playerCount = config.playerCount; + const int playerCount = static_cast( config.playerCount ); // Aiming for region size to be ~300 tiles in a 200-500 range // const int minimumRegionCount = playerCount + 1; - const int expectedRegionCount = ( width * height ) / config.regionSizeLimit; + const int expectedRegionCount = ( width * height ) / static_cast( config.regionSizeLimit ); const uint32_t extendedWidth = width + 2; std::vector data( static_cast( extendedWidth ) * ( height + 2 ) ); @@ -385,7 +404,7 @@ namespace Maps::Generator if ( region._id == 0 ) continue; - DEBUG_LOG( DBG_ENGINE, DBG_WARN, "Region #" << region._id << " size " << region._nodes.size() << " has " << region._neighbours.size() << "neighbours" ) + DEBUG_LOG( DBG_ENGINE, DBG_TRACE, "Region #" << region._id << " size " << region._nodes.size() << " has " << region._neighbours.size() << "neighbours" ) int xMin = 0; int xMax = width; diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index 33cefa42c13..d566669a04f 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -18,7 +18,11 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ #pragma once -#include "map_format_info.h" +#include +namespace Maps::Map_Format +{ + struct MapFormat; +} namespace Maps::Generator { diff --git a/src/fheroes2/maps/map_object_info.cpp b/src/fheroes2/maps/map_object_info.cpp index 7340a03a7ec..0a323bb290b 100644 --- a/src/fheroes2/maps/map_object_info.cpp +++ b/src/fheroes2/maps/map_object_info.cpp @@ -29,6 +29,7 @@ #include #include "artifact.h" +#include "maps.h" #include "maps_tiles_helper.h" #include "monster.h" #include "resource.h" From 9a740403a9c01d167e23c32fed0e8da89126b111 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Fri, 3 May 2024 00:32:28 -0400 Subject: [PATCH 15/26] Fix release build --- src/fheroes2/editor/editor_interface.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index 5739c36453f..202473ea1a7 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -147,8 +147,10 @@ namespace Interface int32_t _selectedTile{ -1 }; int32_t _tileUnderCursor{ -1 }; +#if defined( WITH_DEBUG ) uint32_t _playerCount = 2; uint32_t _regionSizeLimit = 600; +#endif std::function _cursorUpdater; From 9df56bfa857e9c8f5c0604c106978e6650de3fa5 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sun, 19 May 2024 14:43:40 -0400 Subject: [PATCH 16/26] Add a node cache struct --- VisualStudio/fheroes2/sources.props | 2 + fheroes2-vs2019.vcxproj | 6 -- src/fheroes2/maps/map_generator.cpp | 101 +++++++++++++++++++--------- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/VisualStudio/fheroes2/sources.props b/VisualStudio/fheroes2/sources.props index 9afb1ece581..ba58199a244 100644 --- a/VisualStudio/fheroes2/sources.props +++ b/VisualStudio/fheroes2/sources.props @@ -200,6 +200,7 @@ + @@ -408,6 +409,7 @@ + diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index 8bf51e40055..c7bc18038a5 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -18,12 +18,6 @@ x64 - - - - - - {DD8F214C-C405-4951-8F98-66B969BA8E08} Win32Proj diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 460bfc9eaa3..ac4b8ad6a42 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -56,6 +56,12 @@ namespace Maps::Generator const std::vector playerStartingTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND }; const std::vector neutralTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND, Ground::BEACH, Ground::SWAMP, Ground::DESERT }; + int convertExtendedIndex( int index, size_t width ) + { + const size_t originalWidth = width - 2; + return static_cast( ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1 ); + } + enum { TOP = 0, @@ -91,6 +97,38 @@ namespace Maps::Generator {} }; + struct NodeCache + { + size_t extendedWidth = 0; + std::vector data; + + NodeCache( int32_t width, int32_t height ) + : extendedWidth( width + 2 ) + , data( extendedWidth * ( height + 2 ) ) + { + for ( int y = 0; y < height; ++y ) { + const int rowIndex = y * width; + for ( int x = 0; x < width; ++x ) { + const int index = rowIndex + x; + Node & node = data[convertExtendedIndex( index, extendedWidth )]; + + node.index = index; + node.type = NodeType::OPEN; + } + } + } + + Node & getNode( int32_t index ) + { + return data[convertExtendedIndex( index, extendedWidth )]; + } + + Node & getNode( int32_t index, int32_t offset ) + { + return data[convertExtendedIndex( index, extendedWidth ) + offset]; + } + }; + struct Region { public: @@ -123,7 +161,7 @@ namespace Maps::Generator } }; - std::vector GetDirectionOffsets( const int width ) + std::vector getDirectionOffsets( const int width ) { std::vector offsets( 8 ); offsets[TOP] = -width; @@ -137,18 +175,12 @@ namespace Maps::Generator return offsets; } - uint16_t GetDirectionBitmask( uint8_t direction, bool reflect = false ) + uint16_t getDirectionBitmask( uint8_t direction, bool reflect = false ) { return 1 << ( reflect ? ( direction + 4 ) % 8 : direction ); } - int ConvertExtendedIndex( int index, uint32_t width ) - { - const uint32_t originalWidth = width - 2; - return static_cast( ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1 ); - } - - void CheckAdjacentTiles( std::vector & rawData, Region & region, uint32_t rawDataWidth, const std::vector & offsets ) + void checkAdjacentTiles( NodeCache & rawData, Region & region, const std::vector & offsets ) { Node & previousNode = region._nodes[region._lastProcessedNode]; const int nodeIndex = previousNode.index; @@ -163,9 +195,8 @@ namespace Maps::Generator if ( direction > 3 && Rand::Get( 1 ) ) { break; } - const int newIndex = ConvertExtendedIndex( nodeIndex, rawDataWidth ) + offsets[direction]; - Node & newTile = rawData[newIndex]; - if ( newTile.passable & GetDirectionBitmask( direction, true ) ) { + Node & newTile = rawData.getNode( nodeIndex, offsets[direction] ); + if ( newTile.passable & getDirectionBitmask( direction, true ) ) { if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { newTile.region = region._id; region._nodes.push_back( newTile ); @@ -178,17 +209,38 @@ namespace Maps::Generator } } - void RegionExpansion( std::vector & rawData, uint32_t rawDataWidth, Region & region, const std::vector & offsets ) + void RegionExpansion( NodeCache & rawData, Region & region, const std::vector & offsets ) { // Process only "open" nodes that exist at the start of the loop and ignore what's added const size_t nodesEnd = region._nodes.size(); while ( region._lastProcessedNode < nodesEnd ) { - CheckAdjacentTiles( rawData, region, rawDataWidth, offsets ); + checkAdjacentTiles( rawData, region, offsets ); ++region._lastProcessedNode; } } + bool markObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + // Active action object parts must be placed on a tile without any other objects. + // Only ground parts should be checked for this condition. + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { + // Shadow and terrain layer parts are ignored. + continue; + } + + const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; + if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { + return false; + } + + data.getNode( pos.x ); + } + + return true; + } + bool setObjectOnTile( Map_Format::MapFormat & mapFormat, Tiles & tile, const ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); @@ -302,6 +354,8 @@ namespace Maps::Generator const int32_t width = world.w(); const int32_t height = world.h(); + NodeCache data( width, height ); + auto mapBoundsCheck = [width, height]( int x, int y ) { x = std::max( std::min( x, width - 1 ), 0 ); y = std::max( std::min( y, height - 1 ), 0 ); @@ -316,19 +370,6 @@ namespace Maps::Generator // const int minimumRegionCount = playerCount + 1; const int expectedRegionCount = ( width * height ) / static_cast( config.regionSizeLimit ); - const uint32_t extendedWidth = width + 2; - std::vector data( static_cast( extendedWidth ) * ( height + 2 ) ); - for ( int y = 0; y < height; ++y ) { - const int rowIndex = y * width; - for ( int x = 0; x < width; ++x ) { - const int index = rowIndex + x; - Node & node = data[ConvertExtendedIndex( index, extendedWidth )]; - - node.index = index; - node.type = NodeType::OPEN; - } - } - // Step 2. Determine region layout and placement // Insert empty region that represents water and map edges std::vector mapRegions = { { 0, 0, NEUTRAL_COLOR, Ground::WATER, 0 } }; @@ -363,19 +404,19 @@ namespace Maps::Generator const uint32_t regionID = static_cast( mapRegions.size() ); mapRegions.emplace_back( regionID, centerTile, regionColor, groundType, config.regionSizeLimit ); - data[ConvertExtendedIndex( centerTile, extendedWidth )].region = regionID; + data.getNode( centerTile ).region = regionID; } } // Step 3. Grow all regions one step at the time so they would compete for space - const std::vector & offsets = GetDirectionOffsets( static_cast( extendedWidth ) ); + const std::vector & offsets = getDirectionOffsets( static_cast( width + 2 ) ); bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; // Skip the border region for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) { Region & region = mapRegions[regionID]; - RegionExpansion( data, extendedWidth, region, offsets ); + RegionExpansion( data, region, offsets ); if ( region._lastProcessedNode != region._nodes.size() ) stillRoomToExpand = true; } From 3c56fdcc5b378895b22f8effc847e05e8bdf05b9 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sun, 19 May 2024 18:04:32 -0400 Subject: [PATCH 17/26] Refactor region expansion --- src/fheroes2/maps/map_generator.cpp | 107 +++++++++++----------------- 1 file changed, 41 insertions(+), 66 deletions(-) diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index ac4b8ad6a42..6ea81c5103f 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -56,23 +56,7 @@ namespace Maps::Generator const std::vector playerStartingTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND }; const std::vector neutralTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND, Ground::BEACH, Ground::SWAMP, Ground::DESERT }; - int convertExtendedIndex( int index, size_t width ) - { - const size_t originalWidth = width - 2; - return static_cast( ( index / originalWidth + 1 ) * width + ( index % originalWidth ) + 1 ); - } - - enum - { - TOP = 0, - RIGHT = 1, - BOTTOM = 2, - LEFT = 3, - TOP_LEFT = 4, - TOP_RIGHT = 5, - BOTTOM_RIGHT = 6, - BOTTOM_LEFT = 7, - }; + std::vector directionOffsets = { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } }; enum class NodeType { @@ -86,7 +70,7 @@ namespace Maps::Generator struct Node { int index = -1; - NodeType type = NodeType::BORDER; + NodeType type = NodeType::OPEN; uint32_t region = 0; uint16_t mapObject = 0; uint16_t passable = DIRECTION_ALL; @@ -97,35 +81,42 @@ namespace Maps::Generator {} }; - struct NodeCache + class NodeCache { - size_t extendedWidth = 0; + int32_t mapSize = 0; + Node outOfBounds; std::vector data; + public: NodeCache( int32_t width, int32_t height ) - : extendedWidth( width + 2 ) - , data( extendedWidth * ( height + 2 ) ) + : mapSize( width ) + , outOfBounds( -1 ) + , data( width * height ) { + outOfBounds.type = NodeType::BORDER; + for ( int y = 0; y < height; ++y ) { const int rowIndex = y * width; for ( int x = 0; x < width; ++x ) { const int index = rowIndex + x; - Node & node = data[convertExtendedIndex( index, extendedWidth )]; + Node & node = data[index]; node.index = index; - node.type = NodeType::OPEN; } } } - Node & getNode( int32_t index ) + Node & getNode( const fheroes2::Point position ) { - return data[convertExtendedIndex( index, extendedWidth )]; + if ( position.x < 0 || position.x >= mapSize || position.y < 0 || position.y >= mapSize ) { + return outOfBounds; + } + return data[position.y * mapSize + position.x]; } - Node & getNode( int32_t index, int32_t offset ) + Node & getNode( int32_t index ) { - return data[convertExtendedIndex( index, extendedWidth ) + offset]; + return getNode( { index % mapSize, index / mapSize } ); } }; @@ -161,26 +152,12 @@ namespace Maps::Generator } }; - std::vector getDirectionOffsets( const int width ) - { - std::vector offsets( 8 ); - offsets[TOP] = -width; - offsets[RIGHT] = 1; - offsets[BOTTOM] = width; - offsets[LEFT] = -1; - offsets[TOP_LEFT] = -width - 1; - offsets[TOP_RIGHT] = -width + 1; - offsets[BOTTOM_RIGHT] = width + 1; - offsets[BOTTOM_LEFT] = width - 1; - return offsets; - } - uint16_t getDirectionBitmask( uint8_t direction, bool reflect = false ) { return 1 << ( reflect ? ( direction + 4 ) % 8 : direction ); } - void checkAdjacentTiles( NodeCache & rawData, Region & region, const std::vector & offsets ) + void checkAdjacentTiles( NodeCache & rawData, Region & region ) { Node & previousNode = region._nodes[region._lastProcessedNode]; const int nodeIndex = previousNode.index; @@ -195,7 +172,9 @@ namespace Maps::Generator if ( direction > 3 && Rand::Get( 1 ) ) { break; } - Node & newTile = rawData.getNode( nodeIndex, offsets[direction] ); + + fheroes2::Point newPosition = Maps::GetPoint( nodeIndex ); + Node & newTile = rawData.getNode( newPosition + directionOffsets[direction] ); if ( newTile.passable & getDirectionBitmask( direction, true ) ) { if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { newTile.region = region._id; @@ -209,33 +188,30 @@ namespace Maps::Generator } } - void RegionExpansion( NodeCache & rawData, Region & region, const std::vector & offsets ) + void regionExpansion( NodeCache & rawData, Region & region ) { // Process only "open" nodes that exist at the start of the loop and ignore what's added const size_t nodesEnd = region._nodes.size(); while ( region._lastProcessedNode < nodesEnd ) { - checkAdjacentTiles( rawData, region, offsets ); + checkAdjacentTiles( rawData, region ); ++region._lastProcessedNode; } } - bool markObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + bool canFitObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ) { - // Active action object parts must be placed on a tile without any other objects. - // Only ground parts should be checked for this condition. for ( const auto & objectPart : info.groundLevelParts ) { if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { // Shadow and terrain layer parts are ignored. continue; } - const fheroes2::Point pos{ mainTilePos.x + objectPart.tileOffset.x, mainTilePos.y + objectPart.tileOffset.y }; - if ( !Maps::isValidAbsPoint( pos.x, pos.y ) ) { + Node & node = data.getNode( mainTilePos + objectPart.tileOffset ); + + if ( node.type != NodeType::OPEN ) { return false; } - - data.getNode( pos.x ); } return true; @@ -269,11 +245,11 @@ namespace Maps::Generator return true; } - bool objectPlacer( Map_Format::MapFormat & mapFormat, Tiles & tile, ObjectGroup groupType, int32_t type ) + bool objectPlacer( Map_Format::MapFormat & mapFormat, NodeCache & data, Tiles & tile, ObjectGroup groupType, int32_t type ) { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( entranceCheck( tilePos ) ) { + if ( canFitObject( data, objectInfo, tilePos ) && entranceCheck( tilePos ) ) { // do not update passabilities after every object if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { return false; @@ -285,7 +261,7 @@ namespace Maps::Generator return false; } - bool placeCastle( Map_Format::MapFormat & mapFormat, Region & region, int targetX, int targetY ) + bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, int targetX, int targetY ) { const int regionX = region._centerIndex % mapFormat.size; const int regionY = region._centerIndex / mapFormat.size; @@ -297,10 +273,10 @@ namespace Maps::Generator const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); - //const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - //const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); + const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); - if ( true ) { + if ( canFitObject( data, basementInfo, tilePos ) && canFitObject( data, castleInfo, tilePos ) ) { setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); assert( Maps::getLastObjectUID() > 0 ); @@ -334,12 +310,12 @@ namespace Maps::Generator return true; } - bool placeMine( Map_Format::MapFormat & mapFormat, Region & region, const int resource ) + bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, const int resource ) { const auto & node = Rand::Get( region._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - return node.type == NodeType::OPEN && objectPlacer( mapFormat, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); + return node.type == NodeType::OPEN && objectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); } bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) @@ -409,14 +385,13 @@ namespace Maps::Generator } // Step 3. Grow all regions one step at the time so they would compete for space - const std::vector & offsets = getDirectionOffsets( static_cast( width + 2 ) ); bool stillRoomToExpand = true; while ( stillRoomToExpand ) { stillRoomToExpand = false; // Skip the border region for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) { Region & region = mapRegions[regionID]; - RegionExpansion( data, region, offsets ); + regionExpansion( data, region ); if ( region._lastProcessedNode != region._nodes.size() ) stillRoomToExpand = true; } @@ -464,13 +439,13 @@ namespace Maps::Generator } } - if ( region._colorIndex != NEUTRAL_COLOR && !placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { + if ( region._colorIndex != NEUTRAL_COLOR && !placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { // return early if we can't place a starting player castle return false; } if ( region._nodes.size() > 300 ) { // place non-mandatory castles in bigger neutral regions - placeCastle( mapFormat, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); + placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); } if ( config.basicOnly ) { @@ -481,7 +456,7 @@ namespace Maps::Generator for ( const int resource : resoures ) { // TODO: do a gradual distribution instead of guesses for ( int tries = 0; tries < 5; tries++ ) { - if ( placeMine( mapFormat, region, resource ) ) { + if ( placeMine( mapFormat, data, region, resource ) ) { break; } } From a7d0fc0db48fcb76bd39b698777737a703a15188 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Mon, 20 May 2024 23:19:29 -0400 Subject: [PATCH 18/26] Fix object placement logic --- src/fheroes2/maps/map_generator.cpp | 115 ++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 6ea81c5103f..353b7594c47 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -199,8 +199,10 @@ namespace Maps::Generator } } - bool canFitObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + bool canFitObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool isAction = false ) { + fheroes2::Rect objectRect; + for ( const auto & objectPart : info.groundLevelParts ) { if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { // Shadow and terrain layer parts are ignored. @@ -209,24 +211,80 @@ namespace Maps::Generator Node & node = data.getNode( mainTilePos + objectPart.tileOffset ); - if ( node.type != NodeType::OPEN ) { + if ( node.index == -1 || node.type != NodeType::OPEN ) { return false; } + + objectRect.x = std::min( objectRect.x, objectPart.tileOffset.x ); + objectRect.width = std::max( objectRect.width, objectPart.tileOffset.x ); + } + + for ( const auto & partInfo : info.topLevelParts ) { + Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); + + if ( node.index == -1 || node.type != NodeType::OPEN ) { + return false; + } + + objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); + objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); + } + + if ( isAction ) { + for ( int x = objectRect.x - 1; x <= objectRect.width + 1; x++ ) { + Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, 1 } ); + if ( pathNode.index == -1 || pathNode.type == NodeType::OBSTACLE ) { + return false; + } + if ( pathNode.type == NodeType::BORDER ) { + return false; + } + } } return true; } - bool setObjectOnTile( Map_Format::MapFormat & mapFormat, Tiles & tile, const ObjectGroup groupType, const int32_t objectIndex ) + void markObjectPlacement( NodeCache & data, const ObjectInfo & objectInfo, const fheroes2::Point & mainTilePos, const bool isAction = false ) + { + // mark object placement + fheroes2::Rect objectRect; + + for ( const auto & objectPart : objectInfo.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { + // Shadow and terrain layer parts are ignored. + continue; + } + + Node & node = data.getNode( mainTilePos + objectPart.tileOffset ); + objectRect.x = std::min( objectRect.x, objectPart.tileOffset.x ); + objectRect.y = std::min( objectRect.y, objectPart.tileOffset.y ); + objectRect.width = std::max( objectRect.width, objectPart.tileOffset.x ); + objectRect.height = std::max( objectRect.height, objectPart.tileOffset.y ); + + node.type = NodeType::OBSTACLE; + } + + if ( isAction ) { + for ( int x = objectRect.x - 1; x <= objectRect.width + 1; x++ ) { + Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, objectRect.height + 1 } ); + pathNode.type = NodeType::PATH; + } + data.getNode( mainTilePos ).type = NodeType::ACTION; + } + } + + bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tiles & tile, const ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); if ( objectInfo.empty() ) { - // Check your logic as you are trying to insert an empty object! assert( 0 ); return false; } - if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { + // do not update passabilities after every object + if ( !Maps::setObjectOnTile( tile, objectInfo, false ) ) { + assert( 0 ); return false; } @@ -235,27 +293,12 @@ namespace Maps::Generator return true; } - bool entranceCheck( const fheroes2::Point tilePos ) - { - for ( int i = -2; i < 3; i++ ) { - if ( !Maps::isValidAbsPoint( tilePos.x + i, tilePos.y + 1 ) || !Maps::isClearGround( world.GetTiles( tilePos.x + i, tilePos.y + 1 ) ) ) { - return false; - } - } - return true; - } - - bool objectPlacer( Map_Format::MapFormat & mapFormat, NodeCache & data, Tiles & tile, ObjectGroup groupType, int32_t type ) + bool actionObjectPlacer( Map_Format::MapFormat & mapFormat, NodeCache & data, Tiles & tile, ObjectGroup groupType, int32_t type ) { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( canFitObject( data, objectInfo, tilePos ) && entranceCheck( tilePos ) ) { - // do not update passabilities after every object - if ( !Maps::setObjectOnTile( tile, objectInfo, true ) ) { - return false; - } - - Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( type ) ); + if ( canFitObject( data, objectInfo, tilePos, true ) && putObjectOnMap( mapFormat, tile, groupType, static_cast( type ) ) ) { + markObjectPlacement( data, objectInfo, tilePos, true ); return true; } return false; @@ -276,14 +319,16 @@ namespace Maps::Generator const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); - if ( canFitObject( data, basementInfo, tilePos ) && canFitObject( data, castleInfo, tilePos ) ) { - setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + if ( canFitObject( data, basementInfo, tilePos ) && canFitObject( data, castleInfo, tilePos, true ) ) { + putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + markObjectPlacement( data, basementInfo, tilePos, false ); assert( Maps::getLastObjectUID() > 0 ); const uint32_t objectId = Maps::getLastObjectUID() - 1; Maps::setLastObjectUID( objectId ); - setObjectOnTile( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, 12 ); + putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); + markObjectPlacement( data, castleInfo, tilePos, true ); const uint8_t color = Color::IndexToColor( region._colorIndex ); // By default use random (default) army for the neutral race town/castle. @@ -295,19 +340,21 @@ namespace Maps::Generator assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); Maps::setLastObjectUID( objectId ); - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 ) ) { + if ( !putObjectOnMap( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 ) ) { return false; } Maps::setLastObjectUID( objectId ); - if ( !setObjectOnTile( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 + 1 ) ) { + if ( !putObjectOnMap( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 + 1 ) ) { return false; } world.addCastle( tile.GetIndex(), Race::IndexToRace( RANDOM_CASTLE_INDEX ), color ); + + return true; } - return true; + return false; } bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, const int resource ) @@ -315,7 +362,7 @@ namespace Maps::Generator const auto & node = Rand::Get( region._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - return node.type == NodeType::OPEN && objectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); + return actionObjectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); } bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) @@ -323,7 +370,7 @@ namespace Maps::Generator if ( config.playerCount < 2 || config.playerCount > 6 ) { return false; } - if ( config.regionSizeLimit < 100 ) { + if ( config.regionSizeLimit < 200 ) { return false; } @@ -342,7 +389,7 @@ namespace Maps::Generator // TODO: Balanced set up only / Pyramid later const int playerCount = static_cast( config.playerCount ); - // Aiming for region size to be ~300 tiles in a 200-500 range + // Aiming for region size to be ~400 tiles in a 300-600 range // const int minimumRegionCount = playerCount + 1; const int expectedRegionCount = ( width * height ) / static_cast( config.regionSizeLimit ); @@ -399,6 +446,8 @@ namespace Maps::Generator // Step 4. We're ready to save the result; reset the current world first world.generateForEditor( width ); + mapFormat.tiles.clear(); + mapFormat.tiles.resize( width * height ); for ( Region & region : mapRegions ) { if ( region._id == 0 ) @@ -443,7 +492,7 @@ namespace Maps::Generator // return early if we can't place a starting player castle return false; } - if ( region._nodes.size() > 300 ) { + if ( region._nodes.size() > 400 ) { // place non-mandatory castles in bigger neutral regions placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ); } From 7a8fa85bb692b45c2afe9326ca3505ee1cd49085 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Mon, 20 May 2024 23:46:59 -0400 Subject: [PATCH 19/26] Headers --- src/fheroes2/maps/map_generator.cpp | 1 + src/fheroes2/maps/map_object_info.cpp | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 353b7594c47..10d9446d031 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -37,6 +37,7 @@ #include "logging.h" #include "map_format_helper.h" #include "map_format_info.h" +#include "map_object_info.h" #include "maps.h" #include "maps_tiles.h" #include "maps_tiles_helper.h" diff --git a/src/fheroes2/maps/map_object_info.cpp b/src/fheroes2/maps/map_object_info.cpp index d30bfd20e37..86dec5a666a 100644 --- a/src/fheroes2/maps/map_object_info.cpp +++ b/src/fheroes2/maps/map_object_info.cpp @@ -29,11 +29,8 @@ #include #include "artifact.h" -#include "maps.h" -#include "maps_tiles_helper.h" #include "monster.h" #include "resource.h" -#include "world.h" namespace { From e443648a36d5a23423df7c981aa7f62faacd25ac Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sat, 25 May 2024 00:36:29 -0400 Subject: [PATCH 20/26] Basic feedback --- fheroes2-vs2019.vcxproj | 2 +- src/fheroes2/maps/map_generator.cpp | 69 +++++++++++++++++------------ src/fheroes2/maps/map_generator.h | 3 +- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index c7bc18038a5..c5b35a0ad7c 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -48,4 +48,4 @@ - \ No newline at end of file + diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index 10d9446d031..b292c665397 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -49,15 +49,16 @@ #include "world.h" #include "world_object_uid.h" -namespace Maps::Generator +namespace { // ObjectInfo ObjctGroup based indicies do not match old objects - const int NEUTRAL_COLOR = 6; - const int RANDOM_CASTLE_INDEX = 12; - const std::vector playerStartingTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND }; - const std::vector neutralTerrain = { Ground::GRASS, Ground::DIRT, Ground::SNOW, Ground::LAVA, Ground::WASTELAND, Ground::BEACH, Ground::SWAMP, Ground::DESERT }; + const int neutralCOLOR = 6; + const int randomCastleIndex = 12; + const std::vector playerStartingTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, Maps::Ground::WASTELAND }; + const std::vector neutralTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, + Maps::Ground::WASTELAND, Maps::Ground::BEACH, Maps::Ground::SWAMP, Maps::Ground::DESERT }; - std::vector directionOffsets = { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } }; + const std::vector directionOffsets = { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } }; enum class NodeType { @@ -79,7 +80,9 @@ namespace Maps::Generator Node() = default; explicit Node( int index_ ) : index( index_ ) - {} + { + // Do nothing + } }; class NodeCache @@ -96,10 +99,10 @@ namespace Maps::Generator { outOfBounds.type = NodeType::BORDER; - for ( int y = 0; y < height; ++y ) { - const int rowIndex = y * width; - for ( int x = 0; x < width; ++x ) { - const int index = rowIndex + x; + for ( int32_t y = 0; y < height; ++y ) { + const int32_t rowIndex = y * width; + for ( int32_t x = 0; x < width; ++x ) { + const int32_t index = rowIndex + x; Node & node = data[index]; node.index = index; @@ -123,15 +126,14 @@ namespace Maps::Generator struct Region { - public: uint32_t _id{}; int32_t _centerIndex = -1; std::set _neighbours; std::vector _nodes; size_t _sizeLimit{}; size_t _lastProcessedNode{}; - int _colorIndex = NEUTRAL_COLOR; - int _groundType = Ground::GRASS; + int _colorIndex = neutralCOLOR; + int _groundType = Maps::Ground::GRASS; Region() = default; @@ -200,7 +202,7 @@ namespace Maps::Generator } } - bool canFitObject( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool isAction = false ) + bool canFitObject( NodeCache & data, const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool isAction = false ) { fheroes2::Rect objectRect; @@ -246,7 +248,7 @@ namespace Maps::Generator return true; } - void markObjectPlacement( NodeCache & data, const ObjectInfo & objectInfo, const fheroes2::Point & mainTilePos, const bool isAction = false ) + void markObjectPlacement( NodeCache & data, const Maps::ObjectInfo & objectInfo, const fheroes2::Point & mainTilePos, const bool isAction = false ) { // mark object placement fheroes2::Rect objectRect; @@ -266,6 +268,11 @@ namespace Maps::Generator node.type = NodeType::OBSTACLE; } + for ( const auto & partInfo : objectInfo.topLevelParts ) { + objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); + objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); + } + if ( isAction ) { for ( int x = objectRect.x - 1; x <= objectRect.width + 1; x++ ) { Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, objectRect.height + 1 } ); @@ -275,7 +282,7 @@ namespace Maps::Generator } } - bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tiles & tile, const ObjectGroup groupType, const int32_t objectIndex ) + bool putObjectOnMap( Maps::Map_Format::MapFormat & mapFormat, Maps::Tiles & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); if ( objectInfo.empty() ) { @@ -294,7 +301,7 @@ namespace Maps::Generator return true; } - bool actionObjectPlacer( Map_Format::MapFormat & mapFormat, NodeCache & data, Tiles & tile, ObjectGroup groupType, int32_t type ) + bool actionObjectPlacer( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); @@ -305,7 +312,7 @@ namespace Maps::Generator return false; } - bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, int targetX, int targetY ) + bool placeCastle( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, int targetX, int targetY ) { const int regionX = region._centerIndex % mapFormat.size; const int regionY = region._centerIndex / mapFormat.size; @@ -318,7 +325,7 @@ namespace Maps::Generator const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); + const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, randomCastleIndex ); if ( canFitObject( data, basementInfo, tilePos ) && canFitObject( data, castleInfo, tilePos, true ) ) { putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); @@ -328,7 +335,7 @@ namespace Maps::Generator const uint32_t objectId = Maps::getLastObjectUID() - 1; Maps::setLastObjectUID( objectId ); - putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, RANDOM_CASTLE_INDEX ); + putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, randomCastleIndex ); markObjectPlacement( data, castleInfo, tilePos, true ); const uint8_t color = Color::IndexToColor( region._colorIndex ); @@ -351,24 +358,29 @@ namespace Maps::Generator return false; } - world.addCastle( tile.GetIndex(), Race::IndexToRace( RANDOM_CASTLE_INDEX ), color ); + world.addCastle( tile.GetIndex(), Race::IndexToRace( randomCastleIndex ), color ); return true; } return false; } - bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, const int resource ) + bool placeMine( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, const int resource ) { const auto & node = Rand::Get( region._nodes ); Maps::Tiles & mineTile = world.GetTiles( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); return actionObjectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); } +} + +namespace Maps::Generator +{ - bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ) + bool generateWorld( Map_Format::MapFormat & mapFormat, const Configuration & config ) { if ( config.playerCount < 2 || config.playerCount > 6 ) { + assert( config.playerCount <= 6 ); return false; } if ( config.regionSizeLimit < 200 ) { @@ -396,7 +408,7 @@ namespace Maps::Generator // Step 2. Determine region layout and placement // Insert empty region that represents water and map edges - std::vector mapRegions = { { 0, 0, NEUTRAL_COLOR, Ground::WATER, 0 } }; + std::vector mapRegions = { { 0, 0, neutralCOLOR, Ground::WATER, 0 } }; const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); const int innerLayer = std::min( neutralRegionCount, playerCount ); @@ -424,7 +436,7 @@ namespace Maps::Generator const bool isPlayerRegion = layer == 1 && ( i % factor ) == 0; const int groundType = isPlayerRegion ? Rand::Get( playerStartingTerrain ) : Rand::Get( neutralTerrain ); - const int regionColor = isPlayerRegion ? i / factor : NEUTRAL_COLOR; + const int regionColor = isPlayerRegion ? i / factor : neutralCOLOR; const uint32_t regionID = static_cast( mapRegions.size() ); mapRegions.emplace_back( regionID, centerTile, regionColor, groundType, config.regionSizeLimit ); @@ -489,7 +501,7 @@ namespace Maps::Generator } } - if ( region._colorIndex != NEUTRAL_COLOR && !placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { + if ( region._colorIndex != neutralCOLOR && !placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { // return early if we can't place a starting player castle return false; } @@ -521,8 +533,9 @@ namespace Maps::Generator // // make sure objects accessible before // make sure paths are accessible - delete obstacles + // place treasures // place monsters return true; } -} \ No newline at end of file +} diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index d566669a04f..892877ea0b5 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -19,6 +19,7 @@ ***************************************************************************/ #pragma once #include + namespace Maps::Map_Format { struct MapFormat; @@ -33,5 +34,5 @@ namespace Maps::Generator bool basicOnly = true; }; - bool generateWorld( Map_Format::MapFormat & mapFormat, Configuration config ); + bool generateWorld( Map_Format::MapFormat & mapFormat, const Configuration & config ); } From f86c7bfb68ce48f4da3d4891342817769e7c4440 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 01:40:18 -0500 Subject: [PATCH 21/26] Fix code conflicts --- src/fheroes2/maps/map_generator.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index b292c665397..f1f1421f36e 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -282,7 +282,7 @@ namespace } } - bool putObjectOnMap( Maps::Map_Format::MapFormat & mapFormat, Maps::Tiles & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) + bool putObjectOnMap( Maps::Map_Format::MapFormat & mapFormat, Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); if ( objectInfo.empty() ) { @@ -301,7 +301,7 @@ namespace return true; } - bool actionObjectPlacer( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Maps::Tiles & tile, Maps::ObjectGroup groupType, int32_t type ) + bool actionObjectPlacer( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Maps::Tile & tile, Maps::ObjectGroup groupType, int32_t type ) { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); @@ -319,7 +319,7 @@ namespace const int castleX = std::min( std::max( ( targetX + regionX ) / 2, 4 ), mapFormat.size - 4 ); const int castleY = std::min( std::max( ( targetY + regionY ) / 2, 3 ), mapFormat.size - 3 ); - auto & tile = world.GetTiles( castleY * mapFormat.size + castleX ); + auto & tile = world.getTile( castleY * mapFormat.size + castleX ); fheroes2::Point tilePos = tile.GetCenter(); const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); @@ -348,13 +348,13 @@ namespace assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); Maps::setLastObjectUID( objectId ); - if ( !putObjectOnMap( mapFormat, world.GetTiles( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 ) ) { + if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 ) ) { return false; } Maps::setLastObjectUID( objectId ); - if ( !putObjectOnMap( mapFormat, world.GetTiles( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 + 1 ) ) { + if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, region._colorIndex * 2 + 1 ) ) { return false; } @@ -368,7 +368,7 @@ namespace bool placeMine( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, const int resource ) { const auto & node = Rand::Get( region._nodes ); - Maps::Tiles & mineTile = world.GetTiles( node.index ); + Maps::Tile & mineTile = world.getTile( node.index ); const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); return actionObjectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); } @@ -467,7 +467,7 @@ namespace Maps::Generator continue; for ( const Node & node : region._nodes ) { - world.GetTiles( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( region._groundType, true ), false, false ); + world.getTile( node.index ).setTerrain( Maps::Ground::getRandomTerrainImageIndex( region._groundType, true ), false, false ); } // Fix missing references @@ -524,7 +524,7 @@ namespace Maps::Generator } } - Maps::updateRoadOnTile( world.GetTiles( region._centerIndex ), true ); + Maps::updateRoadOnTile( world.getTile( region._centerIndex ), true ); } // set up region connectors based on frequency settings & border length From 107b2ffb4322ed727b11401fb2f37e413b8fc6bf Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 01:51:47 -0500 Subject: [PATCH 22/26] Address feedback --- src/fheroes2/editor/editor_interface.cpp | 2 +- src/fheroes2/gui/ui_map_object.h | 1 + src/fheroes2/maps/map_generator.cpp | 43 ++++++++++++------------ src/fheroes2/maps/map_generator.h | 6 ++-- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 730c41a0791..e39b48888a9 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -888,7 +888,7 @@ namespace Interface rmgConfig.playerCount = _playerCount; rmgConfig.regionSizeLimit = _regionSizeLimit; - if ( Maps::Generator::generateWorld( _mapFormat, rmgConfig ) ) { + if ( Maps::Generator::generateMap( _mapFormat, rmgConfig ) ) { _redraw |= mapUpdateFlags; action.commit(); diff --git a/src/fheroes2/gui/ui_map_object.h b/src/fheroes2/gui/ui_map_object.h index 8f4a3471972..c6afa7df57f 100644 --- a/src/fheroes2/gui/ui_map_object.h +++ b/src/fheroes2/gui/ui_map_object.h @@ -35,5 +35,6 @@ namespace fheroes2 Sprite generateTownObjectImage( const int townType, const int color, const int groundId ); int32_t getTownBasementId( const int groundType ); + int32_t getMineObjectInfoId( const int resource, const int groundType ); } diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_generator.cpp index f1f1421f36e..fdbee757451 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_generator.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -52,7 +52,7 @@ namespace { // ObjectInfo ObjctGroup based indicies do not match old objects - const int neutralCOLOR = 6; + const int neutralColor = 6; const int randomCastleIndex = 12; const std::vector playerStartingTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, Maps::Ground::WASTELAND }; const std::vector neutralTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, @@ -71,10 +71,10 @@ namespace struct Node { - int index = -1; + int index{ -1 }; NodeType type = NodeType::OPEN; - uint32_t region = 0; - uint16_t mapObject = 0; + uint32_t region{ 0 }; + uint16_t mapObject{ 0 }; uint16_t passable = DIRECTION_ALL; Node() = default; @@ -87,15 +87,11 @@ namespace class NodeCache { - int32_t mapSize = 0; - Node outOfBounds; - std::vector data; - public: NodeCache( int32_t width, int32_t height ) : mapSize( width ) , outOfBounds( -1 ) - , data( width * height ) + , data( static_cast( width ) * height ) { outOfBounds.type = NodeType::BORDER; @@ -122,17 +118,22 @@ namespace { return getNode( { index % mapSize, index / mapSize } ); } + + private: + int32_t mapSize{ 0 }; + Node outOfBounds; + std::vector data; }; struct Region { - uint32_t _id{}; - int32_t _centerIndex = -1; + uint32_t _id{ 0 }; + int32_t _centerIndex{ -1 }; std::set _neighbours; std::vector _nodes; - size_t _sizeLimit{}; - size_t _lastProcessedNode{}; - int _colorIndex = neutralCOLOR; + size_t _sizeLimit{ 0 }; + size_t _lastProcessedNode{ 0 }; + int _colorIndex = neutralColor; int _groundType = Maps::Ground::GRASS; Region() = default; @@ -305,7 +306,7 @@ namespace { const fheroes2::Point tilePos = tile.GetCenter(); const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( canFitObject( data, objectInfo, tilePos, true ) && putObjectOnMap( mapFormat, tile, groupType, static_cast( type ) ) ) { + if ( canFitObject( data, objectInfo, tilePos, true ) && putObjectOnMap( mapFormat, tile, groupType, type ) ) { markObjectPlacement( data, objectInfo, tilePos, true ); return true; } @@ -377,7 +378,7 @@ namespace namespace Maps::Generator { - bool generateWorld( Map_Format::MapFormat & mapFormat, const Configuration & config ) + bool generateMap( Map_Format::MapFormat & mapFormat, const Configuration & config ) { if ( config.playerCount < 2 || config.playerCount > 6 ) { assert( config.playerCount <= 6 ); @@ -408,7 +409,7 @@ namespace Maps::Generator // Step 2. Determine region layout and placement // Insert empty region that represents water and map edges - std::vector mapRegions = { { 0, 0, neutralCOLOR, Ground::WATER, 0 } }; + std::vector mapRegions = { { 0, 0, neutralColor, Ground::WATER, 0 } }; const int neutralRegionCount = std::max( 1, expectedRegionCount - playerCount ); const int innerLayer = std::min( neutralRegionCount, playerCount ); @@ -436,7 +437,7 @@ namespace Maps::Generator const bool isPlayerRegion = layer == 1 && ( i % factor ) == 0; const int groundType = isPlayerRegion ? Rand::Get( playerStartingTerrain ) : Rand::Get( neutralTerrain ); - const int regionColor = isPlayerRegion ? i / factor : neutralCOLOR; + const int regionColor = isPlayerRegion ? i / factor : neutralColor; const uint32_t regionID = static_cast( mapRegions.size() ); mapRegions.emplace_back( regionID, centerTile, regionColor, groundType, config.regionSizeLimit ); @@ -460,7 +461,7 @@ namespace Maps::Generator // Step 4. We're ready to save the result; reset the current world first world.generateForEditor( width ); mapFormat.tiles.clear(); - mapFormat.tiles.resize( width * height ); + mapFormat.tiles.resize( static_cast( width ) * height ); for ( Region & region : mapRegions ) { if ( region._id == 0 ) @@ -501,7 +502,7 @@ namespace Maps::Generator } } - if ( region._colorIndex != neutralCOLOR && !placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { + if ( region._colorIndex != neutralColor && !placeCastle( mapFormat, data, region, ( xMin + xMax ) / 2, ( yMin + yMax ) / 2 ) ) { // return early if we can't place a starting player castle return false; } diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_generator.h index 892877ea0b5..4da28161aba 100644 --- a/src/fheroes2/maps/map_generator.h +++ b/src/fheroes2/maps/map_generator.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -17,7 +17,9 @@ * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ + #pragma once + #include namespace Maps::Map_Format @@ -34,5 +36,5 @@ namespace Maps::Generator bool basicOnly = true; }; - bool generateWorld( Map_Format::MapFormat & mapFormat, const Configuration & config ); + bool generateMap( Map_Format::MapFormat & mapFormat, const Configuration & config ); } From b3892e6d09c207ef942707a0f805c6f7ff6831cf Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 01:58:19 -0500 Subject: [PATCH 23/26] Rename the source files --- VisualStudio/fheroes2/sources.props | 4 ++-- src/fheroes2/editor/editor_interface.cpp | 2 +- .../maps/{map_generator.cpp => map_random_generator.cpp} | 2 +- src/fheroes2/maps/{map_generator.h => map_random_generator.h} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename src/fheroes2/maps/{map_generator.cpp => map_random_generator.cpp} (99%) rename src/fheroes2/maps/{map_generator.h => map_random_generator.h} (100%) diff --git a/VisualStudio/fheroes2/sources.props b/VisualStudio/fheroes2/sources.props index d246fc6ab48..136e00c66a1 100644 --- a/VisualStudio/fheroes2/sources.props +++ b/VisualStudio/fheroes2/sources.props @@ -206,8 +206,8 @@ - + @@ -412,8 +412,8 @@ - + diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index e39b48888a9..d53f860f73e 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -60,8 +60,8 @@ #include "interface_radar.h" #include "localevent.h" #include "map_format_helper.h" -#include "map_generator.h" #include "map_object_info.h" +#include "map_random_generator.h" #include "maps.h" #include "maps_fileinfo.h" #include "maps_tiles.h" diff --git a/src/fheroes2/maps/map_generator.cpp b/src/fheroes2/maps/map_random_generator.cpp similarity index 99% rename from src/fheroes2/maps/map_generator.cpp rename to src/fheroes2/maps/map_random_generator.cpp index fdbee757451..8d73d1a0d24 100644 --- a/src/fheroes2/maps/map_generator.cpp +++ b/src/fheroes2/maps/map_random_generator.cpp @@ -18,7 +18,7 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include "map_generator.h" +#include "map_random_generator.h" #include #include diff --git a/src/fheroes2/maps/map_generator.h b/src/fheroes2/maps/map_random_generator.h similarity index 100% rename from src/fheroes2/maps/map_generator.h rename to src/fheroes2/maps/map_random_generator.h From 41011bc1d15ff849b9ca22ab38fdc9421cf31854 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 02:00:27 -0500 Subject: [PATCH 24/26] Fix copyright --- src/fheroes2/editor/editor_interface.h | 2 +- src/fheroes2/gui/ui_map_object.cpp | 2 +- src/fheroes2/gui/ui_map_object.h | 2 +- src/fheroes2/maps/map_random_generator.cpp | 2 +- src/fheroes2/maps/map_random_generator.h | 2 +- src/fheroes2/world/world_regions.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index 21d753c895f..ed5c63d17b3 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2023 - 2024 * + * Copyright (C) 2023 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/gui/ui_map_object.cpp b/src/fheroes2/gui/ui_map_object.cpp index 0d641909daa..bce90310d85 100644 --- a/src/fheroes2/gui/ui_map_object.cpp +++ b/src/fheroes2/gui/ui_map_object.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2023 - 2024 * + * Copyright (C) 2023 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/gui/ui_map_object.h b/src/fheroes2/gui/ui_map_object.h index c6afa7df57f..0a91e396be1 100644 --- a/src/fheroes2/gui/ui_map_object.h +++ b/src/fheroes2/gui/ui_map_object.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2023 - 2024 * + * Copyright (C) 2023 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/maps/map_random_generator.cpp b/src/fheroes2/maps/map_random_generator.cpp index 8d73d1a0d24..9de59646372 100644 --- a/src/fheroes2/maps/map_random_generator.cpp +++ b/src/fheroes2/maps/map_random_generator.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 - 2025 * + * Copyright (C) 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/maps/map_random_generator.h b/src/fheroes2/maps/map_random_generator.h index 4da28161aba..a3bb55b6148 100644 --- a/src/fheroes2/maps/map_random_generator.h +++ b/src/fheroes2/maps/map_random_generator.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 - 2025 * + * Copyright (C) 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/src/fheroes2/world/world_regions.cpp b/src/fheroes2/world/world_regions.cpp index 2dd276b7627..a2114c64f60 100644 --- a/src/fheroes2/world/world_regions.cpp +++ b/src/fheroes2/world/world_regions.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2020 - 2024 * + * Copyright (C) 2020 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * From 39f6eb6391c80d94bce27d188a4ef9c0c182a941 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 02:06:46 -0500 Subject: [PATCH 25/26] Clang-tidy changes --- src/fheroes2/editor/editor_interface.cpp | 8 ++++---- src/fheroes2/maps/map_random_generator.cpp | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index d53f860f73e..87f6f8264dd 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -898,13 +898,13 @@ namespace Interface } } else if ( HotKeyPressEvent( Game::HotKeyEvent::EDITOR_RANDOM_MAP_CONFIGURATION ) ) { - int32_t newCount = _playerCount; + int32_t newCount = static_cast( _playerCount ); if ( Dialog::SelectCount( "Pick player count", 2, 6, newCount ) ) { - _playerCount = newCount; + _playerCount = static_cast( newCount ); } - newCount = _regionSizeLimit; + newCount = static_cast( _regionSizeLimit ); if ( Dialog::SelectCount( "Limit region size", 100, 10000, newCount ) ) { - _regionSizeLimit = newCount; + _regionSizeLimit = static_cast( newCount ); } } #endif diff --git a/src/fheroes2/maps/map_random_generator.cpp b/src/fheroes2/maps/map_random_generator.cpp index 9de59646372..b385b6a17c1 100644 --- a/src/fheroes2/maps/map_random_generator.cpp +++ b/src/fheroes2/maps/map_random_generator.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2025 * + * Copyright (C) 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -177,7 +177,7 @@ namespace break; } - fheroes2::Point newPosition = Maps::GetPoint( nodeIndex ); + const fheroes2::Point newPosition = Maps::GetPoint( nodeIndex ); Node & newTile = rawData.getNode( newPosition + directionOffsets[direction] ); if ( newTile.passable & getDirectionBitmask( direction, true ) ) { if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { @@ -213,7 +213,7 @@ namespace continue; } - Node & node = data.getNode( mainTilePos + objectPart.tileOffset ); + const Node & node = data.getNode( mainTilePos + objectPart.tileOffset ); if ( node.index == -1 || node.type != NodeType::OPEN ) { return false; @@ -224,7 +224,7 @@ namespace } for ( const auto & partInfo : info.topLevelParts ) { - Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); + const Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); if ( node.index == -1 || node.type != NodeType::OPEN ) { return false; @@ -236,7 +236,7 @@ namespace if ( isAction ) { for ( int x = objectRect.x - 1; x <= objectRect.width + 1; x++ ) { - Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, 1 } ); + const Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, 1 } ); if ( pathNode.index == -1 || pathNode.type == NodeType::OBSTACLE ) { return false; } @@ -321,7 +321,7 @@ namespace const int castleY = std::min( std::max( ( targetY + regionY ) / 2, 3 ), mapFormat.size - 3 ); auto & tile = world.getTile( castleY * mapFormat.size + castleX ); - fheroes2::Point tilePos = tile.GetCenter(); + const fheroes2::Point tilePos = tile.GetCenter(); const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); @@ -463,7 +463,7 @@ namespace Maps::Generator mapFormat.tiles.clear(); mapFormat.tiles.resize( static_cast( width ) * height ); - for ( Region & region : mapRegions ) { + for ( const Region & region : mapRegions ) { if ( region._id == 0 ) continue; @@ -472,7 +472,7 @@ namespace Maps::Generator } // Fix missing references - for ( uint32_t adjacent : region._neighbours ) { + for ( const uint32_t adjacent : region._neighbours ) { mapRegions[adjacent]._neighbours.insert( region._id ); } } From 4e2388860f91fec9b2322fc9f51b76fa3d2f58e9 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 18 Feb 2025 02:14:17 -0500 Subject: [PATCH 26/26] iwyu --- src/fheroes2/maps/map_random_generator.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fheroes2/maps/map_random_generator.cpp b/src/fheroes2/maps/map_random_generator.cpp index b385b6a17c1..92209ecf581 100644 --- a/src/fheroes2/maps/map_random_generator.cpp +++ b/src/fheroes2/maps/map_random_generator.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include