Improve road network and verify object placement immediately (#10374)

This commit is contained in:
Ivan Shibanov
2025-12-10 10:25:42 +08:00
committed by GitHub
parent b0273014ab
commit 8366478700
7 changed files with 437 additions and 166 deletions
+22 -10
View File
@@ -856,14 +856,6 @@ namespace
return ( type == MP2::OBJ_CASTLE ) || ( type == MP2::OBJ_RANDOM_TOWN ) || ( type == MP2::OBJ_RANDOM_CASTLE );
}
void removeRoads( Maps::Map_Format::TileInfo & tile, const int32_t tileIndex )
{
tile.objects.erase( std::remove_if( tile.objects.begin(), tile.objects.end(), []( const auto & object ) { return object.group == Maps::ObjectGroup::ROADS; } ),
tile.objects.end() );
world.getTile( tileIndex ).removeObjects( MP2::OBJ_ICN_TYPE_ROAD );
}
bool doesContainCastleEntrance( const Maps::Map_Format::TileInfo & tile )
{
const auto & townObjects = Maps::getObjectsByGroup( Maps::ObjectGroup::KINGDOM_TOWNS );
@@ -2050,7 +2042,7 @@ namespace Maps
}
}
else {
removeRoads( tile, tileIndex );
removeRoadsFromTile( tile, tileIndex );
updateRoadSpritesAround( map, tileIndex );
@@ -2071,12 +2063,25 @@ namespace Maps
// After the check this tile should not contain a road sprite.
if ( !forceRoadOnTile && !doesContainRoads( tile ) ) {
// We remove any existing road sprite if this tile does not contain (or was not forced to contain) the main road sprite.
removeRoads( tile, tileIndex );
removeRoadsFromTile( tile, tileIndex );
}
return;
}
writeRoadSpriteToTile( tile, tileIndex, imageIndex );
}
void removeRoadsFromTile( Maps::Map_Format::TileInfo & tile, const int32_t tileIndex )
{
tile.objects.erase( std::remove_if( tile.objects.begin(), tile.objects.end(), []( const auto & object ) { return object.group == Maps::ObjectGroup::ROADS; } ),
tile.objects.end() );
world.getTile( tileIndex ).removeObjects( MP2::OBJ_ICN_TYPE_ROAD );
}
void writeRoadSpriteToTile( Map_Format::TileInfo & tile, const int32_t tileIndex, const uint8_t imageIndex )
{
auto roadObjectIter = std::find_if( tile.objects.begin(), tile.objects.end(), []( const auto & object ) { return object.group == ObjectGroup::ROADS; } );
if ( roadObjectIter != tile.objects.end() ) {
// Since the tile has a road object, update it.
@@ -2098,6 +2103,13 @@ namespace Maps
}
}
void updateAllRoads( Map_Format::MapFormat & map )
{
const int32_t centerTileIndex = Maps::GetIndexFromAbsPoint( map.width / 2, map.width / 2 );
updateRoadSpritesInArea( map, centerTileIndex, map.width, false );
updateRoadSpritesInArea( map, centerTileIndex, map.width, true );
}
bool doesContainRoads( const Map_Format::TileInfo & tile )
{
for ( const auto & object : tile.objects ) {
+3
View File
@@ -102,6 +102,9 @@ namespace Maps
bool updateRoadOnTile( Map_Format::MapFormat & map, const int32_t tileIndex, const bool setRoad );
void updateRoadSpriteOnTile( Map_Format::MapFormat & map, const int32_t tileIndex, const bool forceRoadOnTile );
void removeRoadsFromTile( Map_Format::TileInfo & tile, const int32_t tileIndex );
void writeRoadSpriteToTile( Map_Format::TileInfo & tile, const int32_t tileIndex, const uint8_t imageIndex );
void updateAllRoads( Map_Format::MapFormat & map );
bool doesContainRoads( const Map_Format::TileInfo & tile );
}
+64 -93
View File
@@ -27,7 +27,6 @@
#include <cstddef>
#include <functional>
#include <initializer_list>
#include <list>
#include <map>
#include <ostream>
#include <set>
@@ -51,11 +50,8 @@
#include "mp2.h"
#include "rand.h"
#include "resource.h"
#include "route.h"
#include "skill.h"
#include "translations.h"
#include "world.h"
#include "world_pathfinding.h"
namespace
{
@@ -330,7 +326,7 @@ namespace Maps::Random_Generator
Rand::PCG32 randomGenerator( generatorSeed );
NodeCache data( width, height );
MapStateManager mapState( width, height );
auto mapBoundsCheck = [width, height]( int x, int y ) {
x = std::clamp( x, 0, width - 1 );
@@ -346,7 +342,7 @@ namespace Maps::Random_Generator
// Step 2. Determine region layout and placement.
// Insert empty region that represents water and map edges
std::vector<Region> mapRegions = { { 0, data.getNode( 0 ), neutralColorIndex, Ground::WATER, 1, 0, RegionType::NEUTRAL } };
std::vector<Region> mapRegions = { { 0, mapState.getNodeToUpdate( 0 ), neutralColorIndex, Ground::WATER, 1, 0, RegionType::NEUTRAL } };
const int neutralRegionCount = std::max( 1, expectedRegionCount - config.playerCount );
const int innerLayer = std::min( neutralRegionCount, config.playerCount );
@@ -364,6 +360,7 @@ namespace Maps::Random_Generator
const int regionCount = mapLayers[layer].first;
const double startingAngle = Rand::GetWithGen( 0, 360, randomGenerator );
const double offsetAngle = 360.0 / regionCount;
const bool regionCountIsEven = ( regionCount % config.playerCount ) == 0;
for ( int i = 0; i < regionCount; ++i ) {
const double radians = ( startingAngle + offsetAngle * i ) * M_PI / 180;
const double distance = mapLayers[layer].second;
@@ -380,9 +377,9 @@ namespace Maps::Random_Generator
const int32_t treasureLimit = isPlayerRegion ? regionConfiguration.treasureValueLimit : regionConfiguration.treasureValueLimit * 2;
const uint32_t regionID = static_cast<uint32_t>( mapRegions.size() );
Node & centerNode = data.getNode( centerTile );
const RegionType innerType = ( layer == 0 ) ? RegionType::NEUTRAL : RegionType::EXPANSION;
const RegionType type = isPlayerRegion ? RegionType::STARTING : innerType;
Node & centerNode = mapState.getNodeToUpdate( centerTile );
const RegionType neutralType = ( layer == 1 && regionCountIsEven ) ? RegionType::EXPANSION : RegionType::NEUTRAL;
const RegionType type = isPlayerRegion ? RegionType::STARTING : neutralType;
mapRegions.emplace_back( regionID, centerNode, regionColor, groundType, regionSizeLimit * 6 / 5, treasureLimit, type );
if ( isPlayerRegion ) {
@@ -404,7 +401,7 @@ namespace Maps::Random_Generator
stillRoomToExpand = false;
// Skip the first region which is the border region.
for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) {
if ( mapRegions[regionID].regionExpansion( data, randomGenerator ) ) {
if ( mapRegions[regionID].regionExpansion( mapState, randomGenerator ) ) {
stillRoomToExpand = true;
}
}
@@ -429,10 +426,7 @@ namespace Maps::Random_Generator
}
}
// Step 5. Castles and mines placement
std::set<int32_t> startingLocations;
std::set<int32_t> actionLocations;
// Step 5. Fix terrain transitions and place Castles
MapEconomy mapEconomy;
for ( Region & region : mapRegions ) {
@@ -453,7 +447,7 @@ namespace Maps::Random_Generator
continue;
}
const Node & adjacentNode = data.getNode( adjacentIndex );
const Node & adjacentNode = mapState.getNode( adjacentIndex );
if ( adjacentNode.region == 0 && adjacentNode.index == adjacentIndex ) {
extraNodes.insert( adjacentIndex );
}
@@ -462,7 +456,7 @@ namespace Maps::Random_Generator
}
for ( const int32_t extraNodeIndex : extraNodes ) {
Node & extra = data.getNode( extraNodeIndex );
Node & extra = mapState.getNodeToUpdate( extraNodeIndex );
region.nodes.emplace_back( extra );
extra.region = region.id;
extra.type = NodeType::BORDER;
@@ -470,37 +464,70 @@ namespace Maps::Random_Generator
if ( region.colorIndex != neutralColorIndex ) {
const fheroes2::Point castlePos = region.adjustRegionToFitCastle( mapFormat );
if ( !placeCastle( mapFormat, data, region, castlePos, true ) ) {
if ( !placeCastle( mapFormat, mapState, region, castlePos, true ) ) {
// Return early if we can't place a starting player castle.
DEBUG_LOG( DBG_DEVEL, DBG_WARN, "Not able to place a starting player castle on tile " << castlePos.x << ", " << castlePos.y )
return false;
}
startingLocations.insert( region.centerIndex );
}
else if ( static_cast<int32_t>( region.nodes.size() ) > regionSizeLimit ) {
// Place non-mandatory castles in bigger neutral regions.
const bool useNeutralCastles = ( config.resourceDensity == ResourceDensity::ABUNDANT );
const fheroes2::Point castlePos = region.adjustRegionToFitCastle( mapFormat );
placeCastle( mapFormat, data, region, castlePos, useNeutralCastles );
placeCastle( mapFormat, mapState, region, castlePos, useNeutralCastles );
}
else {
forceTempRoadOnTile( mapFormat, region.centerIndex );
}
}
Maps::updatePlayerRelatedObjects( mapFormat );
// Step 6. Set up region connectors based on frequency settings and border length.
for ( Region & region : mapRegions ) {
if ( region.groundType == Ground::WATER ) {
continue;
}
for ( Node & node : region.nodes ) {
region.checkNodeForConnections( mapState, mapRegions, node );
}
}
for ( const Region & region : mapRegions ) {
for ( const Node & node : region.nodes ) {
if ( node.type == NodeType::BORDER ) {
placeBorderObstacle( mapFormat, mapState, node, randomGenerator );
}
}
}
// Step 7. Place mines.
const auto tryToPlaceMine = [&]( const std::vector<int32_t> & ring, const int resource ) {
for ( const int32_t tileIndex : ring ) {
const auto & node = mapState.getNode( tileIndex );
if ( placeMine( mapFormat, mapState, node, resource ) ) {
mapEconomy.increaseMineCount( resource );
const int32_t mineValue = getObjectGoldValue( getFakeMP2MineType( resource ) );
placeMonster( mapFormat, Maps::GetDirectionIndex( tileIndex, Direction::BOTTOM ), getMonstersByValue( config.monsterStrength, mineValue ) );
return true;
}
}
return false;
};
for ( const Region & region : mapRegions ) {
if ( region.groundType == Ground::WATER ) {
continue;
}
for ( const auto & [regionId, tileIndex] : region.connections ) {
for ( const auto & step : findPathToNearestRoad( mapState, width, region.id, tileIndex ) ) {
mapState.getNodeToUpdate( step ).type = NodeType::PATH;
forceTempRoadOnTile( mapFormat, step );
}
}
const std::vector<std::vector<int32_t>> tileRings = findOpenTilesSortedJittered( region, width, randomGenerator );
const auto tryToPlaceMine = [&]( const std::vector<int32_t> & ring, const int resource ) {
for ( const int32_t tileIndex : ring ) {
const auto & node = data.getNode( tileIndex );
if ( placeMine( mapFormat, data, node, resource ) ) {
mapEconomy.increaseMineCount( resource );
actionLocations.insert( tileIndex );
const int32_t mineValue = getObjectGoldValue( getFakeMP2MineType( resource ) );
placeMonster( mapFormat, Maps::GetDirectionIndex( tileIndex, Direction::BOTTOM ), getMonstersByValue( config.monsterStrength, mineValue ) );
return true;
}
}
return false;
};
if ( tileRings.size() < 4 ) {
continue;
}
@@ -522,58 +549,11 @@ namespace Maps::Random_Generator
}
}
}
Maps::updatePlayerRelatedObjects( mapFormat );
// Step 6. Set up region connectors based on frequency settings and border length.
for ( Region & region : mapRegions ) {
if ( region.groundType == Ground::WATER ) {
continue;
}
for ( Node & node : region.nodes ) {
region.checkNodeForConnections( data, mapRegions, node );
}
}
for ( const Region & region : mapRegions ) {
for ( const Node & node : region.nodes ) {
if ( node.type == NodeType::BORDER ) {
placeRandomObstacle( mapFormat, data, node, randomGenerator );
}
}
}
// Step 7. Set up pathfinder and generate road network.
AIWorldPathfinder pathfinder;
pathfinder.reset();
const PlayerColor testPlayer = PlayerColor::BLUE;
// Have to remove fog first otherwise pathfinder won't work
for ( int idx = 0; idx < width * height; ++idx ) {
world.getTile( idx ).removeFogForPlayers( static_cast<PlayerColorsSet>( testPlayer ) );
}
world.resetPathfinder();
world.updatePassabilities();
// Set explicit paths
for ( const Region & region : mapRegions ) {
if ( region.groundType == Ground::WATER ) {
continue;
}
pathfinder.reEvaluateIfNeeded( region.centerIndex, testPlayer, 999999.9, Skill::Level::EXPERT );
for ( const auto & [regionId, tileIndex] : region.connections ) {
const auto & path = pathfinder.buildPath( tileIndex );
for ( const auto & step : path ) {
data.getNode( step.GetIndex() ).type = NodeType::PATH;
Maps::updateRoadOnTile( mapFormat, step.GetIndex(), true );
}
}
}
// Step 8: Place powerups and treasure clusters while avoiding the paths.
for ( Region & region : mapRegions ) {
placeObjectSet( mapFormat, data, region, powerupObjectSets, config.monsterStrength, regionConfiguration.powerUpsCount, randomGenerator );
placeObjectSet( mapFormat, data, region, prefabObjectSets, config.monsterStrength, regionConfiguration.treasureCount, randomGenerator );
placeObjectSet( mapFormat, mapState, region, powerupObjectSets, config.monsterStrength, regionConfiguration.powerUpsCount, randomGenerator );
placeObjectSet( mapFormat, mapState, region, prefabObjectSets, config.monsterStrength, regionConfiguration.treasureCount, randomGenerator );
}
// TODO: Step 9: Detect and fill empty areas with decorative/flavour objects.
@@ -592,16 +572,7 @@ namespace Maps::Random_Generator
}
}
// Step 11: Validate that map is playable.
for ( const int32_t start : startingLocations ) {
pathfinder.reEvaluateIfNeeded( start, testPlayer, 999999.9, Skill::Level::EXPERT );
for ( const int action : actionLocations ) {
if ( pathfinder.getDistance( action ) == 0 ) {
DEBUG_LOG( DBG_DEVEL, DBG_WARN, "Not able to find path from " << start << " to " << action )
return false;
}
}
}
Maps::updateAllRoads( mapFormat );
// Visual debug
for ( const Region & region : mapRegions ) {
+188 -36
View File
@@ -26,6 +26,7 @@
#include <cstddef>
#include <functional>
#include <initializer_list>
#include <limits>
#include <map>
#include <utility>
@@ -49,10 +50,18 @@
namespace
{
struct RoadBuilderNode final
{
int32_t _from{ -1 };
uint32_t _cost{ 0 };
int _direction{ Direction::UNKNOWN };
};
constexpr int randomCastleIndex{ 12 };
constexpr int randomTownIndex{ 13 };
constexpr int randomHeroIndex{ 7 };
constexpr int maxPlacementAttempts{ 30 };
constexpr uint32_t roadBuilderMargin{ Maps::Ground::defaultGroundPenalty * 3 };
const std::map<int, std::vector<int>> obstaclesPerGround = {
{ Maps::Ground::DESERT, { 24, 25, 26, 27, 28, 29 } }, { Maps::Ground::SNOW, { 30, 31, 32, 33, 34, 35 } }, { Maps::Ground::SWAMP, { 18, 19, 20, 21, 22, 23 } },
@@ -216,10 +225,14 @@ namespace
return true;
}
void markNodeAsType( Maps::Random_Generator::NodeCache & data, const fheroes2::Point position, const Maps::Random_Generator::NodeType type )
void markNodeAsType( Maps::Random_Generator::MapStateManager & data, const fheroes2::Point position, const Maps::Random_Generator::NodeType type )
{
if ( Maps::isValidAbsPoint( position.x, position.y ) ) {
data.getNode( position ).type = type;
auto & node = data.getNodeToUpdate( position );
// Never override border types (no assert needed; can happen when placing overlapping obstacles)
if ( node.type != Maps::Random_Generator::NodeType::BORDER ) {
node.type = type;
}
}
}
}
@@ -326,6 +339,121 @@ namespace Maps::Random_Generator
return objectIndex;
}
std::vector<int32_t> findPathToNearestRoad( const Maps::Random_Generator::MapStateManager & nodes, const int32_t mapWidth, const uint32_t regionId,
const int32_t start )
{
assert( start > 0 );
assert( mapWidth > 0 );
assert( start < mapWidth * mapWidth );
std::vector<RoadBuilderNode> cache( static_cast<size_t>( mapWidth ) * mapWidth );
assert( start < static_cast<int32_t>( cache.size() ) );
std::vector<int32_t> result;
std::vector<int32_t> nodesToExplore;
nodesToExplore.push_back( start );
cache[static_cast<size_t>( start )]._from = start;
cache[static_cast<size_t>( start )]._cost = 0;
const bool fromActionTile = ( nodes.getNode( start ).type == Maps::Random_Generator::NodeType::ACTION );
const Directions & directions = Direction::All();
int32_t bestRoadIndex = -1;
uint32_t bestRoadCost = std::numeric_limits<uint32_t>::max();
for ( size_t lastProcessedNode = 0; lastProcessedNode < nodesToExplore.size(); ++lastProcessedNode ) {
const int32_t currentNodeIdx = nodesToExplore[lastProcessedNode];
const RoadBuilderNode & currentNode = cache[currentNodeIdx];
// Continue to search until margin is hit to find a potential optimal route
if ( bestRoadIndex != -1 && currentNode._cost > bestRoadCost + roadBuilderMargin ) {
break;
}
const bool comeFromStraight = !Direction::isDiagonal( currentNode._direction );
if ( comeFromStraight && currentNodeIdx != start && currentNode._cost < bestRoadCost && world.getTile( currentNodeIdx ).isRoad() ) {
bestRoadIndex = currentNodeIdx;
bestRoadCost = currentNode._cost;
}
for ( const int direction : directions ) {
if ( !Maps::isValidDirection( currentNodeIdx, direction ) ) {
continue;
}
const bool isDiagonal = Direction::isDiagonal( direction );
// Edge case: force straight roads out of action tiles (e.g. mines)
if ( fromActionTile && currentNodeIdx == start && isDiagonal ) {
continue;
}
// Edge case: avoid tight road turns for a better visuals
if ( isDiagonal && currentNode._from != -1 ) {
const int previousDirection = cache[currentNode._from]._direction;
if ( previousDirection != direction && Direction::isDiagonal( previousDirection ) ) {
continue;
}
if ( previousDirection == Direction::BOTTOM && ( currentNode._direction == Direction::LEFT || currentNode._direction == Direction::RIGHT ) ) {
continue;
}
}
const int newIndex = Maps::GetDirectionIndex( currentNodeIdx, direction );
if ( newIndex == start ) {
continue;
}
const Node & other = nodes.getNode( newIndex );
if ( other.region != regionId || ( other.type != Maps::Random_Generator::NodeType::OPEN && other.type != Maps::Random_Generator::NodeType::PATH ) ) {
continue;
}
RoadBuilderNode & newNode = cache[newIndex];
constexpr uint32_t diagonalMovePenalty = Ground::defaultGroundPenalty * 3 / 2;
const uint32_t movementPenalty = isDiagonal ? diagonalMovePenalty : Ground::defaultGroundPenalty;
const uint32_t movementCost = currentNode._cost + movementPenalty;
if ( newNode._from == -1 || newNode._cost > movementCost ) {
newNode._from = currentNodeIdx;
newNode._cost = movementCost;
newNode._direction = direction;
nodesToExplore.push_back( newIndex );
}
}
}
if ( bestRoadIndex == -1 ) {
return result;
}
for ( int32_t currentStep = bestRoadIndex;; ) {
const RoadBuilderNode & rbNode = cache[static_cast<size_t>( currentStep )];
// Adding additional 0 cost road step to fix road transitions to compensate for missing sprites
if ( result.size() == 1 ) {
if ( rbNode._direction == Direction::BOTTOM_RIGHT ) {
result.emplace_back( Maps::GetDirectionIndex( currentStep, Direction::LEFT ) );
}
else if ( rbNode._direction == Direction::BOTTOM_LEFT ) {
result.emplace_back( Maps::GetDirectionIndex( currentStep, Direction::RIGHT ) );
}
}
result.emplace_back( currentStep );
if ( currentStep == rbNode._from ) {
break;
}
currentStep = rbNode._from;
}
return result;
}
std::vector<std::vector<int32_t>> findOpenTilesSortedJittered( const Region & region, int32_t mapWidth, Rand::PCG32 & randomGenerator )
{
if ( region.centerIndex < 0 || region.nodes.empty() || mapWidth <= 0 ) {
@@ -365,7 +493,7 @@ namespace Maps::Random_Generator
return buckets;
}
bool canFitObject( const NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders )
bool canFitObject( const MapStateManager & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders )
{
bool invalid = false;
fheroes2::Rect objectRect;
@@ -377,7 +505,7 @@ namespace Maps::Random_Generator
invalid = true;
return;
}
if ( node.type != NodeType::OPEN && ( skipBorders || node.type != NodeType::BORDER ) ) {
if ( node.type != NodeType::OPEN && ( skipBorders || ( node.type != NodeType::BORDER && node.type != NodeType::OBSTACLE ) ) ) {
invalid = true;
return;
}
@@ -407,7 +535,7 @@ namespace Maps::Random_Generator
return true;
}
bool canFitObjectSet( const NodeCache & data, const ObjectSet & set, const fheroes2::Point & mainTilePos )
bool canFitObjectSet( const MapStateManager & data, const ObjectSet & set, const fheroes2::Point & mainTilePos )
{
for ( const fheroes2::Point offset : set.entranceCheck ) {
const Node & node = data.getNode( mainTilePos + offset );
@@ -447,35 +575,23 @@ namespace Maps::Random_Generator
return true;
}
void markObjectPlacement( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos )
void markObjectPlacement( MapStateManager & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos )
{
fheroes2::Rect objectRect;
auto updateObjectArea = [&data, &mainTilePos, &objectRect]( const auto & partInfo ) {
Node & node = data.getNode( mainTilePos + partInfo.tileOffset );
objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x );
objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x );
node.type = NodeType::OBSTACLE;
};
iterateOverObjectParts( info, updateObjectArea );
iterateOverObjectParts( info, [&data, &mainTilePos]( const auto & partInfo ) { markNodeAsType( data, mainTilePos + partInfo.tileOffset, NodeType::OBSTACLE ); } );
if ( !MP2::isOffGameActionObject( info.objectType ) ) {
return;
}
for ( int x = objectRect.x - 1; x <= objectRect.width + 1; ++x ) {
markNodeAsType( data, mainTilePos + fheroes2::Point{ x, objectRect.height + 1 }, NodeType::PATH );
}
// Mark extra nodes as path to avoid objects clumping together
markNodeAsType( data, mainTilePos + fheroes2::Point{ objectRect.x - 1, 0 }, NodeType::PATH );
markNodeAsType( data, mainTilePos + fheroes2::Point{ objectRect.width + 1, 0 }, NodeType::PATH );
markNodeAsType( data, mainTilePos, NodeType::ACTION );
}
// Wouldn't render correctly but will speed up placement
void forceTempRoadOnTile( Map_Format::MapFormat & mapFormat, const int32_t tileIndex )
{
Maps::writeRoadSpriteToTile( mapFormat.tiles[tileIndex], tileIndex, 0 );
}
bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tile & tile, const ObjectGroup groupType, const int32_t objectIndex )
{
const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex );
@@ -499,19 +615,35 @@ namespace Maps::Random_Generator
return true;
}
bool placeActionObject( Map_Format::MapFormat & mapFormat, NodeCache & data, Tile & tile, const ObjectGroup groupType, const int32_t type )
bool placeActionObject( Map_Format::MapFormat & mapFormat, MapStateManager & data, Tile & tile, const ObjectGroup groupType, const int32_t type )
{
const fheroes2::Point tilePos = tile.GetCenter();
const auto & objectInfo = Maps::getObjectInfo( groupType, type );
if ( canFitObject( data, objectInfo, tilePos, true ) && putObjectOnMap( mapFormat, tile, groupType, type ) ) {
if ( canFitObject( data, objectInfo, tilePos, true ) ) {
MapStateTransaction transaction( data );
markObjectPlacement( data, objectInfo, tilePos );
return true;
const int32_t tileIndex = tile.GetIndex();
const auto & roadToObject = findPathToNearestRoad( data, mapFormat.width, data.getNode( tileIndex ).region, tileIndex );
if ( roadToObject.empty() ) {
return false;
}
if ( putObjectOnMap( mapFormat, tile, groupType, type ) ) {
for ( const auto & step : roadToObject ) {
data.getNodeToUpdate( step ).type = NodeType::PATH;
forceTempRoadOnTile( mapFormat, step );
}
transaction.commit();
return true;
}
}
return false;
}
bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle )
bool placeCastle( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle )
{
auto & tile = world.getTile( tilePos.x, tilePos.y );
if ( tile.isWater() ) {
@@ -577,6 +709,7 @@ namespace Maps::Random_Generator
// Force roads coming from the castle
const int32_t nextIndex = Maps::GetDirectionIndex( bottomIndex, Direction::BOTTOM );
if ( Maps::isValidAbsIndex( nextIndex ) ) {
data.getNodeToUpdate( bottomIndex ).type = NodeType::PATH;
Maps::updateRoadOnTile( mapFormat, bottomIndex, true );
Maps::updateRoadOnTile( mapFormat, nextIndex, true );
}
@@ -584,14 +717,14 @@ namespace Maps::Random_Generator
return true;
}
bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & node, const int resource )
bool placeMine( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & node, const int resource )
{
Tile & mineTile = world.getTile( node.index );
const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() );
return placeActionObject( mapFormat, data, mineTile, ObjectGroup::ADVENTURE_MINES, mineType );
}
bool placeRandomObstacle( Map_Format::MapFormat & mapFormat, const NodeCache & data, const Node & node, Rand::PCG32 & randomGenerator )
bool placeBorderObstacle( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & node, Rand::PCG32 & randomGenerator )
{
Tile & tile = world.getTile( node.index );
const auto it = obstaclesPerGround.find( tile.GetGround() );
@@ -606,6 +739,7 @@ namespace Maps::Random_Generator
for ( const auto & obstacleId : obstacleList ) {
const auto & objectInfo = Maps::getObjectInfo( ObjectGroup::LANDSCAPE_TREES, obstacleId );
if ( canFitObject( data, objectInfo, tilePos, false ) && putObjectOnMap( mapFormat, tile, ObjectGroup::LANDSCAPE_TREES, obstacleId ) ) {
markObjectPlacement( data, objectInfo, tilePos );
return true;
}
}
@@ -632,7 +766,7 @@ namespace Maps::Random_Generator
}
}
bool placeSimpleObject( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & centerNode, const ObjectPlacement & placement )
bool placeSimpleObject( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & centerNode, const ObjectPlacement & placement )
{
const fheroes2::Point position = Maps::GetPoint( centerNode.index ) + placement.offset;
if ( !Maps::isValidAbsPoint( position.x, position.y ) ) {
@@ -649,8 +783,8 @@ namespace Maps::Random_Generator
return false;
}
void placeObjectSet( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, std::vector<ObjectSet> objects, const MonsterStrength monsterStrength,
const uint8_t expectedCount, Rand::PCG32 & randomGenerator )
void placeObjectSet( Map_Format::MapFormat & mapFormat, MapStateManager & data, Region & region, std::vector<ObjectSet> objectSets,
const MonsterStrength monsterStrength, const uint8_t expectedCount, Rand::PCG32 & randomGenerator )
{
int objectsPlaced = 0;
for ( int attempt = 0; attempt < maxPlacementAttempts; ++attempt ) {
@@ -660,12 +794,30 @@ namespace Maps::Random_Generator
const Node & node = Rand::GetWithGen( region.nodes, randomGenerator );
Rand::ShuffleWithGen( objects, randomGenerator );
for ( const auto & prefab : objects ) {
Rand::ShuffleWithGen( objectSets, randomGenerator );
for ( const auto & prefab : objectSets ) {
if ( !canFitObjectSet( data, prefab, Maps::GetPoint( node.index ) ) ) {
continue;
}
MapStateTransaction transaction( data );
for ( const auto & obstacle : prefab.obstacles ) {
const fheroes2::Point position = Maps::GetPoint( node.index ) + obstacle.offset;
const auto & objectInfo = Maps::getObjectInfo( obstacle.groupType, obstacle.objectIndex );
markObjectPlacement( data, objectInfo, position );
}
const auto & routeToGroup = findPathToNearestRoad( data, mapFormat.width, region.id, node.index );
if ( routeToGroup.empty() ) {
continue;
}
for ( const auto & step : routeToGroup ) {
data.getNodeToUpdate( step ).type = NodeType::PATH;
forceTempRoadOnTile( mapFormat, step );
}
transaction.commit();
for ( const auto & obstacle : prefab.obstacles ) {
placeSimpleObject( mapFormat, data, node, obstacle );
}
+14 -11
View File
@@ -35,7 +35,7 @@ namespace Maps
namespace Maps::Random_Generator
{
class NodeCache;
class MapStateManager;
struct Node;
struct ObjectPlacement;
struct ObjectSet;
@@ -66,19 +66,22 @@ namespace Maps::Random_Generator
MonsterSelection getMonstersByValue( const MonsterStrength monsterStrength, int32_t protectedObjectValue );
std::pair<ObjectGroup, int32_t> getRandomTreasure( const int32_t goldValueLimit, Rand::PCG32 & randomGenerator );
int32_t selectTerrainVariantForObject( const ObjectGroup groupType, const int32_t objectIndex, const int groundType );
std::vector<int32_t> findPathToNearestRoad( const Maps::Random_Generator::MapStateManager & nodes, const int32_t mapWidth, const uint32_t regionId,
const int32_t start );
std::vector<std::vector<int32_t>> findOpenTilesSortedJittered( const Region & region, int32_t mapWidth, Rand::PCG32 & randomGenerator );
bool canFitObject( const NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders );
bool canFitObjectSet( const NodeCache & data, const ObjectSet & set, const fheroes2::Point & mainTilePos );
void markObjectPlacement( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos );
bool canFitObject( const MapStateManager & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders );
bool canFitObjectSet( const MapStateManager & data, const ObjectSet & set, const fheroes2::Point & mainTilePos );
void markObjectPlacement( MapStateManager & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos );
void forceTempRoadOnTile( Map_Format::MapFormat & mapFormat, const int32_t tileIndex );
bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tile & tile, const ObjectGroup groupType, const int32_t objectIndex );
bool placeActionObject( Map_Format::MapFormat & mapFormat, NodeCache & data, Tile & tile, const ObjectGroup groupType, const int32_t type );
bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle );
bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & node, const int resource );
bool placeRandomObstacle( Map_Format::MapFormat & mapFormat, const NodeCache & data, const Node & node, Rand::PCG32 & randomGenerator );
bool placeActionObject( Map_Format::MapFormat & mapFormat, MapStateManager & data, Tile & tile, const ObjectGroup groupType, const int32_t type );
bool placeCastle( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle );
bool placeMine( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & node, const int resource );
bool placeBorderObstacle( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & node, Rand::PCG32 & randomGenerator );
void placeMonster( Map_Format::MapFormat & mapFormat, const int32_t index, const MonsterSelection & monster );
bool placeSimpleObject( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & centerNode, const ObjectPlacement & placement );
void placeObjectSet( Map_Format::MapFormat & mapFormat, NodeCache & data, Region & region, std::vector<ObjectSet> objects, const MonsterStrength monsterStrength,
const uint8_t expectedCount, Rand::PCG32 & randomGenerator );
bool placeSimpleObject( Map_Format::MapFormat & mapFormat, MapStateManager & data, const Node & centerNode, const ObjectPlacement & placement );
void placeObjectSet( Map_Format::MapFormat & mapFormat, MapStateManager & data, Region & region, std::vector<ObjectSet> objects,
const MonsterStrength monsterStrength, const uint8_t expectedCount, Rand::PCG32 & randomGenerator );
}
@@ -39,7 +39,7 @@ namespace
namespace Maps::Random_Generator
{
NodeCache::NodeCache( const int32_t width, const int32_t height )
MapStateManager::MapStateManager( const int32_t width, const int32_t height )
: _mapSize( width )
, _data( static_cast<size_t>( width ) * height )
{
@@ -51,6 +51,44 @@ namespace Maps::Random_Generator
}
}
void MapStateManager::commitTransaction( const size_t record )
{
assert( !_transactionRecords.empty() );
assert( _transactionRecords.back() == record );
_transactionRecords.pop_back();
if ( _transactionRecords.empty() ) {
_history.clear();
}
else {
_history.resize( record );
}
}
void MapStateManager::rollbackTransaction( const size_t record )
{
assert( !_transactionRecords.empty() );
assert( _transactionRecords.back() == record );
_transactionRecords.pop_back();
if ( _history.empty() ) {
return;
}
for ( size_t index = _history.size() - 1; index > record; --index ) {
const StateChange & change = _history[index];
_data[static_cast<size_t>( change.index )] = change.state;
}
if ( _transactionRecords.empty() ) {
_history.clear();
}
else {
_history.resize( record );
}
}
void MapEconomy::increaseMineCount( const int resourceType )
{
const auto it = _minesCount.find( resourceType );
@@ -67,7 +105,7 @@ namespace Maps::Random_Generator
return *it;
}
void Region::checkAdjacentTiles( NodeCache & rawData, const double distanceLimit, Rand::PCG32 & randomGenerator )
void Region::checkAdjacentTiles( MapStateManager & rawData, const double distanceLimit, Rand::PCG32 & randomGenerator )
{
Node & previousNode = nodes[lastProcessedNode];
const int nodeIndex = previousNode.index;
@@ -85,7 +123,7 @@ namespace Maps::Random_Generator
continue;
}
Node & newTile = rawData.getNode( newPosition );
Node & newTile = rawData.getNodeToUpdate( newPosition );
if ( Maps::GetApproximateDistance( centerIndex, newTile.index ) > distanceLimit ) {
previousNode.type = NodeType::BORDER;
@@ -109,7 +147,7 @@ namespace Maps::Random_Generator
}
}
bool Region::regionExpansion( NodeCache & rawData, Rand::PCG32 & randomGenerator )
bool Region::regionExpansion( MapStateManager & rawData, Rand::PCG32 & randomGenerator )
{
// Process only "open" nodes that exist at the start of the loop and ignore what's added.
const size_t nodesEnd = nodes.size();
@@ -122,7 +160,7 @@ namespace Maps::Random_Generator
return lastProcessedNode != nodes.size();
}
bool Region::checkNodeForConnections( NodeCache & data, std::vector<Region> & mapRegions, Node & node )
bool Region::checkNodeForConnections( MapStateManager & data, std::vector<Region> & mapRegions, Node & node )
{
if ( node.type != NodeType::BORDER ) {
return false;
@@ -150,7 +188,7 @@ namespace Maps::Random_Generator
}
for ( uint8_t direction = 0; direction < 4; ++direction ) {
Node & adjacent = data.getNode( position + directionOffsets[direction] );
Node & adjacent = data.getNodeToUpdate( position + directionOffsets[direction] );
if ( adjacent.index == -1 || adjacent.region == id ) {
continue;
@@ -161,7 +199,7 @@ namespace Maps::Random_Generator
break;
}
Node & twoAway = data.getNode( position + directionOffsets[direction] + directionOffsets[direction] );
Node & twoAway = data.getNodeToUpdate( position + directionOffsets[direction] + directionOffsets[direction] );
if ( twoAway.index == -1 || twoAway.type != NodeType::OPEN ) {
continue;
}
@@ -174,7 +212,7 @@ namespace Maps::Random_Generator
adjacent.type = NodeType::PATH;
twoAway.type = NodeType::PATH;
Node & stepBack = data.getNode( position - directionOffsets[direction] );
Node & stepBack = data.getNodeToUpdate( position - directionOffsets[direction] );
if ( stepBack.index != -1 ) {
stepBack.type = NodeType::PATH;
}
+100 -8
View File
@@ -84,12 +84,12 @@ namespace Maps::Random_Generator
}
};
class NodeCache final
class MapStateManager final
{
public:
NodeCache( const int32_t width, const int32_t height );
MapStateManager( const int32_t width, const int32_t height );
Node & getNode( const fheroes2::Point position )
Node & getNodeToUpdate( const fheroes2::Point position )
{
if ( position.x < 0 || position.x >= _mapSize || position.y < 0 || position.y >= _mapSize ) {
// We shouldn't try to get a tile with an invalid index.
@@ -100,7 +100,12 @@ namespace Maps::Random_Generator
return _outOfBounds;
}
return _data[position.y * _mapSize + position.x];
const int32_t index = position.y * _mapSize + position.x;
if ( !_transactionRecords.empty() ) {
recordStateChange( index, _data[index] );
}
return _data[index];
}
const Node & getNode( const fheroes2::Point position ) const
@@ -117,7 +122,26 @@ namespace Maps::Random_Generator
return _data[position.y * _mapSize + position.x];
}
Node & getNode( const int32_t index )
Node & getNodeToUpdate( const int32_t index )
{
if ( index < 0 || index >= _mapSize * _mapSize ) {
// We shouldn't try to get a tile with an invalid index.
assert( 0 );
assert( _outOfBounds.type == NodeType::BORDER );
assert( _outOfBounds.index == -1 );
return _outOfBounds;
}
if ( !_transactionRecords.empty() ) {
recordStateChange( index, _data[index] );
}
return _data[index];
}
const Node & getNode( const int32_t index ) const
{
if ( index < 0 || index >= _mapSize * _mapSize ) {
// We shouldn't try to get a tile with an invalid index.
@@ -133,9 +157,77 @@ namespace Maps::Random_Generator
}
private:
friend class MapStateTransaction;
struct StateChange final
{
int32_t index{ -1 };
Node state;
StateChange() = default;
explicit StateChange( const int32_t index_, const Node & current )
: index( index_ )
, state( current )
{
// Do nothing
}
};
void recordStateChange( const int32_t index, const Node & current )
{
_history.emplace_back( index, current );
}
size_t startTransaction()
{
const size_t record = _history.size();
_transactionRecords.push_back( record );
return record;
}
void commitTransaction( const size_t record );
void rollbackTransaction( const size_t record );
const int32_t _mapSize{ 0 };
Node _outOfBounds{ -1, 0, NodeType::BORDER };
std::vector<Node> _data;
std::vector<StateChange> _history;
std::vector<size_t> _transactionRecords;
};
class MapStateTransaction final
{
public:
explicit MapStateTransaction( MapStateManager & manager )
: _manager( manager )
, _record( _manager.startTransaction() )
{
// Do nothing.
}
MapStateTransaction( const MapStateTransaction & ) = delete;
MapStateTransaction & operator=( const MapStateTransaction & ) = delete;
~MapStateTransaction()
{
if ( !_committed ) {
_manager.rollbackTransaction( _record );
}
}
void commit()
{
assert( !_committed );
_manager.commitTransaction( _record );
_committed = true;
}
private:
MapStateManager & _manager;
size_t _record{ 0 };
bool _committed{ false };
};
enum class RegionType : uint8_t
@@ -178,9 +270,9 @@ namespace Maps::Random_Generator
nodes.emplace_back( centerNode );
}
void checkAdjacentTiles( NodeCache & rawData, const double distanceLimit, Rand::PCG32 & randomGenerator );
bool regionExpansion( NodeCache & rawData, Rand::PCG32 & randomGenerator );
bool checkNodeForConnections( NodeCache & data, std::vector<Region> & mapRegions, Node & node );
void checkAdjacentTiles( MapStateManager & rawData, const double distanceLimit, Rand::PCG32 & randomGenerator );
bool regionExpansion( MapStateManager & rawData, Rand::PCG32 & randomGenerator );
bool checkNodeForConnections( MapStateManager & data, std::vector<Region> & mapRegions, Node & node );
fheroes2::Point adjustRegionToFitCastle( const Map_Format::MapFormat & mapFormat );
};